I built this site for my work and my writing. Here is what goes into it.
The homepage covers the engineering. Ljóss holds the investigations, the game devlogs, and the posts that are not about either. The foundation post explains the name and why I started it. The code is on GitHub (opens in a new tab).
Typefaces
Four faces, each with a job. These samples use the same font files as the rest of the site; the names link to their specimens.
- Fraunces (opens in a new tab)Wordmark and display titles
- Ljóss
- Montserrat (opens in a new tab)Body text and interface
- The details are where it starts.
- Merriweather (opens in a new tab)Ledes and quotations
- A note in the margin.
- Source Code Pro (opens in a new tab)Code and small labels
- const answer = 42;
next/font supplies the screen fonts. The resume uses static Inter and Source Code Pro files from Fontsource because Chromium was drawing the variable-font glyphs as paths in the PDF. The measurements and fix are in Every Letter in This PDF Is a Drawing.
From File to Page
Next.js and React provide the pages. The article pipeline (opens in a new tab) runs on the server:
- 1. SourceMarkdown
Text, frontmatter, figures, and markers for interactive components.
- 2. ServerPrepare
remark and rehype process the article. Shiki colors code; Mermaid becomes SVG.
- 3. BrowserRead & interact
The article arrives as HTML. React connects the interactive controls.
- Figures and code: captions, heading links, syntax highlighting, and diagrams are prepared with the text.
- Interactive explanations: the camera simulation and byte diff let you drive the thing the post is explaining.
- Reading tools: image inspection, copy buttons, and marginal notes run in the browser.
The Lookup
The archive carries a field that reads every published entry. There is no search server behind it: the matching happens in the browser, and the words typed are never sent anywhere. The engine (opens in a new tab) is Rust compiled to WebAssembly, and the same module writes the files at build time and reads them back in the tab, so one tokenizer decides what a word is on both sides.
A build writes three files, and only one of them is fetched whole. It holds the dictionary, the entry rows and the offsets, and it starts downloading when the field takes focus rather than when the first key lands, so the wait sits in front of the first keystroke instead of after it. The other two are read a range at a time: the posting lists of the words actually typed, and one window of reading text for each result printed. Growing a word one letter at a time reads nothing it already holds.
- Every word has to land. The last one is still being typed, so it reaches every ending of itself; the rest are taken as written.
- Order comes from BM25, with a bonus for words that stand next to each other in the entry. A line break parts two words the reader never saw together, so a phrase is not found across the seam between a caption and the paragraph beside it.
- A word the ledger does not hold is answered by the nearest one it does, counting two characters the wrong way round as a single slip. Both words are named under the count, because a name typed deliberately should not be quietly overruled.
Two failures are worth stating. Without JavaScript the field is hidden and the archive list stands on its own. And the addresses of all three files carry a digest of the index, so a page left open across a deploy finds its next range missing and says the ledger has been reprinted, rather than quoting text whose offsets belong to another build.
Services
- Music: Spotify supplies the footer track and listening page. A failed first fetch leaves a link to that page; later failures can leave the previous track showing. How the integration works.
- Views: Upstash Redis stores article counts. Recording a view and reading the count are separate requests.
- Comments: Giscus loads a GitHub discussion below the article. The text remains readable if the thread fails to load.
- Subscriptions: Buttondown (opens in a new tab) for email, or the Atom feed for a reader.
- Analytics: Vercel Analytics and Speed Insights. Google Analytics loads when a measurement ID is configured.
Cache Settings
These are configured intervals; how much reuse they buy depends on the cache in front of them and on the deployment. I last read them off the source (opens in a new tab) on .
- Articles (opens in a new tab): generated routes, with no timed revalidation. The homepage (opens in a new tab) declares one day.
- Spotify (opens in a new tab): five minutes for recent plays; one day for top items.
- View counts (opens in a new tab): 60 seconds in a shared cache, with 300 seconds of stale-while-revalidate.
- PDF (opens in a new tab): temporary files reused for up to a week. This cache belongs to each instance and may disappear between deployments.