§ 01Context
The client is a small, senior advisory practice serving specialty biopharma — the kind of firm whose entire product is judgement, and whose credibility lives or dies on precision of language. They were running a thin single-page site built on a drag-and-drop builder. It said very little, and what it did say could not be traced to anything. They wanted a real site: multi-page, multi-locale-ready, and owned by them rather than rented from an agency.
§ 02Challenge
Mid-project the client raised the objection that reframed the whole build. Reviewing a draft, they asked where the added text had come from and how changes to the site were controlled. It was the right question. In a regulated-adjacent sector, a confident sentence nobody can source is not marketing polish — it is exposure. And the usual agency answers (“we will be careful”, a copy deck in a shared drive) are promises, not controls. They decay the moment anyone edits anything.
Worse, the obvious fix and the client's other requirement pulled in opposite directions. They also wanted to edit their own site without calling us. A CMS lets a client change any sentence at will — which is exactly what a provenance guarantee forbids.
Where did this text come from, and how are changes to it controlled? The only honest answer to that is a mechanism, not an assurance.— The question that set the architecture
§ 03Architecture
We made provenance a property of the content model rather than a process around it, then made the build responsible for enforcing it:
- Typed content, with a required source. Every content record carries a mandatory provenance field pointing at the client document and line it came from. TypeScript makes an unsourced record fail to compile — the guarantee starts before anything runs.
- A build gate that walks the real import graph. A pre-build script traces every content module actually reachable from a route and fails the build if any of them is a draft or blank. Unreachable drafts are allowed to exist; unsourced published copy is not.
- Verification of the claim, not the citation. The first version only checked that a citation was present. A sentence then shipped that cited a real line in a real document while the words themselves were still ours — and the build passed. So the gate was rewritten to compare the copy against the text at the line it cites. Typography is normalised away; wording is not.
- An approval gate on every number. No component hardcodes a figure. All eight publishable numbers render through a gate carrying pending or approved status, and the site stays noindex until they are signed off — with the go-live checklist committed alongside the robots rules.
Because a later client brief rewrote individual sentences inside otherwise-unchanged paragraphs, the model also supports per-fragment citations: one sentence can carry several sources, each verified independently.
Provenance is a required field on every record, with per-fragment citations when one sentence draws on several sources.
Walks the import graph from every route and fails the build unless the copy actually matches the line it cites.
Every published figure routed through a pending/approved switch, with the site held noindex until sign-off.
A hosted CMS writing straight to the repo, with CI that re-stamps provenance on client edits and re-runs the gate.
A sentence cited a real line in a real document, and the words were still ours. The build passed. That is when the gate stopped asking whether a citation exists and started asking whether the claim does.
— Why the check was rewritten
§ 04Handover
Giving the client the keys without dismantling the guarantee was the second half of the job:
- Client edits re-stamp themselves. A CMS edit necessarily breaks the gate — new words no longer match the cited line. CI resolves it by re-stamping provenance as a client edit, but only on commits authored by the CMS bot, verified two independent ways, then re-running the gate before anything is pushed. The client's own words are theirs to own; ours still have to be sourced.
- A schema guard for the CMS footguns. The CMS deletes data keys missing from its schema and invents keys present in it. A dedicated check keeps schema and content in agreement so a routine edit cannot quietly drop a field.
- A contact pipeline with one invariant. Never tell a visitor their request was received when it was not. Built as a server action so it works with JavaScript off, writing to the client's own workspace on the critical path with email as best-effort after. Validation is derived from the approved field list rather than duplicating it — two lists that must agree are one list too many. The client ruled out a third-party captcha because it would contradict their own cookie policy, so the defences are a honeypot and rate limiting.
- Animation, deliberately removed. Reveal-on-scroll made content flash as translucent grey during fast scrolling and rendered as blank with JavaScript off. Rather than delete forty call sites, the component became a passthrough — props intact, so a single block can opt back in on purpose instead of animation creeping back everywhere.
- Metadata that stops lying. Framework metadata merges shallowly per key, so every subpage that set only a title was silently inheriting the root card and canonicalising to the homepage. A single helper now mirrors title and description into the social cards and points each canonical at its own route.
§ 05Result
- A site that can prove itself. Every published sentence resolves to a client document and line, checked on every build rather than in a review meeting.
- Claims under the client's control. Eight figures gated on explicit sign-off, with search indexing held back until they clear.
- A handover that holds. The client edits their own copy through a hosted CMS, and the guarantee survives the edit.
- Shipped, then audited. Thirteen routes live, the renamed service URL redirecting in a single hop, social cards unique across every page, and the whole build re-checked against the client's brief at close — including the two points where we flagged a conflict rather than silently resolving it. On the live build it measures 100 for accessibility and best practices, 98 for performance, and no layout shift at all.
— Publishing something you will have to stand behind? Start a conversation.


