Full-text search on a static site: index at build time, no backend at runtime
A static site does not need Algolia to have search. Generate the index during the build, run the query entirely in the browser, and pay a few seconds of build time.
The one requirement that reliably stalls a static site is search: there is no server, so who runs the query? My answer is nobody — build the index ahead of time and let the browser do the matching. The tool is Pagefind: one command, no backend.
Why not plug in a search service
The options I ruled out first.
| Approach | What it costs |
|---|---|
| Hosted search (Algolia, DocSearch, …) | An account, a quota, a third-party script in every page, and every keystroke routed through someone else’s server |
| Ship one JSON index, filter in the browser | Every visitor downloads every word. Fine at ten posts, megabytes at a hundred |
| No search at all | Then the site is only navigable |
The first two share one flaw: they move the cost onto the reader — one as a third-party request, one as bandwidth. The text on this site is already a static file by the time the build ends. The index has no reason to be produced at any other moment.
Where Pagefind sits
Late in the pipeline:
npm run build
# = astro build && pagefind --site dist
Astro emits all the HTML, then Pagefind walks dist/, splits it, compresses it, shards it, and writes dist/pagefind/. The browser later loads a small runtime and fetches only the shards it actually needs.
So it is not a search service. It is part of the build output, uploaded alongside the HTML, CSS and images.
The last lines of a build look like this:
Indexed 3 languages
Indexed 94 pages
Indexed 2883 words
Indexed 3 filters
What gets indexed, and what does not
By default it swallows every readable string on the page, which is rarely what you want: headers, footers and language switchers repeat on every page, and indexing them just fills your result set with “Home” and “Blog”.
Two attributes are enough:
data-pagefind-body— indexing is restricted to this subtree, so chrome is excluded automatically.data-pagefind-filter="lang:zh"— tag the page so queries can filter on it.
On this site both layouts put both attributes on <main>:
<main id="main" data-pagefind-body data-pagefind-filter="lang:zh">
That Indexed 3 filters line is those three language facets.
Search quality depends on what you exclude as much as on what you include.
Four things to remember
① It does not exist under npm run dev. The index is a build artefact and dev only runs Astro. Getting nothing in dev is expected, not a bug; verifying search requires a real build followed by preview. I spent a while convinced search was broken.
② The order is not negotiable. Pagefind reads dist/, so it must run after the Astro build (do not get the && wrong), and it only sees the final HTML — markers you cannot find in source may already be inlined in the output.
③ Pages rendered by JavaScript barely get indexed. Pagefind reads HTML files. The bodies of my games and tools are mounted on the client, so only the static shell is indexable. Making them searchable would mean rendering the important text at build time — one of the reasons my app pages are a static shell with static content.
④ Do not grep the output for markers. Astro inlines small styles and scripts into the HTML, so the compressed output is not the source you wrote. Verify search by running a real build and preview.
When it is the wrong tool
- Cross-language search (one query returning zh, en and ja hits): Pagefind keeps one index per language; merging is on you.
- Stemming, synonyms, typo tolerance: it is prefix matching, not semantics.
- Indexing user content in real time: not a static site’s job in the first place.
I need none of the three. This blog is a few dozen hand-written pages that rarely change — at that scale, “build the index up front” is the only shape that makes sense.
If a query can be answered without a server, it should not touch one.

Comments
…