Colophon

How this site is built

I do security work. It would be strange to argue for engineering rigour on a site that didn’t have any, so here is the whole thing: what it’s made of, what it deliberately isn’t, and where it’s still weak.

Runtime dependencies
0
Build step to deploy
None
Pages
19
CSS
99 KB
JavaScript
104 KB
Fonts
78 KB, self-hosted
CI checks
5

Zero dependencies is a decision, not an accident

There is no framework here. No bundler, no CSS library, no icon package, no node_modules directory, and nothing to npm install before the site will run. Every page is HTML that a browser reads directly.

This isn’t nostalgia. A dependency is a supply chain, and a supply chain is an attack surface, and that’s LLM03 in the OWASP LLM Top 10 and it has been in the general application list far longer. Every package I add is code I did not write, executing in my visitors’ browsers, updated by someone I have never met. For a personal site the honest cost/benefit almost never favours the package.

The practical effect is that this site cannot be compromised through a dependency, because it has none. That is a small claim about a small site. It is also the kind of claim most sites cannot make.

Two generators, run by hand, output committed

Zero dependencies at runtime does not mean zero tooling. Two Node scripts run at author time, and their output is committed so the repository stays a plain static site that deploys with git push.

tools/build-pages.js stamps shared markup (navigation, footer, head assets, analytics) into every page. It exists because of a specific failure: the navigation was hand-copied across every page, I changed an anchor on the homepage, and ten sub-pages silently kept pointing at a section that no longer existed. Nothing caught it because nothing could. Now a partial changes in one place and CI fails if any page is out of date.

tools/build-blog.js generates the writing index, related-post rails ranked by tag overlap, next/prev links, and the RSS feed from a single data/posts.json. Adding a post used to mean four hand-edits in four files that could disagree with each other. Now it is one entry and the body.

Both use the same convention: a marked region in the file, an idempotent rewrite, and a --check mode that exits non-zero if the committed output has drifted. That last part is what makes them trustworthy rather than merely convenient.

The Lab runs on rules, not on a model

Every interactive exhibit, including the injection simulator, the pattern detector, the atlas, is deterministic and runs entirely in your browser. No backend, no API key, no model call, nothing sent anywhere.

Partly that is forced: this is a static site on GitHub Pages, so there is no server and therefore nowhere safe to keep a key. But the constraint produced a better artefact than the unconstrained version would have. A rule engine gives the same answer to the same input every time, which is what lets the simulator demonstrate that toggling one specific control changed one specific outcome. A sampled model could only ever show correlation.

It also means the classifier and the atlas read from one shared module, so the reference documentation structurally cannot describe a detection rule the tool doesn’t actually have.

Accessibility, verified rather than assumed

Colour contrast is checked with a compositing-aware sweep over every text node on the page, resolving alpha layers and inherited opacity down to the real background rather than eyeballing swatches. Every page currently passes WCAG AA with zero failures, and the sweep runs again after any visual change.

That method has caught things review would not. A recruiter-mode design that dimmed de-emphasised sections to 55% opacity looked fine and failed on eleven elements; it was removed rather than tuned, because reducing the contrast of content someone may still want to read is not a trade worth making for visual hierarchy.

Everything else follows the same rule. prefers-reduced-motion throughout with substitutions rather than removals, 44px touch targets, visible focus rings, one h1 per page, skip links, live regions on anything that updates. The only !important declarations in the stylesheet are inside the reduced-motion block, where overriding is the entire point.

Performance, and what it cost

Fonts are self-hosted variable WOFF2, latin subset only, preloaded. Dropping the third-party stylesheet removed a render-blocking round trip to another origin; dropping latin-ext removed more weight than everything now shipped, after a scan of every page found zero characters that needed it.

Images are sized to their slot and served as WebP with a JPEG fallback, with dimensions declared so nothing shifts as they load. Third-party scripts are deferred until they are needed. The captcha loads on first interaction with the contact form rather than on every page view, since almost nobody reaches that form.

Not everything survived measurement. An SVG sprite was planned until I measured it: stripping every inline icon from the homepage saved well under a kilobyte gzipped, because repeated markup is precisely what gzip handles best. The icons stayed inline. The social preview image is still heavier than it should be.

What CI actually checks

The checks are chosen for the failures a static site really has rather than the ones a template suggests:

Generated markup is current, guarding the failure that already happened once. Link checking: with pages at three directory depths, a broken relative path is the most likely regression and the most damaging when a recruiter hits it. Type checking: checkJs over plain JavaScript, no compile step, nothing shipped; it found dead code in the simulator and a documentation comment that disagreed with its own function. Asset budget: a guard against someone dropping in a 2 MB image or a library “just this once”.

Where it’s weak

A colophon that only lists strengths is a brochure. So:

The stylesheet is a single file. It is organised and tokenised, but it is still a single file that everything reads from, and it will get harder to reason about before it gets easier.

There are no unit tests. Verification is a link check, a type check, and manual browser passes. That is proportionate to a static site today, but the simulator’s rule engine has real branching logic and deserves better than my confidence that I checked the truth table once.

No light mode. A dark-only site is a choice that also happens to be the easy one.

Assets are cached for ten minutes by GitHub Pages and I do not control the headers. Filename hashing would fix it; it is not worth the machinery at this update frequency.

Why this page exists

I ask people to trust my judgement about systems. This is the one system I control end to end, so it is the most honest evidence I can offer about how I actually make decisions, including the ones I reversed, the estimate that did not survive measurement, and the parts I have not got to yet.

The whole thing is on GitHub →

← Back home