Skip publishing timeline

Timeline of posts

← Digesting

Colophon

Last updated: 27 Aug 2026

This site is built and published with a small, readable stack and a bias for flexibility and performance.

Stack#

  • Static site generator: Eleventy (11ty) v4
  • Language & tooling: Node.js (Yarn scripts), Nunjucks templates, Markdown via Markdown‑It with markdown-it-anchor
  • Styles: Sass compiled to a single CSS file; custom utility classes; normalize; type set in IBM Plex Sans and IBM Plex Mono
  • Search: Two modes on the search page:
    • Pagefind — static keyword search, indexes built HTML at deploy time, serves chunked index fragments on demand. Source: search page template
    • Semantic search — build-time vector embeddings (MiniLM-L6-v2 via Transformers.js) + in-browser cosine similarity. Same model runs both sides; no API keys, no server. Read the post
  • Hosting: Vercel (auto‑deploys from GitHub, automatic HTTPS, CDN)
  • Domain: Cloudflare (DNS, security, performance)
  • Fonts: IBM Plex Sans and IBM Plex Mono, self-hosted. The site previously used Recursive.

Build & source#

How closest-in-meaning works#

At the foot of most entries there is a short list of the closest entries by meaning. It is not driven by tags, and it is worth being precise about what it is and is not.

The site already computes vector embeddings for semantic search: each page is split into chunks and each chunk is turned into a 384-number vector by a small language model at build time. Those vectors place text that means similar things near each other, whether or not it shares any words.

The related list reuses that work rather than adding anything new:

  1. Average a page's chunk vectors into one vector for the whole entry, and normalise it to unit length.
  2. Compare every pair by cosine similarity, which for unit vectors is just the dot product. 1.0 is identical, 0 is unrelated.
  3. Keep the best three above a floor of 0.55.

Three details do most of the work:

  • Index pages are excluded. A page like the blog index is "about" everything, so it sits near everything and crowds out real matches.
  • The floor is measured, not chosen by feel. Across this corpus the median entry's best match scores 0.70. At 0.55, about a fifth of entries end up with no neighbour at all, which is the right answer for a genuinely singular piece: a padded suggestion is worse than none.
  • The number shown is the raw similarity, rounded to two places. It is not a percentage or a confidence, and it is stated rather than drawn as a bar, because a bar would imply more precision than a soft signal has.

The trade-off worth naming: this reflects what the writing is about, not what it argues. Two entries taking opposite positions on the same subject will look very close together, because they are, semantically. It is a reading aid, not an endorsement that one follows from the other.

Neighbours are computed after each build and committed as data, so a brand new entry has no neighbours until the next build. Code: scripts/compute-related.mjs.

Privacy#

  • Analytics: GoatCounter (cookie‑free, aggregate counts only). See their rationale on GDPR consent and legitimate interest: GDPR consent notices.
  • No trackers: No ad tech, no cross‑site profiling.

Principles#

  • Prefer simple, inspectable pages over heavy frameworks.
  • Ship as little JS as practical; keep content readable without it.
  • Optimize images and typography for legibility.
  • Document decisions in public when it helps future me (and you).

Classic web badges

Badges sourced from Seirdy’s “Classic web badges” — licensed under CC BY‑SA 4.0. Animated Eleventy badge made by Chris Burnell, self-hosted here rather than hotlinked.


More things I've been digesting

Was this useful?

👍 This was useful Found usefulFeedback count Thanks for the feedback! Curious how this was built?

Read next in sequence