-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.njk
More file actions
executable file
·217 lines (182 loc) · 7.53 KB
/
Copy pathsearch.njk
File metadata and controls
executable file
·217 lines (182 loc) · 7.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
---
title: Search my site
date: 2020-09-12 21:24:50
layout: layouts/base.njk
templateEngineOverride: njk
noindex: true
---
<div data-pagefind-ignore>
<h1 id="search-heading">{{title}}</h1>
<div class="kh-search-widget">
<fieldset id="search-mode" class="kh-search-mode" hidden>
<legend class="kh-u-sr-only">Search mode</legend>
<label class="kh-search-mode__option">
<input type="radio" name="search_mode" value="keyword" checked>
Search by keyword
</label>
<label class="kh-search-mode__option">
<input type="radio" name="search_mode" value="semantic">
Search semantically
</label>
</fieldset>
<div class="kh-search-widget__panel">
<div id="keyword-section">
<p><small><a href="/posts/20260228-replacing-lunr-with-pagefind/">Runs locally in browser</a> and powered by Pagefind.</small></p>
<form action="/search/" method="GET" class="kh-search-widget__form">
<label class="kh-u-sr-only" for="search">Search my site</label>
<input type="search" id="search" placeholder="Search my site" name="search_query" class="kh-form__input">
<button type="submit" class="kh-button" aria-label="Search">
<span class="kh-button__label" aria-hidden="true">Search</span>
<span class="kh-button__icon" aria-hidden="true">→</span>
</button>
</form>
<noscript>
<p>Search requires JavaScript. You can also try
<a href="https://duckduckgo.com/?q=site%3Aallaboutken.com">searching on DuckDuckGo</a>.
</p>
</noscript>
<div class="kh-content kh-grid kh-search-client-side">
<div id="search-results"></div>
</div>
</div>
<div id="semantic-section" hidden>
<p>Search by describing what you're looking for in your own words.</p>
<p><small>This <a href="/posts/20260302-semantic-search-browser-embeddings/">searches by meaning</a>, not exact keywords -- and everything runs locally in your browser.</small></p>
<div class="kh-ask" aria-label="Semantic search conversation">
<div class="kh-ask__conversation" id="ask-conversation" role="log" aria-live="polite" aria-relevant="additions">
</div>
<form class="kh-search-widget__form" id="ask-form" action="/search/" method="GET">
<label class="kh-u-sr-only" for="ask-input">Ask a question about this site</label>
<input type="text" id="ask-input" name="q" placeholder="e.g. articles about improving search" class="kh-form__input" autocomplete="off">
<button type="submit" class="kh-button" aria-label="Ask">
<span class="kh-button__label" aria-hidden="true">Ask</span>
<span class="kh-button__icon" aria-hidden="true">→</span>
</button>
</form>
</div>
</div>
</div>
</div>
<script type="module">
import { initSemanticSearch } from '/semantic-search.js';
// --- Shared utilities ---
const escDiv = document.createElement('div');
function esc(s) {
escDiv.textContent = s;
return escDiv.innerHTML;
}
const params = new URLSearchParams(window.location.search);
// --- Mode toggle ---
const modeFieldset = document.getElementById('search-mode');
const keywordSection = document.getElementById('keyword-section');
const semanticSection = document.getElementById('semantic-section');
const modeRadios = modeFieldset.querySelectorAll('input[name="search_mode"]');
// Show the toggle (requires JS); visually hide the heading (tabs replace it)
modeFieldset.removeAttribute('hidden');
document.getElementById('search-heading').classList.add('kh-u-sr-only');
// Exposed on window for inline onclick fallback links
window.setMode = setMode;
let askInput; // set after module init; setMode() uses it at call time
function setMode(mode) {
const isSemantic = mode === 'semantic';
const [show, hide] = isSemantic ? [semanticSection, keywordSection] : [keywordSection, semanticSection];
hide.setAttribute('hidden', '');
show.removeAttribute('hidden');
modeFieldset.querySelector(`input[value="${mode}"]`).checked = true;
(isSemantic ? askInput : input).focus();
const url = new URL(window.location);
if (isSemantic) {
url.searchParams.set('search_type', 'semantic');
} else {
url.searchParams.delete('search_type');
}
window.history.replaceState({}, '', url);
}
modeRadios.forEach(radio => {
radio.addEventListener('change', () => {
setMode(radio.value);
// Carry query across when switching
if (radio.value === 'semantic' && input.value.trim()) {
askInput.value = input.value.trim();
} else if (radio.value === 'keyword' && askInput.value.trim()) {
input.value = askInput.value.trim();
}
});
});
// --- Keyword search (Pagefind) ---
const input = document.getElementById('search');
const resultsEl = document.getElementById('search-results');
let pagefind = null;
let debounceTimer = null;
async function initPagefind() {
if (!pagefind) {
pagefind = await import('/pagefind/pagefind.js');
await pagefind.options({ excerptLength: 30 });
}
return pagefind;
}
async function doSearch(query) {
if (!query || !query.trim()) {
resultsEl.innerHTML = '';
return;
}
resultsEl.innerHTML = '<p>Searching...</p>';
try {
const pf = await initPagefind();
const search = await pf.search(query);
if (search.results.length === 0) {
resultsEl.innerHTML = '<p>No results found.</p>';
return;
}
const toShow = search.results.slice(0, 10);
const fragments = await Promise.all(toShow.map(r => r.data()));
const total = search.results.length;
const shown = toShow.length;
const countText = total > shown
? `Showing ${shown} of ${total} results.`
: `${total} result${total === 1 ? '' : 's'} found.`;
const html = fragments.map(item => `
<article class="kh-search-result">
<a href="${esc(item.url)}"><h3>${esc(item.meta?.title || item.url)}</h3></a>
<p class="snippet">${item.excerpt}</p>
<p><code>${esc(item.url)}</code></p>
</article>
`).join('');
resultsEl.innerHTML = `<p>${countText}</p>` + html;
} catch (err) {
console.error('Search failed:', err);
resultsEl.innerHTML = '<p>Search is temporarily unavailable. Try <a href="https://duckduckgo.com/?q=site%3Aallaboutken.com">searching on DuckDuckGo</a> instead.</p>';
}
}
const initialQuery = params.get('search_query');
if (initialQuery) {
input.value = initialQuery;
doSearch(initialQuery);
}
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => doSearch(input.value), 300);
});
input.closest('form').addEventListener('submit', (e) => {
e.preventDefault();
clearTimeout(debounceTimer);
const query = input.value;
const url = new URL(window.location);
url.searchParams.set('search_query', query);
window.history.replaceState({}, '', url);
doSearch(query);
});
// --- Semantic search ---
const semantic = initSemanticSearch({ esc, setMode });
askInput = semantic.askInput;
// --- Initial state from URL params ---
if (params.get('search_type') === 'semantic') {
setMode('semantic');
const semanticQuery = params.get('q') || params.get('search_query') || '';
if (semanticQuery) {
askInput.value = semanticQuery;
semantic.handleQuery(semanticQuery);
}
}
</script>
</div>