Colophon ·

How this site is built

Ten pages, no framework, 24.9 KB of text on the home page, and three requests to a domain that isn't mine. Also four things about it I am not happy with, which are at the bottom because they are the interesting part.

[ 01 — Stack ]

Static files and one build step

It is hand-written HTML. There is no framework, no component layer, no client-side router, and no hydration. Every page is a file that a server can hand over without thinking about it.

The single build step compiles Tailwind's utility classes into a static stylesheet. Before that, the site loaded the Play CDN from <head> — about 120 KB gzipped of JavaScript that has to run and generate the CSS before the first styled paint, on a build Tailwind documents as development-only. Compiling it ships 4.0 KB gzipped instead of 120, and nothing has to execute for the page to look right.

The rest is four small JavaScript files totalling 13.1 KB unminified, with no dependencies between them and none from npm: the nav, the cursor, the scroll reveals, and the photography lightbox. The lightbox used to pull GSAP from a CDN for four tweens; it is CSS transitions now.

[ 02 — Weight ]

What the home page actually costs

ResourceGzipped
index.html10.7 KB
css/styles.css — hand-written5.9 KB
css/tailwind.css — compiled4.0 KB
js/cursor.js1.7 KB
js/main.js1.6 KB
js/nav.js1.1 KB
All markup, styles and script24.9 KB

Measured, not estimated. Served locally and gzipped at default compression. Third-party requests and the hero photograph are excluded here and accounted for below.

hero photograph — 172.5 KB 24.9 KB
one image ten pages of HTML, CSS and JS

197 KB for a first view, and 87% of it is one photograph. Every optimisation described on this page is the small green sliver. That is worth knowing before anyone — me included — spends another evening shaving bytes off a stylesheet.

[ 03 — Rules ]

Three rules, all checkable

Values are cheap to write down. These are ones you can verify from the source in about a minute each.

A contrast floor

Two greys are defined in css/styles.css and nothing on the site is allowed to be dimmer: --label at 5.4:1 and --secondary at 6.4:1 against the background. The accent green sits at 11:1. This is enforced by a script that parses every stylesheet and every inline <style> block and fails on anything under 4.5:1 — it caught section labels sitting at 3.5:1 and article dates at 2.7:1, which is below the floor for large text, never mind 11px capitals.

Nothing hides behind JavaScript

Every scroll animation is gated behind a .js class set in the document head. If a script fails, the class is never added, nothing is hidden, and the page is a plain readable document. The mobile nav degrades to a list of links. The custom cursor only hides the real one after the replacement is confirmed running — the original set cursor: none in CSS, which left a broken script with no pointer at all.

The build runs in CI, not on my laptop

The compiled stylesheet depends on class names in the HTML, so a build that only ever runs locally drifts the first time someone forgets. GitHub Actions recompiles it on every push to main and deploys to GitHub Pages. If the compile produces an empty file, the deploy fails rather than shipping an unstyled site.

[ 04 — Third parties ]

Three requests leave for somewhere else

Two go to fonts.googleapis.com and fonts.gstatic.com for Space Grotesk, the display face. That is the whole list. There is no analytics vendor on this site — there was a Google tag, inherited from a template and reporting into a property I could not find, and it has been removed. The résumé viewer at /resume/ posts opens to a Google Sheet I own; nothing else phones anywhere.

Where the images come from

All photography is mine, resized through a sharp script that writes an 800px thumbnail and a 1920px version and regenerates a manifest. The grid lazy-loads thumbnails; the full size is only fetched when you open one.

[ 05 — Honest ]

Four things I am not happy with

A colophon that only lists good decisions is marketing. These are the open ones, roughly in order of how much they bother me.

The hero photograph is 87% of the page
172.5 KB against 24.9 KB for everything else. It is a webp, it is sized correctly, it has fetchpriority="high" — and it still dwarfs the entire rest of the site. The honest fix is a smaller crop or no photograph, and I have kept it because I like it. That is a preference, not an engineering decision, and it should be labelled as one.
There is a Google tracker on a site that otherwise phones nobody
Every other third-party dependency has been removed, and then there is analytics, loading from Google, on every page. IP anonymisation is on, which is close to meaningless as a defence. I want the numbers, which is exactly the reasoning everybody uses.
The article pages each ship their own copy of the CSS
Every writing and project page is deliberately self-contained so one can be rewritten without regression-testing the others. The cost is roughly 8 KB of near-identical inline CSS per page and no cache sharing between them. For seven pages that is a trade I am happy with; at thirty it would be indefensible.
The custom cursor is a preference imposed on you
It is off on touch devices and off under prefers-reduced-motion, and it still replaces a control the operating system already got right. I kept it because the site should feel like a person made it. It is the one piece of this page arguing against the rest of it.

Everything above is checkable against the repository. If a number here is wrong, that is a bug, and I would like to know.