· 4 min read
One document, three destinations
A contact form that produces a real project brief (on screen, as a PDF and as a stored record) built so the three can never disagree about what it says.
What it does
The contact form on this site produces a document. As you type, a project brief builds itself beside the form, with your problem in your own words, what your budget band realistically buys, and roughly how a build that size runs. You can copy it, or download it as a branded PDF and take it to whoever signs things off.
The same brief lands in my inbox as a PDF attachment, and the same brief is stored so I can pull it up later.
So that's three destinations, and the interesting part is what stops them drifting apart.
How copies drift apart
Anything rendered in more than one place will eventually say different things in each place. Say a visitor downloads a PDF saying their budget buys "several parts of the business joined up". The email I read says something slightly different, because that copy was typed a second time into the email template three weeks later. Both files are valid and both render fine. No test fails, because each half is internally consistent.
I only notice when a client quotes the PDF back at me and I don't recognise the sentence.
There's no stack trace for that kind of bug either, so I think the only real defence is making it impossible to have two copies.
One module owns the words
There's one pure module that owns everything a brief says and knows nothing about how it's displayed.
export type BriefDoc = {
title: string;
reference: string;
contact: { name: string; email: string };
budget: Budget;
timeline: Timeline;
problem: string;
sections: BriefSection[];
phases: ReadonlyArray<BuildPhase>;
start: string;
};
export function buildBrief(enquiry: Enquiry, now = new Date()): BriefDoc { … }The live preview, the PDF renderer, the email body and the database row all get handed one of these, and none of them contains a sentence of its own. When the copy for a budget band changes, it changes in one table and every destination follows, because none of them has its own copy to forget about.
That also covers things you wouldn't think of as copy. Budget labels like "$10,000 – $25,000" live in the same module and are read by the form's <select>, by the PDF and by the email. They used to be typed into the select and typed again into the FAQ answer that quotes the price, which is how a site ends up advertising a price it doesn't charge any more.
Deriving instead of writing
Once the content is data, some of it doesn't need writing at all.
The brief tells you roughly how a project of your size runs (scoping, build, handover) with week ranges. That used to be a fixed FAQ answer, and a fixed answer to "how soon could you start?" can't be honest, because the truthful answer depends on two things the visitor has already told me.
So it's derived, and the two inputs do different jobs. Budget sets how long the work takes, because it's a proxy for scope. Timeline sets when the clock starts.
export const BUILD_SHAPE: Record<Budget, ReadonlyArray<BuildPhase>> = {
"under-5k": [ { label: "Week 1", weight: 1, body: "…" }, … ],
"10k-25k": [ { label: "Weeks 1–2", weight: 1.1, body: "…" }, … ],
"25k-plus": [ { label: "Weeks 1–3", weight: 1.4, body: "…" }, … ],
};
export const START_BY_TIMELINE: Record<Timeline, string> = {
asap: "I'd aim to start scoping this week.",
"3-6 months": "No rush. We can scope now and start when it suits you.",
};A large ASAP enquiry and a small exploratory one now produce really different documents, where they used to share one hedged sentence. The proportional bar on the page reads its widths from those same weight values, so the picture can't disagree with the words next to it either.
One band gets special handling. If someone selects "not sure yet", the phases are labelled First / Then / Last with no week numbers at all. Nobody has sized that project yet, so any week numbers would be made up.
Tests that stop a decision being undone
Most of the tests on this module are there to catch someone undoing a decision.
it("promises ownership in exactly one place", () => {
const everywhereElse = [
...HOW_I_WORK.map((s) => `${s.step} ${s.body}`),
...WHAT_I_WONT_DO,
...FAQ.map((f) => `${f.q} ${f.a}`),
].join(" ");
expect(everywhereElse).not.toMatch(/no lock-?in/i);
expect(everywhereElse).not.toMatch(/you own/i);
});The ownership terms are conditional on purpose. Code transfers on final payment, unless a licence is agreed instead. That statement is worth nothing if a cheerful "no lock-in!" shows up elsewhere on the same page, because a client can hold a written document to whichever sentence suits them. Ownership can be promised in one place only, and the suite fails if it creeps back in anywhere else, so I don't have to remember the rule.
There's another test in the same file that fails the build if a literal $ figure appears in any prose file, and another that fails if the word "workflow" appears in client-facing copy, because that's jargon describing how the work is built, not what someone's buying.
These tests are cheap and a bit unusual, and I think they catch things normal tests can't. They answer "is this document still saying what we agreed it says", where a normal test answers "is this function correct".