Search on a static site, without a server
I once wrote that a static site gives up search. That was too pessimistic. This blog now has a search page, and there is still no code running on the server.
How it works#
Pagefind runs right after Hugo. It reads the generated pages and writes a search index next to them: a folder of small files in /pagefind/. The search page loads Pagefind’s script, which fetches only the parts of the index it needs for the words you type. The search happens in your browser. Nothing is sent anywhere, and there is no outside service.
Only posts go into the index. The theme marks the body of each post with data-pagefind-body, and pages without that mark, like the home page and the tag pages, are skipped. A check after every build compares the number of indexed pages with the number of posts.
The price: JavaScript, on one page#
Until now this site had no JavaScript at all, and the server’s security policy said so: default-src 'none' and no script-src. Search needs scripts, so something had to give. The question was how little.
The answer is a second policy that applies to two paths only. nginx picks the policy by URL:
map $uri $csp {
default "default-src 'none'; img-src 'self'; ...";
~^/(search|pagefind)/ "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self'; ...";
}
On the search page, scripts may come from this site only, Pagefind may compile its WebAssembly, and it may fetch the index from this site. Inline scripts are still blocked. Every other page keeps the old policy and cannot run JavaScript at all.
Workers bring their own policy#
Pagefind does the actual searching in a Web Worker: a background script, loaded from /pagefind/. A worker does not take the policy of the page that started it. It gets the one sent with its own file. With the strict policy on /pagefind/, the worker would not have been allowed to fetch the index or compile WebAssembly, so that folder gets the search policy too. It only holds files that Pagefind generated.
This is easy to miss, because a local preview server sends no policy at all and search works there either way. So the tests run against the real nginx container.
Keeping the exception small#
- The site check fails if a
<script>appears on any page other than the search page, or if the search page contains inline code. - The container test checks that the home page, posts, tag pages and a 404 get the strict policy, and that only
/search/and/pagefind/get the other one. - Pagefind is pinned like every other tool, with a checksum. I skipped the newest release, which was five days old, and took the one before it.
Rolling it out#
The policy lives in the nginx image, but the site files do not. So this change needed a new image on the server as well as the usual deploy. In between, the search page showed a short message pointing to the tags instead of a broken search box. The page starts out with that message and a disabled box, and the script only replaces them once Pagefind has loaded.
Try it#
Search for a word. The query ends up in the address bar, so a search like /search/?q=nginx can be shared as a link.