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.