Search without a server
The search box is a <dialog> with an input, a list, and about a hundred lines of JavaScript. There is no search service and no API call at query time.
The index
At build time, every post becomes one entry:
{
"title": "Search without a server",
"url": "/posts/search-without-a-server/",
"date": "2026-09-12",
"tags": ["search", "javascript"],
"description": "How the search overlay works...",
"text": "The search box is a dialog..."
}
The text field is the rendered post with the HTML stripped out. It is capped at 12 KB per post, which is plenty for matching and small enough that the whole index stays quick to parse.
The matching
Queries are split into tokens and normalised. Each token must match somewhere; title matches are worth more than body matches:
export function scorePost(post, tokens) {
const title = normalizeText(post.title);
const text = normalizeText(post.text);
let score = 0;
for (const token of tokens) {
let points = 0;
if (title === token) points += 40;
else if (title.startsWith(token)) points += 24;
else if (title.includes(token)) points += 16;
if (text.includes(token)) points += 2;
if (!points) return 0;
score += points;
}
return score;
}
Prefix matching means partial words work: typing stat already finds this post. Requiring every token keeps multi-word queries precise.
Why not client-side fuzzy search?
Fuzzy matching is lovely for command palettes and frustrating for prose. A blog search usually knows what it is looking for: a title, a tag, or a phrase. Exact substring matching is predictable, explainable, and fast enough that the results appear between keystrokes.
The index is fetched the first time the dialog opens, and cached for the rest of the visit. Until then, it costs nothing.