Connor Faulkner
← Back to Blog

The Annals · Blog entry

An Epic-Fantasy Tome in Zero JavaScript

15 August 2026 · 4 min read

I rebuilt this site as a Malazan-style tome — dramatis personae, epigraphs, a memorial roll for dead experiments — without shipping a single line of framework JavaScript. Scroll-driven CSS animations, a deterministic ash-scatter title, and an antique map drawn from live government data.

Web CSS Design Astro

This site got its third identity this year. The first was a space theme, the second a wine-dark instrument panel. This one is a book.

Specifically, it’s the front matter of a thousand-page fantasy doorstopper — the kind I actually read. My favourite novel is Memories of Ice, the third of Steven Erikson’s Malazan Book of the Fallen, and those books open with equipment I’ve always loved: a hand-drawn map, a dramatis personae sorted by faction, epigraphs from invented historians, and, at the end, a list of the fallen. It turns out a personal site has all of the same parts. Projects are a cast of characters. A blog is annals. My negative-results page was already a list of the fallen — it just didn’t know it yet, so now each dead experiment gets an epitaph carved from its own numbers.

The constraint that made it interesting: the previous redesign shipped zero framework JavaScript, and I wasn’t willing to give that up. Every effect on this site is CSS or build-time SVG. That ruled out the thing that inspired the rebuild — Zandani Zidan’s homepage, where his name falls letter by letter under a little gravity simulation — and forced a better question: how much of that feeling survives without a physics engine?

Ash, scrubbed by scroll

The answer is CSS scroll-driven animations. The title of this site is one <span> per letter, and each letter carries three custom properties: a lateral drift, a fall distance, and a rotation. One keyframe animation moves it to that resting-scattered state — but the animation’s clock is not time, it’s your scroll position:

@supports (animation-timeline: scroll()) {
  .ash-letter {
    animation: ash-scatter linear both;
    animation-timeline: scroll(root block);
    animation-range: 0svh 90svh;
  }
}

Scroll down and the name breaks apart and falls like ash; scroll back up and it reassembles. It’s fully reversible because a scrubbed timeline is just a mapping from scroll offset to animation progress — there’s no state, no listeners, no requestAnimationFrame. The keyframes deliberately have no from block, so 0% is each letter’s natural position. That one decision makes every fallback free: a browser without scroll-timeline support fails the @supports gate and renders the assembled name; a reader with reduced motion enabled hits the site’s blanket animation kill switch and gets the same. The spectacle is progressive enhancement in the strictest sense — the resting layout is the content.

The letter offsets look random but aren’t. They come from a little sine-hash of the letter index, because this site rebuilds itself every night from live data, and Math.random() in a page template would churn the DOM on every rebuild for no reason. Two consecutive builds of this site are byte-identical apart from a build timestamp — and as of this week, CI builds twice and diffs the output to keep it that way.

A campaign map from a government API

Malazan books open with a map, and I happened to have one lying around: the PlanningWatch choropleth of Ireland’s 31 local authorities, drawn at build time from Tailte Éireann boundary data and shaded by the last 30 days of real planning applications. Promoting it to the homepage meant making it feel surveyed rather than exported from GIS software. The whole “antique” treatment is one SVG filter — feTurbulence into feDisplacementMap with a fixed seed, wobbling the boundaries like pen lines — plus engraved ice-coloured strokes and a cartouche caption. Fixed seed for the same reason as the sine-hash: the coastline shouldn’t redraw itself at 04:30 UTC.

(The map keeps its one genuinely hard-won detail: Cork City is a hole inside Cork County’s polygon, and without fill-rule="evenodd" it vanishes. There is a test asserting the map’s council names match the data snapshot’s, because a silent join failure would render a county as zero forever.)

The parts that were words, not code

The epigraphs were the strangest work: short verse in the style of Erikson’s invented historians, attributed to volumes that don’t exist — Annals of the Night Shift, Testimonies of the Dry Run. Every line is original, partly because quoting a living author’s novels across a website is a copyright problem, but mostly because the conceit only holds if the fictional scholarship is about this work. The navigation is dual-labelled — “Dramatis Personae · work” — so the joke never costs a visitor their bearings, and the URLs didn’t change at all.

The rebuild also flushed out a bug that had nothing to do with fantasy novels: my social-preview images had been serving as application/octet-stream since July, because Vercel derives content types from file extensions and my OG route emitted extensionless files. Scrapers reject that outright, so every link preview of this site was likely blank. If you’ve ever wondered whether redesigns are worth it: sometimes the value is that staring at every page for two days makes the real defects impossible to ignore.

The site is still what it was underneath — a static Astro build that fetches real data at build time, rebuilds itself nightly, and ships nothing to your browser but HTML, CSS, and one analytics script. It just reads like something I’d want to pull off my own shelf now.