Search on a bilingual static site: the Pagefind trade-offs

A static site has no backend, so search has to be baked in at build time. Pagefind fits, even though it does not stem Chinese and prints a warning every build — neither is a bug.

There are two ways to do search on a static site: send queries to a third party, or bake the index into the build output. I picked the second, because I did not want an external dependency for one input box, and I did not want visitors’ queries passing through someone else’s server.

Why Pagefind

A few options were tried; Pagefind stayed for three reasons:

  • The index is produced after the build as plain static files, with zero hosting configuration
  • It splits the index per page and only loads a fragment when a search actually happens
  • It supports multiple languages, so one site can carry several indexes

The cost is that it only finds what existed at build time. Dynamic data (a playlist, for instance) is not searchable — a reasonable boundary, since search here is for articles.

The build produces one index per language and splits it into shards; a query loads only the current language. Only pages marked with data-pagefind-body are indexed

Figure: indexes are built at build time, split by language, and sliced per page.

Not stemming Chinese is not a bug

Every build prints:

Note: Pagefind doesn't support stemming for the language zh-cn.

The first time I assumed a misconfiguration. It is by design: Pagefind’s stemmer is Snowball-based and only covers Latin languages. Chinese has no inflection, so stemming would be wrong — character-level matching is the correct behaviour.

The note cannot be disabled (it is hardcoded in the binary; --quiet does not suppress it). The build script filters that exact line while forwarding all other stderr and preserving the exit code. Do not switch to --silent for quiet: it swallows real errors too.

Declare the index scope

Pagefind scans every HTML file by default, and this site has plenty of pages that should never appear in results: tag pages, archives, pagination, 404. Indexing them all only pollutes results.

So only article bodies carry the marker:

<article data-pagefind-body>

Only pages with that attribute get indexed, and the build log states “Indexed N pages” — if N looks wrong, the scope is wrong.

Two indexes, one per language

Each language gets its own index, and the query loads the one matching the current locale. Chinese queries cannot hit English pages, and results never point at content in another language.

The cost is roughly double the build time — about one extra second for 600 pages. Acceptable.

When not to use it

Under a dozen articles, search is a liability: one more network request and one more interaction point, while a reader can simply browse the list. My threshold is more than two pages of articles; below that, no search.

Right now it indexes 600 pages and 18k words for about a second of build time. That is a fair price for a search box that works.

← Back to all posts

Comments

…