Deploy with correct caching
Search works on any static host with no configuration at all. This guide is about not leaving performance on the table: without cache headers you pay revalidation on artifacts that could have been cached for a year, and with a strict CSP you can silently lose the semantic half of the engine.
The headers file
Everything chops-search emits under /search/ carries a content hash
except manifest.json and the three runtime files. Copy this to
static/_headers (Cloudflare Workers static assets and Pages read it
natively; so does Netlify):
/search/model.*
Cache-Control: public, max-age=31536000, immutable
/search/index.*
Cache-Control: public, max-age=31536000, immutable
/search/snippets.*
Cache-Control: public, max-age=31536000, immutable
/search/pkg/*
Cache-Control: public, max-age=31536000, immutable
# The manifest names every hashed file, so it must never go stale.
/search/manifest.json
Cache-Control: public, max-age=0, must-revalidate
# Unhashed runtime, loaded on every page. Five minutes bounds upgrade
# staleness without a conditional request per navigation.
/search/chops-search.js
Cache-Control: public, max-age=300
/search/chops-search.css
Cache-Control: public, max-age=300
/search/search-worker.js
Cache-Control: public, max-age=300
The wasm under /search/pkg/ is unhashed on disk but always requested with
?v=<build hash> by the worker, so pinning it is safe: a rebuild changes the
query string and therefore the browser's cache key. And a stale page script
paired with fresh artifacts degrades to "search unavailable", never to wrong
results, which is why five minutes of runtime staleness is acceptable.
Verify after deploying:
curl -sI https://your.site/search/manifest.json | grep -i cache-controlThe three afternoon-eaters
Content-Type: application/wasm. Streaming wasm instantiation fails without it. Most hosts get this right; verify only if you front yours with something unusual.- CSP. Wasm compilation needs
'wasm-unsafe-eval'inscript-src, the worker needsworker-src 'self', and range fetches needconnect-src 'self'. Missing any of them shows as "search unavailable" rather than an obvious error. With tabi'senable_csp = true:
If a directive is already defined for another purpose (an analytics endpoint inallowed_domains = [ { directive = "script-src", domains = ["'self'", "'wasm-unsafe-eval'"] }, { directive = "worker-src", domains = ["'self'"] }, { directive = "connect-src", domains = ["'self'"] }, ]connect-src, say), make sure'self'is in its list; a defined directive that omits it silently blocks the range fetches. - Range requests. Cloudflare, Netlify, and S3 honour them; some dev
servers don't (
zola serveincluded). The worker tolerates a 200-instead-of-206 by ingesting the whole file, so a range-hostile host degrades to eager loading rather than breaking. Test range behaviour against a real preview deploy, and read the network tab: per-query requests should be partial responses of a kilobyte or less.