Over about two weeks I worked through a fleet of location sites for a multi-site service business. Fourteen sites, near identical, two languages, static HTML behind a CDN. The work produced a long list of defects, and when I sorted them at the end almost all of them had one property in common.
Every one had shipped past a check that was green at the time.
Not missing checks. Green ones. A contrast gate that answered every question it could ask, correctly, while buttons rendered their labels at a contrast ratio of 1.00:1. A patch script that reported twelve sites already patched, having patched none. A legal-page audit at sixteen of sixteen PASS while three of those pages said something untrue.
A fleet does not create new kinds of bugs. It removes your ability to get away with the old ones, through three multipliers:
One fact becomes many copies. The same sentence lives in the HTML, in the JSON-LD, in the manifest, in the plain-text file you publish for machines, in the second language. A fix reaches the copy you happened to be looking at.
One check runs in one state. Cold cache, rest state, one theme, one language, one viewport, your machine. Your visitor is somewhere else.
One assertion repeated fourteen times looks like consensus. A result identical across a whole fleet reads as a strong signal rather than a broken instrument.
What follows is seven of those, chosen so no two share a mechanism.
1. The bug only your visitors have
Start with the one nearly everybody has, because the header block gets copied between projects more than almost any other config.
Cache-Control: public, max-age=31536000, immutable
That is correct and standard for a fingerprinted asset. immutable is a promise that the bytes at this URL will never change, and for /js/app.4f2a9c1b.js the promise is true, because changing the content changes the name.
Now put it on /css/site.css. The promise is now false, and the browser believes it anyway. A returning visitor holds the old stylesheet for a year and will not revalidate, because immutable specifically means do not ask again.
Here is what makes it survive. Think about how you check a deploy landed. You curl it. You open a private window. You check on your phone. You look at the build diff. You run an audit tool. Every one of those samples a client with no prior state, and this bug exists only for a client with prior state. Your entire verification habit is structurally incapable of observing it.
So the site owner emails you saying the change did not go out, and you check again from a cold client, and it is plainly there, and you begin to doubt them. The failure signature is that the person who can see the bug cannot diagnose it, and the person who can diagnose it cannot see it.
# Every URL serving immutable without a fingerprint in the name
curl -sI https://example.com/css/site.css | grep -i cache-control
If it says immutable and the filename has no hash, you have it.
Two traps live inside the fix. The first is that fingerprinting is only real if the hash is a function of the content. A build that stamps a hash from the build timestamp, or from a version constant nobody increments, produces a name that changes when it should not and stays when it should. That is worse than no hash at all, because it looks like the problem is solved. Prove it with a determinism-and-sensitivity pair: build twice with no changes and confirm the names are identical, then change one byte and confirm the name moves.
The second trap only appears once you finally point a real browser at the problem. If the site registers a service worker, the worker answers the fetch before the network sees it, and your local audit is now measuring whatever the worker cached, which is production. One stub fixes it:
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'serviceWorker', { get: () => undefined });
});
Without that stub, a local run of one audit reported 210 failures. With it, 8. The 210 were real, and they were about the previous deploy.
2. A green gate that cannot express the question
The fleet had a contrast checker in its build. It read the design token file and asked, for each pair, whether the foreground cleared 4.5:1 against the background. It answered every one of those questions correctly and it was green.
Meanwhile 158 buttons across 51 pages rendered their label at 1.00:1. The text was the same colour as the fill. It was invisible.
The mechanism is ordinary cascade. The buttons were anchors. The component set both halves of the pair in its rest state:
.btn-primary { background: var(--brand); color: var(--on-brand); }
and its hover rule changed only the background. Elsewhere, in a shared base stylesheet, sat a perfectly reasonable rule:
a:hover { color: var(--brand-ink); }
.btn-primary:hover is specificity (0,2,0) for the properties it sets, but it does not set color at all, so for that one property the winner is a:hover at (0,1,1), which beats the plain class. On hover the label took the link colour while the background took the same value, and the two met.
Nothing is wrong with either colour. Both are legitimate tokens that were never intended to meet. The gate could not catch this because "which colour wins on this element in this state" is a question about the cascade, and the gate only had the palette. Adding hover rows to the token table does not fix it. It is a category error, not a missing test case.
The move worth stealing is how you find out. Back-test the gate by injecting the exact defect and confirming it still passes. Append the offending pair to your stylesheet, run the gate, and watch it stay green. You have now measured your gate's blind spot rather than your site.
Then add a second check that asks the other question. Statically, this one is cheap and it found four more latent instances immediately:
# Any state rule that moves a background without restating a colour
grep -oE '[^{}]*:(hover|focus|focus-visible|active)[^{}]*\{[^}]*\}' dist/site.css \
| grep -E 'background(-color)?:' | grep -vE '(^|;)\s*color:'
The rule that prevents it is one line of discipline: any state rule that moves a background owns its foreground. Restate color on every :hover, :focus and :active that touches background, in every theme.
3. Count your states before you audit them
The sites wrote a theme onto the root element and a high-contrast class onto the body, from two independent stored preferences. Two toggles is not two states. It is four.
All the contrast work up to that point had measured two of them. Failing element counts per state, fleet wide:
| State | Failing elements |
|---|---|
| Light | 557 |
| Dark | 4,220 |
| Light plus high contrast | 960 |
| Dark plus high contrast | 7,912 |
The high-contrast stylesheet defined a light palette and never touched the dark surface tokens. With both on, cards and panels kept painting themselves dark while the text turned black. 1.01:1 across 7,912 elements.
Sit with which state that is. It is the accessibility mode. Turning on the feature intended to help made the page unreadable, and the users most likely to encounter it are the least likely to file a report you can act on. An accessibility mode also carries an assumption of safety, so nobody audits it adversarially.
The general form is mechanical:
# Enumerate every persisted display toggle, then take the product
grep -rn "localStorage.setItem" src/js/
grep -rn "documentElement.setAttribute\|body.classList.add" src/js/
Seed each combination and measure it. The same logic extends past themes. Breakpoints are content dependent, so a second language with longer words needs its own: on this fleet the translated navigation needed a breakpoint 174px wider than the original, and until it got one the header wrapped. And interaction states hide a subtler version, where a flex-shrink default absorbs a 109px overflow as overlap instead. The row still fits the viewport, so no overflow check reports anything, and the text simply sits on top of other text.
Unifying claim: your check runs in one state, and your visitors are distributed across all of them.
4. Patch scripts that report success and change nothing
This is the most portable section, because everyone writes these and nobody tests them.
A string replacement that matches nothing does not fail. It returns the input unchanged, the script exits 0, and the diff is empty. An empty diff after a patch run looks exactly like a patch that was already applied.
It gets worse with an idempotency guard. The usual pattern is to look for a marker and skip if present. Choose a marker string that also occurs elsewhere in the file and the script reports already patched: 12 across the fleet, having patched none. That output is indistinguishable from a correct second run. It is success-shaped.
And a patcher that catches its own parse errors so it can keep going will exit 0 having skipped the files it could not read, and the && behind it deploys the estate unpatched while reporting ok.
All of these are fixed by arithmetic rather than by care:
// Replace, but assert how many times
function sub(text, find, replace, expected = 1) {
const parts = text.split(find);
const found = parts.length - 1;
if (found !== expected) {
throw new Error(`Expected ${expected} match(es) of ${JSON.stringify(find.slice(0, 60))}, found ${found}`);
}
return parts.join(replace);
}
Then, at the end of any fleet run:
if (changed + alreadyDone !== total) throw new Error(`Unaccounted: ${total - changed - alreadyDone}`);
if (skipped) throw new Error(`${skipped} file(s) skipped, which is never ok in a patch run`);
if (isFirstRun && changed === 0) throw new Error('First run changed nothing. The anchor is wrong.');
That assertion set caught real defects for me twice in one session, on anchors I had read carefully and would have sworn were right.
One more habit before any bulk edit: census the shapes. Hash the block each site matches and count distinct hashes. "Remove it everywhere" quietly assumes the markup is identical everywhere, and in a fleet it is usually two or three shapes, of which only some are broken.
# How many distinct shapes am I actually about to edit?
for f in sites/*/index.html; do
grep -A5 'data-widget="rates"' "$f" | sha1sum | cut -c1-8
done | sort | uniq -c
5. Verify the verifier
A result that is identical on every site in a fleet is evidence about your harness, not about your sites.
This happened three separate times in one week. Once, a checker reported the same finding on 100% of the estate; the finding was a bug in the checker. Once, a batch runner transcribed from a working single-site checker silently dropped its conditional predicates, so checks that should not have applied ran anyway and failed everywhere. Once, a hard-coded list of page slugs meant one wrong entry produced an identical false finding on all fourteen.
Uniformity feels like a strong signal. It reads as consensus. It is the one result you should trust least, because a real world of fourteen sites has variance in it.
Three rules, all cheap:
Disprove one by hand. Before acting on any finding that hits every site, open one site and confirm it manually. If it is real on that one, it is probably real. If it is not, you have a checker bug that was about to become fourteen commits.
Mutation-test the gate. A gate reused from an earlier project is green and asserts nothing, which is worse than no gate because it occupies the slot. Introduce the exact defect the gate exists to catch, run it, and confirm it fails. If it passes, the gate is decoration.
Print the denominator, with reasons. Not "18 passed" but "18 passed, 4 skipped because no pricing block, 2 unreachable". A bare total cannot distinguish a clean run from a run that examined almost nothing. In one case a stray continue in verification code made an audit report 9 of 24 when the true answer was 18 of 24; in another, a swallowed shell-quoting exception let a repair script report "0 repaired" having checked nothing at all.
The underlying point is that verification code is untested code. It is the code nobody writes a test for, because it is the test, and a pessimistic audit feels responsible, so a wrong one goes unquestioned for a long time.
// The cheapest possible guard against a silently short run
if (assertionsRun !== pages.length * checksPerPage) {
throw new Error(`Ran ${assertionsRun}, expected ${pages.length * checksPerPage}`);
}
6. The claims your site makes that no tool reads
This is the section with real stakes, and it is the one I would keep if I could keep only one.
A legal-page audit across the fleet returned sixteen of sixteen PASS. Three of those sites were serving a privacy policy that was false. One stated the site operated no forms, on a site with a live contact form that had already received submissions.
The checker asked whether the page mentioned the topic. It never asked whether the sentence was true. Presence is not accuracy, and a template makes presence trivial to achieve on every site at once.
The discipline that fixes it is a claim table. Every sentence that asserts a fact gets one column for the sentence and one column for the command that proves it:
| Sentence in the policy | Command that proves it |
|---|---|
| "We do not operate contact forms." | grep -rl "<form" dist/ | wc -l returns 0 |
| "We use no analytics cookies." | the built HTML contains no analytics loader |
| "We share data with no third parties." | no third-party origin appears in any script src |
If a sentence has no command, it is not templatable. Only template across a fleet what a command can produce. Anything a human has to know about that specific location is per-site asserted content, and it needs a person to sign off on it once.
Two supporting details, both of which cost me time:
You cannot conclude "this site has no forms" from the rendered homepage. Forms live on contact pages, in footers, behind a route. The measurement that produced the false claim sampled one page.
And removing an integration is not the same as removing the promise it made. Take out a third-party embed and the vendor is still named in your privacy policy, which is now a false statement in the other direction. Check it bidirectionally: every vendor named in the policy should appear in the built output, and every third-party origin in the output should appear in the policy.
One editing rule that is counterintuitive and worth stating plainly: never "correct" a promise that is broader than the law requires down to the legal minimum. If a page promises more than it has to, that is a commitment somebody made, not a bug.
7. One fact, many surfaces, and one of them has no reader you can interview
A repair script fixed a factual error across the fleet. It worked. Every page showed the corrected value.
Its file walker had exactly one branch: endsWith('.html'). Every machine-readable surface kept the wrong number. The plain-text files published for language models, the structured data, the JSON. Humans saw the corrected value on every site. Machines saw the old one on every site, for days, and nothing in any pipeline noticed, because every human-facing check was correct.
The same shape appeared twice more in the same fortnight. A page whose runtime-rendered footer was translated and whose served static fallback was not, so the developer's browser showed the right language and the bytes a crawler consumes showed the wrong one. And copy corrected in a visible FAQ while the JSON-LD copy of the same answer went on asserting the opposite, which matters because the structured data is the copy a machine actually parses.
The runnable answer is small and you should add it to any fix that touches a fact:
# After correcting a fact, grep for the OLD value across EVERY extension
rg -F "1.9 mi" --glob '!node_modules' -g '*'
Not -g '*.html'. Every extension, including .txt, .md, .json and anything under .well-known. If the old value survives anywhere, your walker had one branch and your fleet has two truths.
Two related habits. Count the authored copies of any fact before you trust one: a value that appears in prose on every page and in no data file is a hand-copied fact, and it has already drifted. And assert cross-surface equality in the build, so the HTML and the machine-readable copy are compared to each other rather than each being checked alone.
The spec you skimmed
One short one, because it is the most quotable thing I learned and it lands the whole thesis in a sentence.
A robots.txt carried twenty-one named crawler groups and one directive published once, under User-agent: *. Under RFC 9309 a crawler obeys exactly one group, the most specific one that matches it, and it does not inherit anything from the wildcard. Twenty of those crawlers saw nothing. A validator that greps the file for the directive finds it and passes.
# Groups versus directives. If the directive count is 1 and the group count is not, read again.
awk 'BEGIN{RS=""} /User-agent:/{g++} /Content-Signal:/{c++} END{print g" groups, "c" directives"}' robots.txt
The check greps for the string. The parser reads the structure. They disagree, and only one of them is your visitor.
Three rules, if you take nothing else
Mutation-test every gate. Introduce the exact defect it exists to catch and confirm it fails. A gate you have never seen fail is a gate you have never tested.
Make every check print its denominator. What did it examine, how many things did it skip, and why. A bare pass count cannot tell you it examined nothing.
Verify from the position of the person who will be hurt. The returning visitor with a warm cache. The reader in the second language. The machine reading your plain-text surface. The person with the accessibility mode switched on. Each of those is a state your checks do not run in, and each of them is somebody.
If you want the wider map of doing this work across a portfolio of small sites without paying an agency to hold it, that is what I wrote The $20 Dollar Agency for.
Downloads
Three checklists, because they have different audiences and different shelf lives.
Ship-day cache checklist: ten rows, each one command, for the class of bug only your returning visitors have. Download
# Ship-Day Cache Checklist
Every row is one command or one console line. Run it after a deploy, before you tell anyone the
change is live.
The class of bug this catches has one signature: **the owner sees the old thing, you see the new
thing, and you start to doubt them.** That happens because every habit you have for verifying a
deploy samples a client with no prior state, and these bugs only exist for a client with prior
state.
---
## 1. immutable on a path that has no fingerprint
`immutable` promises the bytes at this URL will never change. It is true for
`/js/app.4f2a9c1b.js` and false for `/css/site.css`. A browser that has the false version will
not revalidate for the whole `max-age`, commonly a year.
```bash
curl -sI https://example.com/css/site.css | grep -i '^cache-control'
curl -sI https://example.com/js/site.js | grep -i '^cache-control'
```
FAIL if the value contains `immutable` and the path contains no content hash.
Fix: fingerprint the filename, or drop `immutable` and shorten `max-age`.
`public, max-age=600, stale-while-revalidate=86400` keeps repeat views instant while allowing
the change through.
## 2. The hash is not actually a function of the content
A hash stamped from a build timestamp, a version constant, or the clock changes when it should
not and stays when it should. Worse than no hash, because it looks solved.
```bash
# Determinism: same input, same names
npm run build && ls dist/assets > /tmp/a.txt
npm run build && ls dist/assets > /tmp/b.txt
diff /tmp/a.txt /tmp/b.txt && echo "deterministic"
# Sensitivity: one byte changes the name
echo "/* x */" >> src/css/site.css
npm run build && ls dist/assets > /tmp/c.txt
diff /tmp/b.txt /tmp/c.txt && echo "FAIL: content changed, name did not"
```
You need BOTH to pass. Determinism alone is satisfied by a constant.
## 3. A glob header rule stamps immutable on your 404s
Header rules match the request path, not the response. A rule for `/*.css` applies to
`/nope.css` too, so a 404 gets cached hard, and the URL is poisoned for the life of the
`max-age` even after you publish the real file.
```bash
curl -sI https://example.com/css/does-not-exist.css | grep -iE '^(http|cache-control)'
```
FAIL if a 404 comes back with a long `max-age` or `immutable`.
## 4. A service worker is answering, so your audit measures the last deploy
If the site registers a worker, it responds before the network. Any local audit, any headless
check, any "let me just look at it" is now reading whatever the worker cached.
```js
// Puppeteer / Playwright: run the audit against the SERVER, not the worker
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'serviceWorker', { get: () => undefined });
});
```
In one measured case the same audit reported 210 failures with a live worker and 8 without. The
210 were real and they were about the previous deploy.
Check whether you have one at all:
```bash
curl -s https://example.com/ | grep -oE 'serviceWorker\.register\([^)]*\)'
```
## 5. Vary: Accept makes every Cache API lookup miss
The Cache API matches on headers listed in `Vary` by default. If a response carries
`Vary: Accept`, a later `cache.match(request)` will usually miss, because the runtime `Accept`
header does not match the one stored.
The failure is quiet: the offline page still works, so the worker looks healthy while serving
almost nothing from cache.
```js
// Every lookup, without exception
const hit = await cache.match(request, { ignoreVary: true });
```
If you do content negotiation (an HTML branch and a Markdown branch on one URL) then `Vary`
must be on **both** branches, or a shared cache will hand one audience the other's body.
## 6. A precached URL must byte-match the URL the page requests
`cache.addAll` is atomic. One 404 in the list rejects the whole install and you get a worker
with an empty cache and no error anywhere obvious.
And a precache entry of `/css/site.css` does not serve a page requesting
`/css/site.css?v=3`. Query string included, or it misses.
```js
// Fail loudly at install rather than silently at first offline load
await Promise.all(PRECACHE.map(async (u) => {
const r = await fetch(u, { cache: 'no-cache' });
if (!r.ok) throw new Error(`precache 404: ${u}`);
}));
```
## 7. The cache version constant moved backwards
A patch that writes a literal version rather than incrementing one can move a site's cache
version down. Clients holding a higher version never invalidate.
```bash
# Current live version vs what you are about to ship
curl -s https://example.com/sw.js | grep -oE "CACHE_(NAME|VERSION)\s*=\s*['\"][^'\"]+"
grep -oE "CACHE_(NAME|VERSION)\s*=\s*['\"][^'\"]+" src/sw.js
```
FAIL if the local value sorts lower than the live one.
## 8. One shared cache stamp across a fleet couples every deploy
If several sites share a service worker whose cache name is a single shared constant, bumping it
to fix one site invalidates the caches of all of them. That is usually not what you wanted, and
it makes each site's cache lifetime depend on unrelated work.
Prefer a per-site component in the cache name.
## 9. Verify from a WARM client, once, on purpose
The whole class exists because nobody does this. Keep one browser profile that you never clear,
visit the site before a deploy, deploy, and revisit **without** a hard reload.
That is the only check in this list that reproduces what your actual returning visitor
experiences. It takes ten seconds and it is the one that would have caught everything above.
## 10. Do not accept "it works for me" as evidence
If the owner says the change did not land and it plainly did for you, the disagreement itself is
the finding. Ask what they see, and assume the cache until proven otherwise.
---
## The one-line summary
`immutable` is a promise about a URL, not about a file. Only make it when the URL changes with
the bytes.
Fleet patch-script preflight: the arithmetic that turns a success-shaped no-op into a thrown error, plus a copy-paste helper. Download
# Fleet Patch-Script Preflight
For anyone about to run a script across more than one site. The failures here are not crashes.
They are **success-shaped**: exit code 0, no error, an empty diff, and a summary line that reads
exactly like a correct run.
Copy the helper module at the bottom and the rest of this becomes assertions rather than care.
---
## Before you write the patch
### 1. Census the shapes
"Remove it everywhere" assumes the markup is identical everywhere. In a fleet it is usually two
or three shapes, and often only some are broken.
```bash
for f in sites/*/index.html; do
grep -A5 'data-widget="rates"' "$f" | sha1sum | cut -c1-8
done | sort | uniq -c
```
One hash means one shape and a simple patch. Three hashes means your anchor matches three
different things, and a replacement tuned to one of them will silently do nothing to the others.
### 2. Sweep by the failing VALUE, not by the selector list
If a checker told you which selectors are broken, do not patch that list. Patch everything
carrying the offending value. The list is what the checker could see; the value is the truth.
### 3. Decide what "already done" looks like, and pick a marker that cannot collide
An idempotency marker that also occurs elsewhere in the file makes the script report
`already patched: 12` while patching none. That output is indistinguishable from a correct
second run. Grep for your marker across the fleet BEFORE you use it:
```bash
grep -rc 'jw-patch-v7' sites/ | grep -v ':0' || echo "marker is unused, safe"
```
---
## While it runs
### 4. Every replacement asserts its match count
A `String.replace` that matches nothing returns the input and reports nothing.
```js
function sub(text, find, replace, expected = 1) {
const parts = text.split(find);
const found = parts.length - 1;
if (found !== expected) {
throw new Error(
`Expected ${expected} match(es) of ${JSON.stringify(String(find).slice(0, 60))}, found ${found}`
);
}
return parts.join(replace);
}
```
Use the split/join form rather than a regex or a string `replace` when the replacement text may
contain `$`. In a string replacement `$&`, `$1` and `$'` are substitution patterns and will
corrupt the output. A function replacement escapes nothing, which is the other safe option.
### 5. Never make a node-level edit with a block-level string match
If you are editing structured data, JSON, or anything with nesting, parse it, change the node,
and serialize. A string match on the surrounding block will eventually delete a sibling that
happened to share the container.
Delete a block only when you have proven the block contains nothing but the thing you are
removing.
### 6. Never swallow a parse error
```js
// This exits 0 having skipped the files it could not read.
try { patch(file); } catch (e) { skipped++; }
```
A skip is fine to record and never fine to ignore. If the `&&` behind your script runs a deploy,
you have just shipped the unpatched estate while reporting ok.
---
## Before you believe the result
### 7. The arithmetic
```js
if (changed + alreadyDone !== total) {
throw new Error(`Unaccounted: ${total - changed - alreadyDone} file(s)`);
}
if (skipped) throw new Error(`${skipped} skipped, which is never ok in a patch run`);
if (isFirstRun && changed === 0) throw new Error('First run changed nothing. The anchor is wrong.');
```
### 8. Print the denominator, with reasons
Not `18 passed`. Instead:
```
18 passed, 4 skipped (no pricing block), 2 unreachable (timeout)
```
A bare total cannot distinguish a clean run from a run that examined almost nothing.
### 9. A result identical on every site is evidence about your harness
Uniformity reads as consensus and it is the result you should trust least. A real fleet of
fourteen sites has variance in it.
**Disprove one by hand** before acting on any finding that hits 100%. If it is real on the one
you open, it is probably real everywhere. If it is not, you just avoided fourteen wrong commits.
### 10. Mutation-test any gate you did not write today
A gate copied from an earlier project is green and asserts nothing, which is worse than no gate
because it occupies the slot. Introduce the exact defect it exists to catch, run it, confirm it
fails, then remove the defect. A gate you have never seen fail is a gate you have never tested.
### 11. Your verification code is untested code
It is the code nobody writes a test for, because it *is* the test. A stray `continue` once made
an audit report 9 of 24 when the answer was 18 of 24.
```js
if (assertionsRun !== pages.length * checksPerPage) {
throw new Error(`Ran ${assertionsRun}, expected ${pages.length * checksPerPage}`);
}
```
### 12. Prove the generator is a fixed point
If one member of the fleet is generated rather than hand-maintained, a directory-walking patch
edits its build output and the next build reverts you.
```bash
npm run build -- --out /tmp/build-a
npm run build -- --out /tmp/build-b
diff -r /tmp/build-a /tmp/build-b && echo "fixed point"
```
Patch the source of a generated site, never its output.
---
## Deploy-time traps that look like patch bugs
- **The CLI reads config from the current directory, not from `--dir`.** Publishing folder A while
standing in folder B can pick up B's redirects and headers.
- **A hand-maintained table of per-site IDs has one wrong row**, and the deploy succeeds, into the
wrong site. Preflight every ID against the host's own list before the first upload.
- **A full-manifest deploy expresses deletion by omission.** A script that walks a flat local
mirror will remove anything the mirror lacks, including functions you never touched.
- **Cleaning the publish root can delete deployed artifacts** that were never in your source tree.
- **Your build tool does not remove output it no longer generates.** Stale directories from an
older config sit in the publish root and ship forever. Gate on it:
```js
const stale = ['src', 'node_modules', 'dist'].filter((d) => existsSync(`_site/${d}`));
if (stale.length) throw new Error(`Stale output in publish root: ${stale.join(', ')}`);
```
---
## The one-line summary
If your patch script cannot tell you how many things it changed, how many it skipped, and why,
it has not told you anything.
Claims you cannot template: one column for the sentence, one for the command that proves it. Download
# Claims You Cannot Template
A working sheet for anyone publishing the same policy, footer or "about" copy across more than
one site.
The rule this exists to enforce: **only template across a fleet what a command can produce.**
Everything else is a claim about one specific site, and it needs a person to assert it once.
A legal-page audit that returned sixteen of sixteen PASS sat on top of three pages that were
saying something untrue. The checker asked whether the page mentioned the topic. It never asked
whether the sentence was true. Presence is not accuracy, and a template makes presence trivial
to achieve everywhere at once.
---
## How to use this
For every sentence on a shared page that asserts a fact, fill in the right column. If you cannot
write a command, the sentence is not templatable and it moves to the per-site list.
| Sentence | Command that proves it |
|---|---|
| "We do not operate contact forms." | `grep -rl "<form" dist/ \| wc -l` returns 0 |
| "We collect no personal information." | no `<input>` of type email, tel or text in `dist/` |
| "We use no analytics or advertising cookies." | no analytics or ad loader in any built `<script src>` |
| "We share no data with third parties." | every third-party origin in `dist/` appears in the policy |
| "Our site is available in English and Spanish." | both locale trees exist and every page has a counterpart |
| "All prices shown include tax." | the price field in the data file has a tax-inclusive flag |
| "We are open 24 hours." | the hours field in the data file says so, per site |
| "This location has parking." | an amenity field, per site, non-empty |
| "We are licensed and insured." | a coverage field in the data file is non-empty, per site |
| "We have served this area since YEAR." | a founded field in the data file, per site |
The first four are templatable. The rest are per-site, and every one of them has been wrong on
somebody's fleet because it was templated.
---
## The checks that make it real
### Forms: you cannot conclude "no forms" from the homepage
Forms live on contact pages, in footers, and behind routes. Walk the built output, not one URL.
```bash
grep -rl "<form" dist/ | sed 's|^dist||'
```
Also beware that some hosts strip unknown attributes, so a marker attribute you were grepping
for may not survive to production. Grep the deployed bytes, not your source.
### Third parties: check it in BOTH directions
Removing an integration is not the same as removing the promise it made. Take out an embed and
the vendor stays named in your policy, which is now false in the other direction.
```bash
# Origins actually loaded by the built site
grep -rhoE 'src="https?://[^/"]+' dist/ | sed 's|.*//||' | sort -u > /tmp/origins.txt
# Vendors named in the policy
grep -oiE '(vendor-a|vendor-b|vendor-c)' dist/privacy/index.html | sort -u > /tmp/named.txt
# Each list should explain the other
comm -23 /tmp/origins.txt /tmp/named.txt # loaded but not disclosed
comm -13 /tmp/origins.txt /tmp/named.txt # disclosed but not loaded
```
### Absence claims: scope them to exactly what you checked
"We found no X" is only true for the surface you examined, on the date you examined it. Write the
scope into the sentence, or do not make the claim.
Bad: "This site sets no cookies."
Better: "The pages listed in our sitemap set no cookies on first load, checked YYYY-MM-DD."
### One fact, many surfaces
A fact fixed in the HTML and left standing in the JSON-LD, the plain-text file for machines, or
the second language is still published, just to a different reader.
```bash
# After correcting any published fact, grep for the OLD value across EVERY extension
rg -F "the old value" --glob '!node_modules' -g '*'
```
Not `-g '*.html'`. Include `.txt`, `.md`, `.json`, `.xml` and anything under `.well-known`.
### Count the authored copies before you trust one
A fact that appears in prose on every page and in no data file is a hand-copied fact, and it has
already drifted. Move it into a data file, render it from there, and the drift becomes
impossible rather than unlikely.
```bash
# How many independent copies of this number exist?
rg -F "$VALUE" -c --glob '!node_modules' | sort -t: -k2 -rn | head
```
### Shared chrome carries per-site facts
A footer, a phone number, a booking link or an address in a shared component will be correct on
the site you built it from and wrong everywhere else. The strongest check a fleet allows is:
**a sibling site's value appearing on this site is a hard failure**, not a warning.
```bash
# Every site's phone number should appear on exactly one site
for site in sites/*/; do
for phone in $(cat phones.txt); do
grep -ql "$phone" "$site" && echo "$(basename $site) contains $phone"
done
done | sort
```
---
## Editing rules
**Never narrow a promise down to the legal minimum.** If a page promises more than the law
requires, somebody made that commitment deliberately. Correcting it "down" is a business
decision, not a copy fix.
**Never write a claim about coverage, licensing or insurance from a template.** Gate it on a
per-site data field that a human filled in, and let the sentence be absent when the field is
empty. An absent sentence is safe; a templated one is a statement about a real person's
liability.
**A banned-word sweep will flag the compliant sentences too**, including the sentence that states
the rule. Read every hit. A blind find and replace over policy copy will "correct" the sentence
explaining why the word is not used.
**Run your copy lint inside structured data as well as prose.** A claim you already fixed in the
visible text goes on being published if the JSON-LD copy was not part of the sweep. That copy is
the one machines read.
**Bilingual pages carry the same sentence twice in two encodings.** A literal character in a JSON
string and an HTML entity in the visible markup are the same sentence to a reader and two
different strings to your script. One replace fixes half and looks finished.
---
## The one-line summary
If a sentence on your shared template has no command that proves it, it is not shared content.
It is a claim about one site, wearing shared clothes.
Related reading
Four pages told Google they were the About page and nothing flagged it: the same shape in structured data, where every validator passes because each node is valid on its own.
Accessibility toggles combine, so audit every state: the long form of section 3, with the state matrix worked through.
Your JSON-LD validates but Google ignores it because the graph is broken: valid nodes, broken relationships, nothing reported.
Why a Multi-Location Schema Audit Exists: templating identity across near identical location pages, which is where most of these multiply.
Retrofitting a design system: what broke: the cascade failures in section 2, from the other direction.
Fact-check notes and sources
immutable means do not revalidate: RFC 8246 defines the Cache-Control extension as indicating the response body will not change over its freshness lifetime, so a client need not revalidate even on reload. RFC 8246
A crawler obeys one group and does not inherit from the wildcard: RFC 9309 section 2.2.1 specifies that a crawler must match the most specific group of records and that groups are not merged. RFC 9309, Robots Exclusion Protocol
Specificity is compared per declaration, not per rule: a rule with higher specificity does not win properties it never sets, which is why a:hover can take color from a class selector. CSS Cascading and Inheritance Level 5
A media query adds no specificity: @media wraps rules without contributing to their selector weight, so a rule inside one ties with the same selector outside it and is decided on source order. CSS Conditional Rules Module
4.5:1 is the AA threshold for normal text: WCAG 2.2 Success Criterion 1.4.3 Contrast (Minimum). W3C WCAG 2.2
A service worker intercepts fetches before the network: the Fetch event handler can respond from a cache, which is why an audit against a site with a registered worker may never reach the server it thinks it is testing. MDN, Service Worker API
This post is informational, not legal or SEO consulting advice. The sites described are anonymised and no client, brand, location or vertical is identified. Mentions of third parties are nominative fair use and no affiliation is implied.