Table Of Contents
Ken Hawkins builds web and analytics infrastructure for large organizations, EMBL and UNDRR among them, and writes about it on a personal site built with Eleventy. The source is public, which is how I ended up reading his build scripts at eleven at night instead of grading. Earlier this year I read his post on semantic search on a static site with no API keys, built a version of it for thewhitestonefoundation.org, ran it in production across my whole fleet of sites, and then tore the entire thing out in July and replaced it with Pagefind.
That is not a story about Hawkins being wrong. It is a story about what happens when a sound idea meets the wrong corpus and the wrong readers, and it is the second of five things I think indieweb people should take from his writing. Here are all five.
1. Your static site's backend is the rest of the internet #
Hawkins wanted a “was this useful?” button on a GitHub Pages site. The standard 2026 answer is a JavaScript widget talking to somebody's SaaS. His answer, in Feedback buttons without JavaScript, using a 1990s web pattern, is a Cloudflare Worker used exactly the way we used CGI scripts in 1998: the button is a plain HTML link, clicking it hits the Worker, the Worker increments a counter in KV storage and answers with a 302 redirect back to the post with a #thanks fragment, and a CSS :target rule reveals a thank-you message that was in the HTML all along. Zero client-side JavaScript. The Worker is under 200 lines. He even serves the running count as an SVG image from /count/posts/slug.svg, which is the 1996 hit counter reborn as an edge function.
What I love about this is the argument underneath it. A static site is not a site without a backend; its backend is the rest of the internet, composed one origin at a time. The GeoCities guestbook and the edge function are the same architecture with better latency. HTML and CSS do the actual work, and JavaScript is an enhancement rather than a load-bearing wall. That is the indieweb position stated as infrastructure rather than as ideology, and it is more persuasive that way.
2. Try the ambitious thing, then be willing to measure it and tear it out #
This is the centerpiece, because I lived it.
Hawkins's semantic search post is direct about its limitations and scale. He says up front that vector search on a personal blog is a toy, and that the technique earns its keep on documentation sites with hundreds of pages where readers do not know the right vocabulary. His numbers, which he actually publishes: the browser downloads a ~23 MB quantized MiniLM model (q8 quantization takes it down from 90 MB with no noticeable quality loss for sentence embeddings) plus ~4 MB of ONNX Runtime WASM, roughly 30 MB on the first query, cached afterward. His vectors.json for 89 pages came to 645 chunks, 2 MB raw and about 600 KB gzipped. On the build side, @huggingface/transformers weighs about 476 MB in node_modules, mostly ONNX native binaries for every platform you will never deploy to. He hand-tuned his similarity threshold to 0.25. He even tested the lighter Model2Vec “potion” static embedding models, which would drop the download to 4 to 15 MB, and found only about 32% Jaccard overlap with MiniLM's top results on his small corpus. Not good enough; he said so.
I read all that and built it anyway, because I run six sites and the cross-site search problem is real for me. My version lived on <thewhitestonefoundation.org> in a series of scripts like scripts/build-semantic-vectors.mjs with a scripts/metasearch.config.mjs: Xenova/all-MiniLM-L6-v2 via Transformers.js at 384 dimensions, plus a lite variant at 192 dimensions capped at 3 chunks per document for a smaller first payload. Chunking targeted 1100 characters with 200 characters of overlap, max 12 chunks per document. Similarity threshold 0.2, max 10 results, embeddings rounded to 4 decimal places, batch size 8, output to _site/metadata/vectors.json and vectors-lite.json. A scheduled GitHub Action, ingest-allsites.yml, crawled the other five sites on a cron and committed the resulting JSON back into the flagship repo, which meant search results were exactly as fresh as the last successful cron run and no fresher.
And here is the confession. The config had a hand-tuned BOOST_MAP that ranked jcrt.org/archives/ at 1000, journal.thenewpolis.com at 800, jcrt.org at 600, thenewpolis.com at 400, esthesis.org at 300, and thewhitestonefoundation.org at 150. I was hand-weighting a semantic ranker. If you find yourself writing authority rules on top of a system whose entire pitch is that it understands meaning, that is a tell, and I ignored it for months.
What I could not ignore was the readers. The 30 MB download is a one-time cost that caches, and on a documentation site someone visits weekly that is a perfectly defensible trade. But a scholarly org site does not have weekly visitors. People land once from a search engine, read one article, and leave. “Cached after first use” is a promise that never arrives for them: nearly every visitor pays the full cost and almost none of them amortize it. Nobody is patient enough to download an AI model to use a search box, and they should not have to be.
So the Whitestone CHANGELOG entry for 2026-07-08 reads, in full: “Removed the semantic-vector search and cross-site metasearch/allsites pipeline (including ingest-allsites.yml); replaced by Pagefind multisite search.” The deletion landed in the v3 rebuild in commit 3aba402. What replaced it: Pagefind 1.5.2, where npm run build:search is just pagefind --site _site --output-subdir pagefind, and an org-wide search page at /metadata/search/ that merges five sites' Pagefind indexes in the browser via mergeIndex, each site serving CORS headers on /pagefind/*. No cron, no embedding step, never staler than the last deploy. I wrote up that migration in the Whitestone v3 post, so I will not retell it here.
The comparison on my corpus, using Hawkins's own payload numbers since we ran the same model:
| Semantic (MiniLM in-browser) | Pagefind 1.5.2 | |
|---|---|---|
| First query | ~30 MB model + WASM runtime | ~10 KB JS + ~75 KB WASM |
| Per query after that | in memory | ~10 to 30 KB index fragments |
| Freshness | last successful cron run | last deploy |
| Ranking | cosine similarity + my BOOST_MAP |
keyword relevance, no patching |
Speed won. Patience lost. But the concept is sound, and it will be right for someone: a documentation site, a large archive, any corpus where readers do not know the vocabulary the authors used. The JCRT archive, a quarter century and change of theory-dense journal issues where nobody searches for the words actually on the page, is arguably exactly that corpus, which is why I have not stopped thinking about it. If I ever bring embeddings back, it will be there, behind an explicit opt-in, and it will be because Hawkins convinced me the technique works even as my own deployment convinced me the default should be Pagefind.
3. Ship both, and let the user pick #
Hawkins does not treat keyword versus semantic as a religious war, and this may be the most quietly radical thing about his search page. He ships both. Keyword search is the default; semantic is an explicit opt-in that lazy-loads the model on first query with a progress bar; every semantic response links back to keyword search; and the <noscript> fallback is just a link to DuckDuckGo with site: scoping. That is progressive enhancement practiced as an epistemology: you do not actually know which tool a given reader needs, so you stop pretending and offer both, each pointing at the other.
His companion post, Site search without a search service, has two small tips I adopted directly. First, treat data-pagefind-body as a whitelist: mark the content you want indexed rather than maintaining an ever-growing blocklist of navs and footers, because whitelists fail closed and blocklists fail embarrassingly. Second, pass --exclude-selectors "pre, code" so code samples stay out of your search excerpts. Both are one-line changes. Both are the kind of tip you only get from someone who ran the thing in production. I do wonder about bringing back this ship both approach to help out scholars and offer a deeper research option for those seeking to go deeper.
4. Content architecture is the thing nobody funds and everybody needs #
In Content architecture: the delivery problem, Hawkins reaches for the shipping container. Before 1956 every port handled cargo its own way; standardizing the box, not the ships or the cranes, is what made intermodal freight possible. And the detail that matters: Malcolm McLean granted free licenses on the container patents to ISO in 1965, because standardization only works when adoption costs nothing.
His application is organizational. An org running multiple sites drifts, without anyone deciding to, into six slightly different definitions of “a card,” and then syndication quietly breaks. It is never a dramatic failure. It is a slow accumulation of almost-compatible structures, each locally reasonable.
I manage six Eleventy sites for The Whitestone Foundation, and I have watched this drift happen under my own hands, between repos where I wrote every line. My answer is boring on purpose: each site has one src/_data/metadata.yaml acting as the control plane, and the schemas are documented and shared rather than improvised per property. When a card or a citation block needs to change, it changes in the schema, and every site inherits the change. This is also, not incidentally, the indieweb argument for microformats and for POSSE-friendly markup: your content is a container that other people's cranes need to lift, and the box has to be standard even when the sites are not.
5. Measure what you actually meant, or do not bother #
Hawkins's day job shows most clearly in How to measure impact when analytics lie. There are no analytics benchmarks for intergovernmental organizations or technical policy sites, so he synthesized targets from adjacent sectors and, crucially, wrote down his reasoning about what transfers. The concrete example: the widely-cited claim that nonprofits get 16% of their traffic from social media comes from fundraising organizations running emotional campaigns. For a technical policy org, 3 to 8% is the appropriate target, and low social traffic is not a problem to fix. He also tags every page with a content-type metatag read into GA4 as a custom dimension, so a 50% bounce rate on reference content registers as success while the same number on a landing page registers as failure.
My version of this: an academic journal archive is reference content. Someone arrives from a search engine, finds the article, downloads the PDF, and leaves, and that is the archive working perfectly. Judging it by blog metrics, time on site, pages per session, return visits, is a category error, and I have caught myself making it. The number was never the meaning; the meaning was whatever question you had before you opened the dashboard.
His recent piece The ladder was always context extends the same instinct to careers: implementation has gotten cheap, context has not, and the apprenticeship ladder was never really an implementation ladder, it was a context-acquisition pipeline. As someone who teaches, I felt that one in my syllabus. The junior tasks were never valuable because the code was hard to type. They were valuable because typing it was how you learned what the organization actually meant.
Why he is worth your feed reader #
The indieweb has plenty of people publishing their successes. Hawkins publishes the experiments that did not pan out, with the numbers attached: the 476 MB node_modules, the 32% Jaccard overlap that killed the lighter model, the threshold he hand-tuned and admits he hand-tuned. That is the actual indieweb contribution, and it compounds. This post exists because he showed his work, and tearing out my own semantic search was easier, faster, and less painful than it should have been, because someone else had already documented the tradeoff honestly and I could check my experience against his instead of against my own sunk cost.
Build the ambitious thing. Measure it. Write down what happened either way. Somebody two hops away in the network is reading your build scripts at eleven at night, and it might save them a cron job.
We need to not only build in public but also write and fail in public.
Tags : notes website 11ty pagefind search indieweb web-development
Webmentions
No webmentions yet.