Workflow
Measured, Not Guessed: How This Site Was Built
Static Astro, Svelte islands that have to justify themselves, and quality gates that read the built output rather than the source. Why the architecture is shaped that way, and what it caught the site claiming about itself.
On this page8 sections
The column you are reading is 715 pixels wide. For most of this site’s life its own stylesheet said that width held about 78 characters of running prose — comfortably inside the band typography references recommend, and therefore nothing to look at again.
Then somebody counted. Not estimated: counted, by walking the built page and assigning every rendered character to a line box. The answer was 100.
The 78 had been arithmetic on the wrong unit — 769 divided by 9.83, the advance
width of the digit zero, which CSS calls ch. That is a character count only in
a face whose zero happens to be of average width. A unit had been read as a
measurement, and it had sat there long enough that three files were quoting it
to each other.
Nothing was broken. Every page rendered and every test passed. The site was simply wrong about itself in a way that nothing it owned could detect, because nothing it owned was in the business of detecting it.
That is the defect class this architecture is built against: not code that fails, but claims nobody checked against the thing that actually shipped. What follows is how the site is put together, and which parts of it can tell when it is lying.
Measuring the built page
The typeface is the small example, and it is worth exactly one chapter.
Two complete candidates were put in front of the site’s owner to read, blind.
Both were injected at document_start, so the font swap and any layout shift
were the ones a real build produces rather than a post-load repaint, and both
were served same-origin, because a candidate measured from a font CDN is a page
this site would never ship. Alongside the reading, a harness measured what a
machine can measure: face metrics, characters per line, decoded bytes per route,
layout shift, contrast.
In the recorded comparison the measurements favoured the incumbent on four of the eight dimensions they could score — decoded cost, layout shift, numeral behaviour and fallback metrics — and had nothing whatsoever to say about the one that decided it, which is what a page is like to read for ten minutes. The owner chose the other candidate, and the cost was measured rather than waved through: between 35 and 67 kilobytes more per reading route, because the incumbent still sets the headings either way, so the winner is a second family on top rather than a swap.
Two things carry forward, and they are why this opens a technical article.
Measure the built page, not the source — the ch defect survived because
review reads source, and the source looked reasonable. And keep the numbers
that argued the other way: a decision record listing only the winning figures
is a justification wearing a record’s clothes.
Why it stayed static
The site is Astro in static output mode with Svelte islands, deployed to Cloudflare Pages from a Git-connected branch. There is no server rendering, no React, and no application framework above the router.
That was not mainly a performance decision. Every route is a file on disk before anything is deployed, and that property is what everything else here depends on. The bundle checker, the feed assertions, the accessibility scans and the screenshot suite do not exercise a running application and reason about what it probably emits. They open the build output and read it — so a passing check is a statement about what the reader will receive, not about a development server that resembles it.
The corollary is that those checks deliberately do not build; each reads a build directory someone else produced. Run one against a stale build and it will report on a three-commit-old artifact, which is a real trap and the reason the release ladder is an order rather than a set. The alternative, where every check rebuilds its own copy, is worse in a way that is easy to miss: the gate would verify one artifact while the release shipped another.
Static output also removes a category of thing that can be true in staging and false in production. No request-time branch, no environment-dependent render, no per-user variation. The page is the page.
There is exactly one deployment path, and the second is disabled by convention rather than by capability. A direct upload command exists and works, which is precisely the problem: it would push whatever sits in a local build directory — uncommitted edits, a stale artifact, the wrong branch — straight to production. Production contains what is on the main branch, or the claim that review means anything is untrue.
Islands are decisions
Astro ships zero client JavaScript by default, so every interactive component here is an explicit, individually argued exception. There are five across nineteen routes: two engineering calculators, two Learn surfaces, and one explorer inside an article. The homepage has none — it ships a single shared script of about 1.5 KB gzipped and no island at all.
What keeps that honest is a rule about where a hydration directive may be
written. An article body may not write one at all. A client:* directive in
prose is a hydration decision nobody reviews: it is not in a component’s diff,
it does not appear in the component inventory, and the next article copies it
because the last one had it. So a widget is three files:
src/lib/engineering/<domain>/ framework-neutral maths. Tested, no DOM.
src/components/mdx/interactive/X.svelte state and rendering. No formulas.
src/components/mdx/interactive/X.astro the client:* directive, and why.
The article imports the .astro file. That file owns the directive and carries
the written reason the browser is required, where a reviewer will actually meet
it.
The bottom layer matters most. Engineering modules are framework-neutral: plain
TypeScript, no DOM, no component imports, tested against closed-form solutions.
The stiffness explorer in
another article does not contain so
much as a * 1e6; every number it displays comes from a module checked against
the closed forms, the symmetry conditions and the scaling laws. That same module
generates the static charts higher up that page, and a test fails if a committed
chart stops matching what the module produces — because a curve drawn by hand
looks exactly as convincing when it is wrong. The
beam calculator is the same arrangement around a
different domain.
A formula duplicated into a UI component is an ownership failure before it has produced a wrong number.
Because Astro server-renders an island, the fallback is the widget itself: with JavaScript off, that explorer still prints both matrices, fully evaluated. Nothing on the page says “enable JavaScript”, because nothing needs to.
Note
The same instinct decides what happens at build time rather than in the browser. Wide equation blocks were once unreachable by keyboard on a phone. The fix was a build-time transform making each one a focusable, labelled region — not a client-side script, because an article has to work with JavaScript off, and a fix that exists only after hydration is not a fix for the reader who needed it.
Budgets that fail a build
Every route carries a JavaScript budget, and the budget is enforced rather than reported.
What is measured is the static-import closure of everything a route’s HTML names, gzipped — not the source, and not the sum of files someone remembered to list. A route fails when it grows past 2,048 bytes absolutely or 5% relatively, whichever is the looser.
It also fails, at any byte size whatsoever, when it gains a hydrated island. That clause is the one that does the work. Bytes creep, and a byte threshold is a negotiation. An island is a decision, and a decision appearing in a diff with no argument beside it is exactly what the budget exists to catch. A hundred-byte island fails as surely as a hundred-kilobyte one, because the question is not what it costs but whether anybody chose it.
The route list is itself part of the contract. Adding a page — this one, for instance — fails the check until the budget file is regenerated, forcing the new route’s cost past a person once rather than letting it vanish into a site total.
Zero, and what it hid
Every supported page is screenshotted at fixed widths in both themes, and the committed images are compared byte for byte, with no masks and no tolerance.
It takes two settings to say that, and for a long stretch only one of them was set:
toHaveScreenshot: {
threshold: 0,
maxDiffPixels: 0,
animations: "disabled",
caret: "hide",
scale: "css",
},
maxDiffPixels: 0 does not mean “no pixel may differ”. It means “no pixel may
differ by more than threshold”, and
the default threshold
is 0.2 — a 20% per-pixel colour distance. So the suite spent its first stretch
of life asserting that no pixel had moved by more than a fifth of the colour
space, while the document beside it claimed zero tolerance.
The gap was not theoretical. In the recorded run, a whole-palette repaint of the homepage passed this suite 16 of 16 as unchanged. One scene in that run captured the mobile navigation sheet, and behind the sheet sat an entirely superseded set of three cards — different titles, different body copy, different icons. It passed, because the sheet’s backdrop dims that strip of page enough that every per-pixel delta landed under the default.
The fix was one line. What it exposed is more interesting than what it fixed: the suite had been passing for months, and a passing suite is the most expensive kind of wrong, because nobody audits a green check.
Zero tolerance is only meaningful against a stated renderer, and the honest version of that claim includes its limit. These images are Chromium-on-Windows renderings. In a recorded run on a Windows CI runner, pointed at the same committed files with the same browser version and settings, 13 of 27 scenes matched and 14 did not. The split is not random: every byte-exact scene was a page of text and panels, and every failure carried a photograph, a syntax-highlighted block, an equation, an island-drawn diagram or a 300-cell numeric table.
So the byte comparison stays a local gate, and continuous integration runs two lanes that hold on any platform instead — structural relationships over the same scene matrix, and a determinism check that captures the matrix twice on one runner and requires the two to be identical. The tempting alternative, a small tolerance so a Linux runner agrees with a Windows baseline, is precisely the setting that let the palette repaint through in the first place.
Occurrences, not counts
The accessibility result is written to one fixed sentence:
N serious violation occurrences across M scans, produced by K rules.
Today it reads 0 serious violation occurrences across 41 scans, produced by 0 rules, with 0 critical.
The wording is a contract, not ceremony. A bare “4 violations” hides which problem you have: one rule failing on four nodes is a single mistake repeated, and four rules failing on one node each is four mistakes, needing different fixes. The scan count is there for the same reason: a clean run over a matrix that never visits a page proves nothing about that page, and every route this site added arrived unscanned until the matrix widened to reach it. It has been widened five times, from 16 scans to 41, and eight of those are activated states rather than pages at rest — a navigation sheet open, a media facade after someone pressed play, a form in its error path.
The recorded reading was not always zero. It was 4 occurrences across 16 scans from 1 rule — the unreachable scrollable region that the build-time transform described above was written to close.
Two things this number is careful not to claim. The first is a boundary. In the recorded scan of the article routes, the site’s own DOM came back clean, and the YouTube player embedded in one of them contributed 8 serious and 4 critical occurrences inside its own document. Those nodes belong to YouTube; no markup here can change them. What changed is that the page now ships a facade — a real link and a poster — and requests nothing from youtube.com until a reader presses play. That is not a fix, and is deliberately not described as one. A reader who presses play gets those nodes with the player. What they no longer get is a third-party document nobody asked for.
The second is about what a scanner is for. Run against a genuinely broken version
of that facade, the nested-interactive rule in
axe ran and passed. A scanner’s rule
set is not a specification of what the DOM should be, only a list of things it
knows how to look for. So every state worth scanning also carries structural
assertions written by hand: the modal contract for the navigation sheet, the
tab-to-panel relationship for the practice island, the removal-not-zeroing
contract for both calculators’ error states. A scan is the second net, never
the first.
What had to be deleted
The rule that has removed the most content here is that a capability claim needs a destination returning 200.
An audit found the homepage advertising two engineering tools —
/tools/section-properties and /tools/column-buckling — badged “beta” and
“soon”. Neither route existed. Both 404’d. They had been written as a three-card
row because a three-card row looks better than a one-card row, and that is the
entire mechanism by which a personal site starts lying: not dishonesty, but
layout pressure.
The way they are kept gone is architectural rather than disciplinary. Every collection surface reads from a content collection rather than a page-local array, so a section cannot describe software that has no route — the data source and the routing table are the same object. Exactly one project is live, so exactly one project card renders, and a sparse section is the correct rendering of a short collection.
The same rule cost something harder to give up. The homepage once carried a generated tower figure annotated with 52 storeys, a 5.8-second period and a drift ratio of 1/38. Every number was genuinely derived: the module integrated a sway model and the annotations followed its constants, so retuning one changed the drawing. But those constants were authored — not the output of an analysis of any building. As generative typography that was fine; presented as evidence on a page about structural engineering software, it would have been the interface asserting something false about its own domain. It was retired rather than reused, and the island went with it.
The general form: where there is no real figure, the answer is typographic. Not a mock-up, not a fake dashboard, not a generic futuristic building. An empty slot is a truthful rendering of an empty slot.
Who decides
The work described here was carried out by AI agents, one per issue, coordinated by another, with every merge gated on human approval. The mechanics are ordinary; the constraint is the interesting part.
Every agent authenticates as the owner’s account. GitHub will not let a user approve a pull request they authored, so an agent that opens one cannot produce the approving review that “no self-merge” asks for, however thorough its review was. Pretending otherwise would be worse than stating it: an agent writing “LGTM” on its own pull request is not an approval and must never be presented as one.
The consequence is counter-intuitive enough that an earlier version of the branch protection checklist got it backwards. Requiring one approving review — the obviously more rigorous setting — would have made every pull request permanently unmergeable, including the one that introduced the rule. Required approvals are therefore 0, which is the honest number rather than a relaxation. The review is real; it is simply not a thing the platform can record under this identity model. What is enforceable was automated instead: the checks are non-optional, and nothing merges past a red one.
That division is the point. Machines hold the gates that can be stated as a rule — the byte comparison, the budget, the occurrence count, the route contract. What is left over is judgement, and the judgements here were the owner’s, including the typeface chosen against the measurements. Every merge is the owner’s click.
What the agents supplied was volume and consistency: counting entries rather
than estimating them, capturing twice to compare checksums, re-deriving a figure
rather than quoting it. That is also, precisely, the sort of work that produced
the ch defect at the top of this article — a number carried forward
faithfully, by three files, for a long time, without anyone asking what it was a
number of. The discipline that catches that is not a better agent. It is a
rule that every figure names the artifact it came from, and a person who reads.