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 ]

Four requests leave for somewhere else

Two go to fonts.googleapis.com and fonts.gstatic.com for Space Grotesk, the display face. The other two are analytics — mine rather than a vendor's. Page and résumé activity is beaconed to script.google.com, where a script I wrote appends it to a spreadsheet I own, and one lookup per visit goes to ipwho.is to turn an IP into a city and a network operator.

There is no Google Analytics, no tag manager, no CDN script, no icon library, no embedded widget. A Google tag did sit here for eight months, inherited from a template and reporting into a property I could never find. It is gone.

What the analytics record

Which pages, in what order, and how long each was actually focused. Referrer or campaign tag. Device, browser, operating system, screen size. City, country and network operator, from the IP lookup. Résumé opens also record reading time and whether the PDF was downloaded. A random first-party id tells a returning visitor from a new one.

No fingerprinting. No canvas, audio or font probing, no keystroke capture, no mouse tracking, no session replay. Those techniques exist to identify people who have chosen not to be identified. Nothing here can name you — only the machine a particular link was opened on.

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.
I removed one tracker and then wrote my own
The Google tag is gone, which is an improvement. In its place is a script that records your path through the site and asks a third party to turn your IP into a city and an employer. It is first-party, the data lands in a spreadsheet only I can open, and it is still tracking. Wanting to know whether a particular company opened my résumé is a good reason to me and no reason at all to you.
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.