@piercebarney/whs-eleventy 2026.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/cli.js +55 -0
- package/lib/_project.js +42 -0
- package/lib/a11y.js +178 -0
- package/lib/check-links.js +288 -0
- package/lib/compliance.js +677 -0
- package/lib/content-check.js +45 -0
- package/lib/doctor.js +416 -0
- package/package.json +44 -0
- package/scripts/bundle-standard.js +35 -0
- package/standard/CHANGELOG.md +625 -0
- package/standard/core.md +1493 -0
package/standard/core.md
ADDED
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
# Web project house style — CORE (stack-agnostic)
|
|
2
|
+
|
|
3
|
+
**Version:** 2026-09-01 · **Status:** active
|
|
4
|
+
|
|
5
|
+
This is the stack-agnostic contract every web project follows, regardless of
|
|
6
|
+
framework, host, or CSS system. It says **what** must be true, with concrete
|
|
7
|
+
specs. It never says **how** — that lives in one implementation doc per stack
|
|
8
|
+
(`stacks/eleventy-netlify.md`, `stacks/phoenix.md`, `stacks/sveltekit.md`).
|
|
9
|
+
|
|
10
|
+
**How to use this:** at project start, run the stack-selection flow
|
|
11
|
+
(`#adopting`). Then load **this file plus the one `stacks/*.md` file** matching
|
|
12
|
+
the project's `framework:` key, and read the `styling` subsection named by its
|
|
13
|
+
`css:` key. Where an impl doc is silent on a contract here, this file governs
|
|
14
|
+
unchanged. Where they appear to conflict, that is a bug — fix one doc, never
|
|
15
|
+
fork the principle.
|
|
16
|
+
|
|
17
|
+
Each chapter: **Contract** (what MUST be true) · **Specs** (concrete
|
|
18
|
+
values/thresholds) · **Rationale** (why, when non-obvious) · **Verify** (how the
|
|
19
|
+
gate or audit page proves it) · **Implementation** (pointer to your stack doc).
|
|
20
|
+
The two closing chapters (`#out-of-scope`, `#anti-patterns`) are reference lists,
|
|
21
|
+
not contracts.
|
|
22
|
+
|
|
23
|
+
Chapter headings are `## <slug> — <Title>`. The `<slug>` is a frozen ID; cite it
|
|
24
|
+
as `core.md#<slug>`. Never cite by number.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# Part A — Repository & stack
|
|
29
|
+
|
|
30
|
+
## repo-hygiene — Repository hygiene
|
|
31
|
+
|
|
32
|
+
**Contract.** The repo is legible: two docs and no third, a clean ignore list, a
|
|
33
|
+
committed lockfile, one default branch, small self-contained changes.
|
|
34
|
+
|
|
35
|
+
**Specs.**
|
|
36
|
+
- Exactly two prose docs: `README.md` (what the site is + how to run and deploy
|
|
37
|
+
it — the narrative) and `CLAUDE.md` / `AGENTS.md` (the operating rules an
|
|
38
|
+
assistant follows, including this project's stack-selection block per
|
|
39
|
+
`#adopting`). No `PROJECT.md` or third rules file.
|
|
40
|
+
- `.gitignore` covers: dependencies, build output, local caches, `.env`,
|
|
41
|
+
OS cruft (`.DS_Store`). Commit `.env.example`.
|
|
42
|
+
- Commit the dependency lockfile.
|
|
43
|
+
- Default branch is **`main`**.
|
|
44
|
+
- Conventional-commit messages; one logical change per commit (and per pull
|
|
45
|
+
request, on projects that use them — see `#ci-cd`).
|
|
46
|
+
|
|
47
|
+
**Verify.** Manual review. The audit page's infra tab flags a committed `.env`,
|
|
48
|
+
a missing lockfile, a non-`main` default branch.
|
|
49
|
+
|
|
50
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
51
|
+
|
|
52
|
+
## runtime-pin — Runtime & toolchain pinning
|
|
53
|
+
|
|
54
|
+
**Contract.** The language/runtime version is pinned in exactly one file — the
|
|
55
|
+
one the stack's tooling reads natively — and nowhere else.
|
|
56
|
+
|
|
57
|
+
**Specs.**
|
|
58
|
+
- One source of truth for the runtime version. Not two (e.g. not a version file
|
|
59
|
+
*and* a host setting).
|
|
60
|
+
- CI and local dev both read that same file.
|
|
61
|
+
|
|
62
|
+
**Rationale.** Two version sources drift; a build that passes locally then fails
|
|
63
|
+
in CI on a different runtime is the failure this prevents.
|
|
64
|
+
|
|
65
|
+
**Verify.** The audit page compares the pinned version to the running version.
|
|
66
|
+
|
|
67
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
68
|
+
|
|
69
|
+
## config-idiom — Configuration is idiomatic and minimal
|
|
70
|
+
|
|
71
|
+
**Contract.** Configuration leans on framework defaults, reads as a table of
|
|
72
|
+
contents, and uses one mechanism per concern.
|
|
73
|
+
|
|
74
|
+
**Specs.**
|
|
75
|
+
- Prefer defaults; only configure what genuinely differs.
|
|
76
|
+
- When a config file starts juggling many responsibilities, split the bodies
|
|
77
|
+
into modules that the main file registers — the main file stays a manifest.
|
|
78
|
+
- One mechanism for redirects and headers (not a mix of a config file and stray
|
|
79
|
+
sidecar files).
|
|
80
|
+
- Environment access is centralised (see `#seo-urls` for the build-context
|
|
81
|
+
module and `#secrets` for tokens).
|
|
82
|
+
|
|
83
|
+
**Verify.** Review.
|
|
84
|
+
|
|
85
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# Part B — Content & rendering
|
|
90
|
+
|
|
91
|
+
## content-model — Externalized, validated content
|
|
92
|
+
|
|
93
|
+
**Contract.** Page content lives in structured data separate from templates, and
|
|
94
|
+
a build-time schema gate hard-fails the build on invalid content.
|
|
95
|
+
|
|
96
|
+
**Specs.**
|
|
97
|
+
- User-visible copy (titles, descriptions, body prose, lists) is authored as
|
|
98
|
+
data, not embedded in markup. Templates render data and nothing else.
|
|
99
|
+
- A validator runs before the build completes and **exits non-zero** on any
|
|
100
|
+
error, printing every error (not just the first), each as `path: expected X`.
|
|
101
|
+
- Validation covers: required keys present; correct types; enum values; **unique
|
|
102
|
+
ids/slugs** (they become URLs and anchors); array lengths the markup depends
|
|
103
|
+
on; every page has a non-empty `title` and `meta_description` within length
|
|
104
|
+
bounds (see `#seo-meta`).
|
|
105
|
+
- The same validator is runnable standalone (fast, no full build) and is part of
|
|
106
|
+
the one gate (`#the-gate`).
|
|
107
|
+
- Where non-developers edit content, provide editor-time validation (a schema
|
|
108
|
+
the editor understands) in addition to the build gate; a Git-backed CMS over
|
|
109
|
+
the same data files is acceptable — a separate database for site content is
|
|
110
|
+
not. Where an **LLM agent** edits the data files directly, it follows a
|
|
111
|
+
documented per-project content-ops protocol: what it may change (the content
|
|
112
|
+
files, not code or structure), a pre-commit content check (the
|
|
113
|
+
content-relevant slice of the gate), and a `content:` commit convention. See
|
|
114
|
+
your stack's impl doc.
|
|
115
|
+
- **Drafts** are a data flag. A draft renders in local and preview builds
|
|
116
|
+
(reviewable) but is excluded from the production build and the sitemap.
|
|
117
|
+
Publishing is removing the flag.
|
|
118
|
+
- The project records its target publishing rate for new content pages in
|
|
119
|
+
`CLAUDE.md`. Bulk-publishing many pages at once depresses indexing; pace
|
|
120
|
+
deliberately.
|
|
121
|
+
|
|
122
|
+
**Rationale.** Content that lives in templates can't be validated, can't be
|
|
123
|
+
edited safely by non-developers, and drifts from its own stated facts (a
|
|
124
|
+
calculator whose copy says "18%" while the code uses 15%).
|
|
125
|
+
|
|
126
|
+
**Verify.** The gate fails the build on invalid content. The audit page shows
|
|
127
|
+
validation status.
|
|
128
|
+
|
|
129
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
130
|
+
|
|
131
|
+
## rendering — Rendering & progressive enhancement
|
|
132
|
+
|
|
133
|
+
**Contract.** Core content and navigation work with no client JavaScript. Client
|
|
134
|
+
JS enhances; it never gates the essentials. Any logic that renders on both the
|
|
135
|
+
server and the client is written once.
|
|
136
|
+
|
|
137
|
+
**Specs.**
|
|
138
|
+
- The server-rendered (or prerendered) HTML is complete: a crawler or a
|
|
139
|
+
JS-disabled browser gets the full content and working links.
|
|
140
|
+
- Client JS adds interactivity (filtering, live recompute, a quiz). A
|
|
141
|
+
`<noscript>` note covers only the interactive affordances, never the content.
|
|
142
|
+
- If a feature re-renders content in the browser from the same data the build
|
|
143
|
+
used, there is **one renderer module** shared by both sides — never two copies
|
|
144
|
+
of the same logic.
|
|
145
|
+
- Nothing that matters (core content, primary CTA) blocks on a network fetch.
|
|
146
|
+
Live data arrives after first paint and degrades to a clear fallback state
|
|
147
|
+
(see `#third-parties`).
|
|
148
|
+
|
|
149
|
+
**Verify.** Load a key page with JS disabled: content and links present.
|
|
150
|
+
Grep for duplicated render logic.
|
|
151
|
+
|
|
152
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
# Part C — Assets & styling
|
|
157
|
+
|
|
158
|
+
## assets — Self-contained assets
|
|
159
|
+
|
|
160
|
+
**Contract.** Every static asset is served from your own origin. Generated
|
|
161
|
+
assets are generated from the brand source, never hand-authored, never allowed
|
|
162
|
+
to go stale.
|
|
163
|
+
|
|
164
|
+
**Specs.**
|
|
165
|
+
- Fonts, CSS, JS, icons, and images are self-hosted. **No CDN links, no Google
|
|
166
|
+
Fonts** — there is no feature-need exception for a static asset. A system font
|
|
167
|
+
stack also satisfies "no external font origin".
|
|
168
|
+
- Fonts, when custom, are subset to the scripts actually used, declared with
|
|
169
|
+
`font-display: swap` and a metric-matched fallback, and the above-the-fold
|
|
170
|
+
face is preloaded. **Verify the subset actually covers the site's text** — a
|
|
171
|
+
mis-scoped subset (e.g. Cyrillic-only) loads with no error and silently falls
|
|
172
|
+
back to the system stack, so the custom font is dead weight nobody notices.
|
|
173
|
+
- The web manifest is served as `application/manifest+json` (some hosts default
|
|
174
|
+
a `.webmanifest` to `application/octet-stream`; set the header).
|
|
175
|
+
- Favicons, app icons (`apple-touch-icon`), the web manifest, and OG images are
|
|
176
|
+
**generated from the brand token source** (`#brand-source`) as part of the
|
|
177
|
+
build — never a hand-made file, never a remote request. They are gitignored
|
|
178
|
+
and regenerated, or committed with a CI check that fails on `git diff` after
|
|
179
|
+
regeneration.
|
|
180
|
+
- Raster images ship with explicit `width`/`height` (or `aspect-ratio`).
|
|
181
|
+
|
|
182
|
+
**Rationale.** Self-hosting buys reproducible builds, no third-party runtime
|
|
183
|
+
failure, and a short privacy policy. Generating brand assets means a brand
|
|
184
|
+
change can't leave a stale favicon or share card behind.
|
|
185
|
+
|
|
186
|
+
**Verify.** The link/reference check (`#internal-links`) fails on any external
|
|
187
|
+
origin in the output not in the manifest (`#third-parties`) and on any missing
|
|
188
|
+
referenced asset. The audit page shows the generated-asset gallery.
|
|
189
|
+
|
|
190
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
191
|
+
|
|
192
|
+
## styling — Styling constants
|
|
193
|
+
|
|
194
|
+
**Contract.** One CSS system per project, one token source, strict-CSP-safe, and
|
|
195
|
+
contrast-correct at the token level.
|
|
196
|
+
|
|
197
|
+
**Specs.**
|
|
198
|
+
- Pick one CSS approach and record it in `CLAUDE.md` (`css:` key). Options are
|
|
199
|
+
defined per stack in the impl doc's `styling` section.
|
|
200
|
+
- **One token source** feeds everything (`#brand-source`): the live stylesheet,
|
|
201
|
+
generated OG cards, generated icons, and `theme-color` all read the same
|
|
202
|
+
colours/type/spacing.
|
|
203
|
+
- **Strict-CSP-safe:** no `style-src 'unsafe-inline'`, no inline `style=`
|
|
204
|
+
attributes in templates, no per-page `<style>` blocks. Utility-class systems
|
|
205
|
+
(Tailwind) are fine; inline style strings are not.
|
|
206
|
+
- Contrast meets WCAG AA. When a pair fails, fix the **token**, not the usage.
|
|
207
|
+
- Theme is applied via a data attribute set before first paint (`#theme`).
|
|
208
|
+
- A second stylesheet only for a genuinely separable widget, justified in a
|
|
209
|
+
comment.
|
|
210
|
+
|
|
211
|
+
**Verify.** CSP has no `'unsafe-inline'` for styles. The audit page flags any
|
|
212
|
+
sub-AA token pair.
|
|
213
|
+
|
|
214
|
+
**Implementation.** See your stack's impl doc `styling` section (one subsection
|
|
215
|
+
per supported CSS system).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
# Part D — Client behaviour
|
|
220
|
+
|
|
221
|
+
## client-logic — Client-side logic
|
|
222
|
+
|
|
223
|
+
**Contract.** Any logic that computes a result a user relies on is a pure,
|
|
224
|
+
independently testable function. Page code only wires it to the DOM.
|
|
225
|
+
|
|
226
|
+
**Specs.**
|
|
227
|
+
- A formula, unit conversion, price, tax, or date calculation lives in its own
|
|
228
|
+
module as a pure function (inputs → result, no DOM, no globals), loadable by
|
|
229
|
+
both the browser and the test runner.
|
|
230
|
+
- Page/component code does only: read inputs → call the pure function → format →
|
|
231
|
+
write the DOM.
|
|
232
|
+
- **The wiring is CSP-safe** — no inline `<script>`, no `on*=` handlers; the
|
|
233
|
+
baseline `script-src 'self'` (`#security-headers`) rejects both. It is a
|
|
234
|
+
loaded script file. For a per-page calculator, the browser-only wiring can sit
|
|
235
|
+
in a `typeof document !== "undefined"` block inside the *same* pure module, so
|
|
236
|
+
one file both loads DOM-free in the test runner and wires the page in the
|
|
237
|
+
browser. (An unhashed inline script is only an option on a stack that hashes
|
|
238
|
+
it into the CSP — most static setups can't.)
|
|
239
|
+
- Feature-detect and guard (`if (el) …`, `try/catch` around storage APIs).
|
|
240
|
+
- **No client-side error tracking by default.** Correctness is covered by the
|
|
241
|
+
domain tests (`#domain-tests`), not by watching production logs. (Adding error
|
|
242
|
+
tracking is adding a third-party — `#third-parties`.)
|
|
243
|
+
|
|
244
|
+
**Verify.** `#domain-tests` covers the pure functions. Review flags a formula
|
|
245
|
+
computed inline in a template, an inline `<script>` or `on*=` handler in the
|
|
246
|
+
output, or DOM code in the "pure" module's top level.
|
|
247
|
+
|
|
248
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
249
|
+
|
|
250
|
+
## theme — Light/dark theme
|
|
251
|
+
|
|
252
|
+
**Contract.** The theme is set before first paint, respects the OS preference
|
|
253
|
+
when unset, and persists the user's choice.
|
|
254
|
+
|
|
255
|
+
**Specs.**
|
|
256
|
+
- A tiny script, first thing in `<head>`, reads the stored preference and sets a
|
|
257
|
+
root data attribute before paint, wrapped in `try/catch`. Under the baseline
|
|
258
|
+
`script-src 'self'` it is a **separate render-blocking file** (`<script
|
|
259
|
+
src="/theme-init.js">`, no `defer`) — or a hashed inline block on a stack that
|
|
260
|
+
wires hashes into the CSP. An unhashed inline script is blocked.
|
|
261
|
+
- With no stored preference, fall back to `prefers-color-scheme`.
|
|
262
|
+
- The toggle persists the choice to local storage (or the stack's equivalent).
|
|
263
|
+
- `theme-color` (light + dark) and `color-scheme: light dark` values come from
|
|
264
|
+
the brand source (`#brand-source`).
|
|
265
|
+
|
|
266
|
+
**Rationale.** Setting the theme after paint causes a flash; not persisting it
|
|
267
|
+
surprises the returning visitor.
|
|
268
|
+
|
|
269
|
+
**Verify.** Reload in each theme — no flash. The audit page shows the resolved
|
|
270
|
+
`theme-color` values.
|
|
271
|
+
|
|
272
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
# Part E — SEO & metadata
|
|
277
|
+
|
|
278
|
+
## seo-urls — Canonical & absolute URLs
|
|
279
|
+
|
|
280
|
+
**Contract.** Every canonical, Open Graph, and sitemap URL is absolute and
|
|
281
|
+
anchored to the **production** origin, on every build in every context. One
|
|
282
|
+
module is the single reader of environment / deploy context.
|
|
283
|
+
|
|
284
|
+
**Specs.**
|
|
285
|
+
- A `site` config holds `{ name, url }` where `url` is the production origin.
|
|
286
|
+
Canonical/OG/sitemap URLs always use it — never the current deploy's origin.
|
|
287
|
+
- Exactly one `<link rel="canonical">` per page, absolute, trailing slash
|
|
288
|
+
matching the permalink.
|
|
289
|
+
- A single **build-context module** is the only place that reads environment
|
|
290
|
+
variables for the site (build scripts may read their own tokens — `#secrets`).
|
|
291
|
+
It exposes at least `{ target, isProduction, commit }`. `isProduction` is
|
|
292
|
+
driven by a variable **you set** on the deploy path, not inferred from the
|
|
293
|
+
host.
|
|
294
|
+
- Everything context-dependent (robots rules, the `noindex` flag, audit-page
|
|
295
|
+
build facts) derives from that module.
|
|
296
|
+
|
|
297
|
+
**Verify.** Grep the output for the production origin in every canonical/OG/
|
|
298
|
+
sitemap URL. Grep the codebase: exactly one file reads `process.env` / the
|
|
299
|
+
env equivalent for the site.
|
|
300
|
+
|
|
301
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
302
|
+
|
|
303
|
+
## seo-meta — Per-page metadata, Open Graph & Twitter
|
|
304
|
+
|
|
305
|
+
**Contract.** Every page has a validated title and description and a complete,
|
|
306
|
+
data-driven Open Graph + Twitter tag set. Tags are emitted by one shared
|
|
307
|
+
implementation, never hand-written per page.
|
|
308
|
+
|
|
309
|
+
**Specs.**
|
|
310
|
+
- `title` and `meta_description` are required per page and validated for
|
|
311
|
+
non-emptiness and length (`#content-model`).
|
|
312
|
+
- One shared head component/partial emits, on **every** page:
|
|
313
|
+
|
|
314
|
+
| Tag | Value |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `og:type` | `website` by default; `article` for dated content |
|
|
317
|
+
| `og:title` | the page title (a `" — Site"` suffix may be dropped) |
|
|
318
|
+
| `og:description` | the page `meta_description` |
|
|
319
|
+
| `og:url` | absolute, production origin, identical to the canonical |
|
|
320
|
+
| `og:site_name` | the site name |
|
|
321
|
+
| `og:locale` | `en_US` (or the real locale) |
|
|
322
|
+
| `og:image` | absolute URL to a self-hosted image (`#og-image`) |
|
|
323
|
+
| `og:image:alt` | short description of the image |
|
|
324
|
+
| `twitter:card` | `summary_large_image` when the image is ≥1200×630, else `summary` |
|
|
325
|
+
| `twitter:title` / `twitter:description` / `twitter:image` | mirror the `og:*` values |
|
|
326
|
+
|
|
327
|
+
- `article`-type pages additionally emit `article:published_time` /
|
|
328
|
+
`article:modified_time` (ISO 8601), `article:author`, and
|
|
329
|
+
`article:section` / `article:tag` where the data has them.
|
|
330
|
+
- `<html lang="…">` is set; one `<h1>` per page; headings ordered, no level
|
|
331
|
+
skipped.
|
|
332
|
+
|
|
333
|
+
**Verify.** Paste a deployed URL into an OG debugger; the audit page renders the
|
|
334
|
+
full tag set per page. Validation fails a page with a missing/over-length
|
|
335
|
+
`meta_description`.
|
|
336
|
+
|
|
337
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
338
|
+
|
|
339
|
+
## og-image — OG image spec & generation
|
|
340
|
+
|
|
341
|
+
**Contract.** Every page resolves to a self-hosted, absolute, production-origin
|
|
342
|
+
OG image that is generated from the brand tokens at build time.
|
|
343
|
+
|
|
344
|
+
**Specs.**
|
|
345
|
+
- **1200×630** (1.91:1), under ~1 MB, **PNG or JPG — not SVG, not WebP** (some
|
|
346
|
+
scrapers reject them), served from `/og/`.
|
|
347
|
+
- Absolute, production origin, on every build regardless of target.
|
|
348
|
+
- A site-wide `default` image is used whenever a page supplies none; per-page
|
|
349
|
+
override via a frontmatter/data key resolved so the template reads one value.
|
|
350
|
+
- Cards are **generated from the brand token source** (`#brand-source`) as part
|
|
351
|
+
of the build. Never hand-drawn in an editor. Never committed stale (or
|
|
352
|
+
committed with a CI `git diff --exit-code` gate after regeneration).
|
|
353
|
+
- The generator is cache-aware (skips cards whose inputs are unchanged) so it is
|
|
354
|
+
cheap on incremental rebuilds.
|
|
355
|
+
|
|
356
|
+
**Verify.** The audit page's OG gallery shows every card at true 1.91:1 with its
|
|
357
|
+
dimensions and file size; the link check fails on a missing `og:image` file.
|
|
358
|
+
|
|
359
|
+
**Implementation.** See your stack's impl doc, same slug — the generation
|
|
360
|
+
toolchain differs substantially per stack.
|
|
361
|
+
|
|
362
|
+
## structured-data — JSON-LD
|
|
363
|
+
|
|
364
|
+
**Contract.** Every page emits valid JSON-LD appropriate to its type.
|
|
365
|
+
|
|
366
|
+
**Specs.**
|
|
367
|
+
- One block per page type: `WebSite` (home), plus `WebApplication` / `Article` /
|
|
368
|
+
`Product` / `BreadcrumbList` / `Organization` as the page warrants.
|
|
369
|
+
- Every block parses as JSON and carries the required fields for its declared
|
|
370
|
+
`@type`.
|
|
371
|
+
- Built by serialising a data object, never hand-written string templating.
|
|
372
|
+
- After any change to the JSON-LD shape, run it once through Google's Rich
|
|
373
|
+
Results Test (catches eligibility, which the offline parse check can't).
|
|
374
|
+
|
|
375
|
+
**Verify.** The link/reference check asserts every `application/ld+json` block
|
|
376
|
+
parses and has its `@type`'s required fields.
|
|
377
|
+
|
|
378
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
379
|
+
|
|
380
|
+
## sitemap-robots — Sitemap, robots, feeds
|
|
381
|
+
|
|
382
|
+
**Contract.** `sitemap.xml`, `robots.txt`, and (for article sites) `feed.xml`
|
|
383
|
+
are generated from the content set. None is hand-maintained.
|
|
384
|
+
|
|
385
|
+
**Specs.**
|
|
386
|
+
- `sitemap.xml`: indexable pages only, absolute URLs from the production origin,
|
|
387
|
+
`<lastmod>` from the content's date or git. Excludes the 404, redirects, the
|
|
388
|
+
audit page, and anything flagged non-collectable.
|
|
389
|
+
- `robots.txt`: keyed on `isProduction` — in production, `Allow: /` +
|
|
390
|
+
`Disallow:` the audit page + `Sitemap:` line; in every non-production build,
|
|
391
|
+
`Disallow: /`.
|
|
392
|
+
- `feed.xml` (article sites only): newest ~20 dated entries, absolute URLs and
|
|
393
|
+
IDs from the production origin, `updated` from the most recent entry; a
|
|
394
|
+
`<link rel="alternate" type="application/atom+xml">` in the head. Tool sites
|
|
395
|
+
with no dated content skip this.
|
|
396
|
+
|
|
397
|
+
**Verify.** The output contains generated `sitemap.xml` and `robots.txt`; both
|
|
398
|
+
parse; `robots.txt` content matches the build target.
|
|
399
|
+
|
|
400
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
401
|
+
|
|
402
|
+
## noindex — Non-production builds are un-indexable
|
|
403
|
+
|
|
404
|
+
**Contract.** Every build that is not the production artifact is un-indexable,
|
|
405
|
+
by three layers in order of reliance.
|
|
406
|
+
|
|
407
|
+
**Specs.**
|
|
408
|
+
1. **Build-time (primary):** when `isProduction` is false, the layout emits a
|
|
409
|
+
site-wide `<meta name="robots" content="noindex, nofollow">` and `robots.txt`
|
|
410
|
+
is `Disallow: /`. This travels with the artifact regardless of how it is
|
|
411
|
+
served. The same flag gates display ads (`#ads`) — they never load off a
|
|
412
|
+
non-production build.
|
|
413
|
+
2. **Header backstop:** the host's response headers add `X-Robots-Tag: noindex`
|
|
414
|
+
for preview/branch contexts where the platform supports it. Treated as a
|
|
415
|
+
bonus — not relied on.
|
|
416
|
+
3. **Canonical:** every page's canonical points at the production origin, so an
|
|
417
|
+
indexed stray still redirects search engines to production.
|
|
418
|
+
|
|
419
|
+
**Verify.** Build with the target unset → output carries `noindex` meta and
|
|
420
|
+
`Disallow: /`. Build for production → neither.
|
|
421
|
+
|
|
422
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
# Part F — Brand & audit
|
|
427
|
+
|
|
428
|
+
## brand-source — Brand as single source of visual truth
|
|
429
|
+
|
|
430
|
+
**Contract.** One file holds every brand primitive. The live CSS, the generated
|
|
431
|
+
OG cards, and the generated favicons all consume it. A brand change flows to all
|
|
432
|
+
three on the next build with no separate step.
|
|
433
|
+
|
|
434
|
+
**Specs.**
|
|
435
|
+
- One file (`brand.*` / a theme block / a tokens module) holds: brand
|
|
436
|
+
colour(s), accent, background, text colour, font family + file paths, and the
|
|
437
|
+
logo mark's path data.
|
|
438
|
+
- The stylesheet consumes those tokens (via generated custom properties or the
|
|
439
|
+
framework's theme mechanism) and **never hardcodes a brand value**.
|
|
440
|
+
- The OG-card renderer and the icon generator read the same file directly.
|
|
441
|
+
- Hardcoding a brand colour/font/logo anywhere else — the stylesheet, `<head>`,
|
|
442
|
+
a card template, an icon file — is a **defect**.
|
|
443
|
+
|
|
444
|
+
**Verify.** Grep for hex colours / font-family strings outside the brand file.
|
|
445
|
+
The audit page renders swatches and the type scale from the brand file.
|
|
446
|
+
|
|
447
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
448
|
+
|
|
449
|
+
## audit-page — The audit page
|
|
450
|
+
|
|
451
|
+
**Contract.** One non-crawlable page renders everything drift-prone in one
|
|
452
|
+
place, with a summary that surfaces failures without clicking into every tab.
|
|
453
|
+
|
|
454
|
+
**Specs.**
|
|
455
|
+
- One route/page, excluded from collections, sitemap, and nav; `<meta
|
|
456
|
+
name="robots" content="noindex, nofollow">` unconditionally; an all-contexts
|
|
457
|
+
`X-Robots-Tag: noindex` header for its path; a `Disallow:` line in production
|
|
458
|
+
`robots.txt`.
|
|
459
|
+
- **Contains nothing that would be harmful if public.** It is served as-is (or
|
|
460
|
+
rendered client-side from baked data), so treat it as readable by anyone who
|
|
461
|
+
finds the URL. Secret **presence** may appear as a boolean
|
|
462
|
+
(`TOKEN: present`); a secret **value** never appears. No client-side password
|
|
463
|
+
— for real locking use the host's auth, and still put nothing sensitive on it.
|
|
464
|
+
- **Tabs**, not one long scroll, with: a persistent summary bar listing every
|
|
465
|
+
failed/warned check and its tab; a status dot per tab; a "show all" toggle for
|
|
466
|
+
find-in-page and printing.
|
|
467
|
+
- Tabs cover: brand tokens + contrast flags + type scale; generated assets (OG
|
|
468
|
+
cards at true ratio with dimensions/size, favicons); link + privacy integrity
|
|
469
|
+
results; infrastructure config (repo introspection + optional live
|
|
470
|
+
host/CI-API checks + a manual checklist with committed `verifiedOn` dates);
|
|
471
|
+
build facts (target, commit, generator timestamps); **Compliance** (the
|
|
472
|
+
per-chapter `PASS` / `FAIL` / `MANUAL` grid from `#compliance`, plus the
|
|
473
|
+
`standard-version` drift row). The summary bar counts non-`PASS` chapters
|
|
474
|
+
alongside failed checks.
|
|
475
|
+
- The infrastructure checks run at build time following `#third-parties` rules
|
|
476
|
+
(cache the result, never fail the build if an API is down, stamp "as of …").
|
|
477
|
+
A subset runs as a deploy pre-flight that **hard-fails** on a cost- or
|
|
478
|
+
SEO-breaking mismatch and warns on the rest.
|
|
479
|
+
|
|
480
|
+
**Rationale.** Out-of-repo config (a host build toggle, a DNS record, a search
|
|
481
|
+
console verification) is the most drift-prone part of any project and nothing in
|
|
482
|
+
the repo can enforce it. One surface makes drift visible.
|
|
483
|
+
|
|
484
|
+
**Verify.** Open the page after a build; the summary bar is clean.
|
|
485
|
+
|
|
486
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
# Part G — Security, caching, privacy
|
|
491
|
+
|
|
492
|
+
## security-headers — Security response headers
|
|
493
|
+
|
|
494
|
+
**Contract.** Every response carries the baseline security header set, including
|
|
495
|
+
a strict Content-Security-Policy. The CSP widens only to match the declared
|
|
496
|
+
origin manifest, in one place, and is verified against that list.
|
|
497
|
+
|
|
498
|
+
**Specs.**
|
|
499
|
+
- Baseline headers on every response:
|
|
500
|
+
- `X-Content-Type-Options: nosniff`
|
|
501
|
+
- Framing denied: `X-Frame-Options: DENY` **or** `SAMEORIGIN` together with
|
|
502
|
+
`frame-ancestors 'none'` in the CSP
|
|
503
|
+
- `Referrer-Policy: strict-origin-when-cross-origin`
|
|
504
|
+
- `Permissions-Policy: geolocation=(), camera=(), microphone=()`
|
|
505
|
+
- Baseline CSP (a fully self-contained site):
|
|
506
|
+
```
|
|
507
|
+
default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'
|
|
508
|
+
```
|
|
509
|
+
- When `#third-parties` adds an origin, the matching directive (`connect-src` /
|
|
510
|
+
`script-src` / `img-src` / `frame-src`) widens to **exactly** those origins,
|
|
511
|
+
in the one header definition, nowhere else.
|
|
512
|
+
- Display ads (`#ads`) are the one place the CSP widens substantially. That
|
|
513
|
+
relaxation is scoped to the ad directives, documented with a comment, and any
|
|
514
|
+
`'unsafe-inline'` it forces stays out of `default-src`.
|
|
515
|
+
- HTTPS is enforced (HSTS via the platform or `force_ssl`). A platform-managed
|
|
516
|
+
HSTS header is not restated in the config.
|
|
517
|
+
|
|
518
|
+
**Verify.** The link/reference check fails on any origin the CSP grants that is
|
|
519
|
+
not in the origin manifest. (Not the reverse: a host or form processor is in
|
|
520
|
+
the manifest — it sees visitor data — but is not a browser-resource origin, so
|
|
521
|
+
it needn't appear in the CSP.) The audit page shows the **live** header set on
|
|
522
|
+
`/` — i.e. what the host actually sends, so a loosened dashboard setting shows
|
|
523
|
+
up there.
|
|
524
|
+
|
|
525
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
526
|
+
|
|
527
|
+
## caching — Cache headers & fingerprinting
|
|
528
|
+
|
|
529
|
+
**Contract.** Cache lifetimes match mutability. `immutable` is only ever set on
|
|
530
|
+
content-fingerprinted filenames. A changed URL ships a redirect from the old
|
|
531
|
+
one.
|
|
532
|
+
|
|
533
|
+
**Specs.**
|
|
534
|
+
- HTML: `Cache-Control: public, max-age=0, must-revalidate` — never `immutable`.
|
|
535
|
+
- Fingerprinted assets (content hash in the filename): `public, max-age=31536000,
|
|
536
|
+
immutable`.
|
|
537
|
+
- Non-fingerprinted assets: a moderate `max-age` with `must-revalidate`.
|
|
538
|
+
- A project either fingerprints its app CSS/JS (then caches it `immutable`) or
|
|
539
|
+
serves it un-fingerprinted with revalidation — pick one and apply it
|
|
540
|
+
consistently.
|
|
541
|
+
- When a page's permalink changes, a `301` from the old path ships in the **same
|
|
542
|
+
change** as the rename.
|
|
543
|
+
|
|
544
|
+
**Verify.** Review the header rules. The audit page / a link check flags a
|
|
545
|
+
renamed permalink with no redirect.
|
|
546
|
+
|
|
547
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
548
|
+
|
|
549
|
+
## third-parties — External resources & the origin manifest
|
|
550
|
+
|
|
551
|
+
**Contract.** Self-contained is the default. A genuine feature need may add an
|
|
552
|
+
external resource, but every external touch is deliberate, declared, isolated,
|
|
553
|
+
and degrades gracefully.
|
|
554
|
+
|
|
555
|
+
**Specs.**
|
|
556
|
+
- **The decision test, in order:**
|
|
557
|
+
1. Can the data be fetched at build time and baked in? If it changes slower
|
|
558
|
+
than you deploy — do that. No runtime origin, no CSP change, no key
|
|
559
|
+
exposure. Cache the response; commit a fallback fixture.
|
|
560
|
+
2. Does it need a secret (API key)? Then it **cannot** be called from the
|
|
561
|
+
browser — call it server-side (a server route, or a thin serverless proxy
|
|
562
|
+
that holds the key and forwards only the needed fields).
|
|
563
|
+
3. Is the feature still usable if the call fails or is blocked? It must be —
|
|
564
|
+
design the fallback state first (`AbortController` timeout, clear fallback,
|
|
565
|
+
never a permanent spinner; absent-not-broken with JS off; never blocks
|
|
566
|
+
first paint).
|
|
567
|
+
4. Only then add the origin to the manifest and widen the CSP.
|
|
568
|
+
- **One canonical origin manifest** lists every external origin the site touches
|
|
569
|
+
— host, form handler, anything the steps above added — each as
|
|
570
|
+
`{ origin, purpose, dataSeen, loadedWhen }`.
|
|
571
|
+
- The gate fails the build on any external origin in the output that is not in
|
|
572
|
+
the manifest, and on any mismatch between the manifest and the CSP.
|
|
573
|
+
- A third-party `<script>` (last resort, no self-hostable equivalent) needs a
|
|
574
|
+
Subresource Integrity hash + `crossorigin`, is loaded lazily, and is never
|
|
575
|
+
render-blocking. It forfeits the strict-CSP / short-privacy benefits for that
|
|
576
|
+
page — note it in the commit message (and the PR, if any).
|
|
577
|
+
- A **display-ad provider** is a declared third party. Its origins go in the
|
|
578
|
+
manifest; its full set of consequences (consent, CSP, CLS, non-prod gating,
|
|
579
|
+
`ads.txt`) is collected in `#ads`.
|
|
580
|
+
- A **model / LLM provider** the project calls is a declared third party — its
|
|
581
|
+
origin in the manifest, its data-retention and training posture stated in
|
|
582
|
+
`dataSeen`. Because the call carries an API key it is server-side only (step 2
|
|
583
|
+
above); the full set of consequences is collected in `#llm-integration`.
|
|
584
|
+
|
|
585
|
+
**Verify.** The link/reference check enforces the manifest ↔ output ↔ CSP
|
|
586
|
+
agreement.
|
|
587
|
+
|
|
588
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
589
|
+
|
|
590
|
+
## privacy — Privacy posture
|
|
591
|
+
|
|
592
|
+
**Contract.** The default is no tracking of any kind. Analytics, if the project
|
|
593
|
+
needs it, is either privacy-first or consent-gated. The privacy page is
|
|
594
|
+
generated from the origin manifest, never hand-written.
|
|
595
|
+
|
|
596
|
+
**Specs.**
|
|
597
|
+
- **Default posture:** no analytics, no cookies beyond a functional session, no
|
|
598
|
+
fingerprinting, no consent banner needed.
|
|
599
|
+
- If analytics is required, one of:
|
|
600
|
+
- **privacy-first** — self-hosted (Plausible, GoatCounter) or server-log-based,
|
|
601
|
+
no cookie, no cross-site identifier; or
|
|
602
|
+
- **consent-gated** — a third-party analytics/tag manager (including GA/GTM) is
|
|
603
|
+
permitted **only** when it does not load or set anything until the visitor
|
|
604
|
+
opts in through a CMP, and a decline is honoured for the whole session.
|
|
605
|
+
A third-party analytics script that loads unconditionally is **non-compliant**.
|
|
606
|
+
- **Display advertising (`#ads`)** always requires a provider-certified CMP and
|
|
607
|
+
always sets cookies — it is never the privacy-first path. A site that runs ads
|
|
608
|
+
has a consent banner by necessity.
|
|
609
|
+
- The privacy page renders from the origin manifest (`#third-parties`) — the
|
|
610
|
+
list of who sees visitor data and why. No hand-written prose enumerating
|
|
611
|
+
services that goes stale when a dependency changes.
|
|
612
|
+
- Policy pages are a collection; `Last updated` comes from the git commit date,
|
|
613
|
+
not a hand-typed line.
|
|
614
|
+
- Adding any origin that sees visitor data (an IP in a log counts) is a
|
|
615
|
+
privacy-page change in the same commit.
|
|
616
|
+
- Native form submissions: state what the handler receives, retention, and link
|
|
617
|
+
its data-processing terms.
|
|
618
|
+
- If the project sends end-user content to a model provider (`#llm-integration`),
|
|
619
|
+
the privacy page names the provider and what leaves your origin, and — where
|
|
620
|
+
the provider offers it — the no-training / zero-retention option is the one in
|
|
621
|
+
use.
|
|
622
|
+
|
|
623
|
+
**Verify.** The privacy page's third-party list is generated. Review confirms
|
|
624
|
+
any analytics is privacy-first or gated behind a CMP.
|
|
625
|
+
|
|
626
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
627
|
+
|
|
628
|
+
## ads — Display advertising
|
|
629
|
+
|
|
630
|
+
**Contract.** Display ads are opt-in per project. When present, the ad provider
|
|
631
|
+
is a declared third party (`#third-parties`), loads only after consent through a
|
|
632
|
+
provider-certified CMP (`#privacy`), sits in layout slots with reserved
|
|
633
|
+
dimensions (no layout shift), never loads on a non-production build, and its
|
|
634
|
+
required root files are generated or managed rather than left to drift. The CSP
|
|
635
|
+
relaxation the provider needs is the widest the policy ever gets, scoped to the
|
|
636
|
+
ad directives, and documented.
|
|
637
|
+
|
|
638
|
+
**Specs.**
|
|
639
|
+
- **Opt-in.** An `ads:` key in the `CLAUDE.md` House-style section — `none`
|
|
640
|
+
(default) or the provider (e.g. `adsense`). No ad code exists unless recorded.
|
|
641
|
+
- **Consent gate (`#privacy`).** A CMP the ad provider **certifies** is in place
|
|
642
|
+
(for AdSense: a Google-certified CMP — legally required for EEA / UK /
|
|
643
|
+
Switzerland traffic). It presents a consent choice *before* the ad loader
|
|
644
|
+
sets any cookie or identifier, and a decline is honoured (no ads, or
|
|
645
|
+
non-personalized only where the provider supports it). Whether the prompt
|
|
646
|
+
shows to every visitor or only to visitors in regulated regions is a project
|
|
647
|
+
decision recorded in `CLAUDE.md`. What is **non-compliant** regardless: the ad
|
|
648
|
+
loader firing and setting cookies before the CMP has resolved.
|
|
649
|
+
- **Declared (`#third-parties`).** Every ad-provider origin (loader, ad-serving,
|
|
650
|
+
ad iframes, the CMP, quality/verification endpoints) is in the origin manifest
|
|
651
|
+
with `purpose: advertising` and its `dataSeen`. The privacy page renders it
|
|
652
|
+
like any other entry.
|
|
653
|
+
- **CSP (`#security-headers`).** The provider's origins are added to
|
|
654
|
+
`script-src`, `frame-src`, `img-src`, and `connect-src` (often `style-src`
|
|
655
|
+
too — ad code injects styles) in the one header definition, with a comment
|
|
656
|
+
saying which directives and why. If the provider genuinely needs
|
|
657
|
+
`'unsafe-inline'` in a script/style directive, it is confined to that
|
|
658
|
+
directive, recorded as a named exception in the manifest, and shown on the
|
|
659
|
+
audit page. **Never `'unsafe-eval'`, and never a widened `default-src`.**
|
|
660
|
+
- **No layout shift.** Every ad slot is a container with an explicit reserved
|
|
661
|
+
size (a `min-height` or `aspect-ratio` matching the unit). An unfilled slot
|
|
662
|
+
holds its space or is removed cleanly — never left to reflow. This is what
|
|
663
|
+
keeps `#observability`'s CLS green.
|
|
664
|
+
- **Never on non-production (`#noindex`).** The ad loader and every slot are
|
|
665
|
+
gated on the same `isProduction` flag as `noindex`. Serving ads on a preview /
|
|
666
|
+
staging URL wastes impressions, risks an invalid-traffic policy strike, and
|
|
667
|
+
pollutes analytics.
|
|
668
|
+
- **Root files.** `ads.txt` (and `app-ads.txt` if the account has apps) at the
|
|
669
|
+
site root, listing authorized sellers, is generated from data or is a reviewed
|
|
670
|
+
file — not hand-edited over time. Excluded from the sitemap. Treated like
|
|
671
|
+
`robots.txt`.
|
|
672
|
+
- **Performance & rendering (`#rendering`).** The loader is `async`, runs after
|
|
673
|
+
first paint, is never render-blocking, and never gates core content. Prefer
|
|
674
|
+
explicitly placed units over auto-ads / page-level scripts that rewrite the
|
|
675
|
+
layout.
|
|
676
|
+
- **Editorial** — ad density, placement, and which pages carry ads follow the
|
|
677
|
+
provider's policies (no ads on error / thank-you / thin pages; a label where a
|
|
678
|
+
placement could be mistaken for content). Out of code scope, recorded in
|
|
679
|
+
`CLAUDE.md`.
|
|
680
|
+
|
|
681
|
+
**Rationale.** Ads are the single largest deviation from this standard's
|
|
682
|
+
defaults — cookies, a CMP, a permissive CSP, cross-origin iframes, and a Core
|
|
683
|
+
Web Vitals cost. Isolating all of it in one contract keeps the rest of the
|
|
684
|
+
standard honest and makes the tradeoff explicit and reviewable.
|
|
685
|
+
|
|
686
|
+
**Verify.** The audit page has an **Ads** panel (only when `ads:` ≠ `none`):
|
|
687
|
+
`ads.txt` present and parseable; the CMP script present; the CSP grants exactly
|
|
688
|
+
the declared ad origins; any `'unsafe-*'` exception noted; the loader gated on
|
|
689
|
+
`isProduction`. The link/reference check (`#internal-links`) fails on any
|
|
690
|
+
`adsbygoogle` / ad-provider reference in a non-production build and on a missing
|
|
691
|
+
`ads.txt` in the production output. Manually, in a fresh browser on production:
|
|
692
|
+
decline consent → no ad requests in the network panel; accept → ads load; check
|
|
693
|
+
CLS in the browser's own tools.
|
|
694
|
+
|
|
695
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
# Part H — Links, forms, tests, accessibility
|
|
700
|
+
|
|
701
|
+
## internal-links — Internal linking
|
|
702
|
+
|
|
703
|
+
**Contract.** Internal linking is data-driven and validated at build time. A
|
|
704
|
+
dangling internal reference fails the build.
|
|
705
|
+
|
|
706
|
+
**Specs.**
|
|
707
|
+
- Every content page links **2–4 sibling/related pages**; a page with none is a
|
|
708
|
+
dead end.
|
|
709
|
+
- Navigation is generated from data, never hand-listed per layout.
|
|
710
|
+
- Cross-links use root-relative paths, never the absolute production URL.
|
|
711
|
+
- A build-time check walks the built output and **exits non-zero** on:
|
|
712
|
+
- any internal `href` that doesn't resolve to a generated page
|
|
713
|
+
- any related/glossary slug with no matching content
|
|
714
|
+
- any referenced asset (`og:image`, preload, `<img src>`, icon) missing from
|
|
715
|
+
the output
|
|
716
|
+
- any external `<a>` missing `rel="noopener"` (or `noreferrer`)
|
|
717
|
+
- any external origin (`href`/`src`/`fetch`/`<script>`) not in the origin
|
|
718
|
+
manifest
|
|
719
|
+
- any origin the CSP grants that is not in the origin manifest
|
|
720
|
+
- any inline `<script>` (without `src`, non-JSON type), inline `<style>`,
|
|
721
|
+
`on*=` handler, or `style=` attribute in the output — the strict CSP
|
|
722
|
+
(`#security-headers`, `#styling`) rejects all of these, so a build check is
|
|
723
|
+
the guard against one creeping back in
|
|
724
|
+
- any JSON-LD block that doesn't parse or lacks its `@type`'s required fields
|
|
725
|
+
|
|
726
|
+
**Verify.** The check is part of the gate.
|
|
727
|
+
|
|
728
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
729
|
+
|
|
730
|
+
## forms — Forms
|
|
731
|
+
|
|
732
|
+
**Contract.** Prefer the host platform's native form handling. Every form has
|
|
733
|
+
spam mitigation and meets the client UX/accessibility contract. The handler is
|
|
734
|
+
defined once.
|
|
735
|
+
|
|
736
|
+
**Specs.**
|
|
737
|
+
- Use the host's native form handling before reaching for a third-party form
|
|
738
|
+
processor — a processor is a new external origin (`#third-parties`) and a
|
|
739
|
+
privacy disclosure (`#privacy`).
|
|
740
|
+
- Spam mitigation on every form: an off-screen honeypot field at minimum; add a
|
|
741
|
+
rate limit / captcha for higher-value targets.
|
|
742
|
+
- Client UX/a11y contract: a validity gate before submit; an `aria-live` status
|
|
743
|
+
region; the submit button disabled with a busy state while in flight; a real
|
|
744
|
+
error branch, not only a success path; on success, clear feedback.
|
|
745
|
+
- The submit handler is defined once (a shared script/module), never duplicated
|
|
746
|
+
per page.
|
|
747
|
+
- Every control has a real associated `<label>`; errors set `aria-invalid` and
|
|
748
|
+
are announced via `aria-live` referenced by `aria-describedby` (`#a11y`).
|
|
749
|
+
|
|
750
|
+
**Verify.** Submit a test message end to end after deploy. Review the handler is
|
|
751
|
+
not duplicated.
|
|
752
|
+
|
|
753
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
754
|
+
|
|
755
|
+
## domain-tests — Testing domain logic
|
|
756
|
+
|
|
757
|
+
**Contract.** Anything that computes a result a user relies on is a pure
|
|
758
|
+
function with its own test. The tests encode the rules the page's own copy
|
|
759
|
+
states.
|
|
760
|
+
|
|
761
|
+
**Specs.**
|
|
762
|
+
- The computation is a pure function in its own module (`#client-logic`), tested
|
|
763
|
+
with the stack's built-in test runner — no test framework needed for this.
|
|
764
|
+
- **Tests include:**
|
|
765
|
+
- the worked example from the page's own explanatory copy (they must agree)
|
|
766
|
+
- boundaries: zero, one, 100%, empty/NaN input, negative input, very large
|
|
767
|
+
values, floating-point rounding (`0.1 + 0.2`)
|
|
768
|
+
- every rule the copy states ("rounds up to the next whole cent", "caps at 40
|
|
769
|
+
hours")
|
|
770
|
+
- For code that calls an external API: the **transform** of the response is a
|
|
771
|
+
pure tested function with a captured sample response as the fixture; the fetch
|
|
772
|
+
itself is not unit-tested, but the failure path (timeout / non-200 → fallback
|
|
773
|
+
state) is; a serverless proxy/endpoint gets its own spec (forwards only the
|
|
774
|
+
expected fields, returns the fallback shape on upstream failure, never leaks
|
|
775
|
+
the key).
|
|
776
|
+
- A calc/transform module changed without a matching test change is a review red
|
|
777
|
+
flag.
|
|
778
|
+
|
|
779
|
+
**Verify.** The tests are part of the gate.
|
|
780
|
+
|
|
781
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
782
|
+
|
|
783
|
+
## a11y — Accessibility checklist
|
|
784
|
+
|
|
785
|
+
**Contract.** Every page meets the checklist below. An automated a11y scan over
|
|
786
|
+
every page runs in the gate — and is understood to catch regressions, not
|
|
787
|
+
everything.
|
|
788
|
+
|
|
789
|
+
**Specs.**
|
|
790
|
+
- **Skip link** as the first focusable element, targeting the main landmark,
|
|
791
|
+
visible on focus.
|
|
792
|
+
- **One `<h1>` per page**; headings in order, no level skipped.
|
|
793
|
+
- **Modals are `<dialog>` + `showModal()`** — native focus trap and inert
|
|
794
|
+
backdrop. Move focus to the first field on open, restore to the trigger on
|
|
795
|
+
close; `Escape` closes natively; the close control has an `aria-label`. Don't
|
|
796
|
+
hand-roll any of it.
|
|
797
|
+
- **`prefers-reduced-motion`:** every transition/animation (reveals, fades,
|
|
798
|
+
`scroll-behavior`) lives inside `@media (prefers-reduced-motion:
|
|
799
|
+
no-preference)`.
|
|
800
|
+
- **`<meta name="theme-color">`** with light/dark `media` variants and
|
|
801
|
+
**`color-scheme: light dark`** on the root, values from `#brand-source`.
|
|
802
|
+
- **Forms:** real `<label for>` on every control; errors set `aria-invalid` and
|
|
803
|
+
are announced via an `aria-live` region referenced by `aria-describedby`;
|
|
804
|
+
never signal state by colour alone.
|
|
805
|
+
- **`<details>`/`<summary>`** disclosure widgets: `<summary>` is already a
|
|
806
|
+
keyboard button — don't re-bind Enter/Space or add `role`/`tabindex`.
|
|
807
|
+
- **Icons:** decorative → `aria-hidden="true"`; meaningful → `<title>` in the
|
|
808
|
+
SVG or `aria-label` on the control.
|
|
809
|
+
- **Contrast** meets WCAG AA; fix failures at the token (`#brand-source`), not
|
|
810
|
+
with a one-off override.
|
|
811
|
+
- **Focus visibility:** a visible focus ring on every interactive element; if
|
|
812
|
+
you override the default, keep one.
|
|
813
|
+
- Before a release that touches interaction, drive the changed flow
|
|
814
|
+
**keyboard-only and with a screen reader** once.
|
|
815
|
+
|
|
816
|
+
**Verify.** The automated scan is in the gate. The audit page flags sub-AA
|
|
817
|
+
contrast. The manual keyboard/SR pass is on the change checklist for interaction
|
|
818
|
+
changes.
|
|
819
|
+
|
|
820
|
+
**Implementation.** See your stack's impl doc, same slug — the impl doc carries
|
|
821
|
+
the scanner mechanism only.
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
# Part I — Gate, CI/CD, secrets, observability
|
|
826
|
+
|
|
827
|
+
## the-gate — The one check gate
|
|
828
|
+
|
|
829
|
+
**Contract.** There is exactly one check command that runs everything. Every
|
|
830
|
+
enforcement point runs that same command — never a hand-picked subset.
|
|
831
|
+
|
|
832
|
+
**Specs.**
|
|
833
|
+
- One gate command runs, ordered cheapest-first: lint/format check → content
|
|
834
|
+
validation → domain tests → build → link/reference integrity → a11y scan.
|
|
835
|
+
(Optionally a performance budget once the site has a baseline.)
|
|
836
|
+
- The same command is invoked by: the pre-push git hook, the CI workflow, the
|
|
837
|
+
deploy script, and the assistant before it reports work complete.
|
|
838
|
+
- There is **no "CI-only" variant** and no partial local run. If the gate
|
|
839
|
+
passes, the change is shippable.
|
|
840
|
+
- The deploy path re-runs the gate and has **no bypass flag**.
|
|
841
|
+
- Checks that need credentials or inspect production-only state (e.g. the infra
|
|
842
|
+
doctor) are **not** in the gate — they run at build time for the audit page
|
|
843
|
+
and as a deploy pre-flight.
|
|
844
|
+
- **Completeness and standard-version drift are checked separately** by
|
|
845
|
+
`#compliance`, not here. The gate catches *regressions* of contracts already
|
|
846
|
+
implemented; a chapter that was never implemented, or one the standard added
|
|
847
|
+
after you adopted it, is a migration gap — not a gate failure.
|
|
848
|
+
|
|
849
|
+
**Verify.** Grep every enforcement point — each calls the one gate command.
|
|
850
|
+
|
|
851
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
852
|
+
|
|
853
|
+
## compliance — Codebase-vs-standard conformance
|
|
854
|
+
|
|
855
|
+
**Contract.** One command reports the codebase's state against **every chapter**
|
|
856
|
+
of the standard — `PASS` / `FAIL` / `MANUAL` / `N/A` per chapter. It is the
|
|
857
|
+
drift-and-completeness sweep that `#the-gate` (regressions only) does not do,
|
|
858
|
+
and it is **not** part of the gate.
|
|
859
|
+
|
|
860
|
+
**Specs.**
|
|
861
|
+
- One command walks every CORE chapter and every mirrored impl section and emits
|
|
862
|
+
one result each:
|
|
863
|
+
- **`PASS`** — mechanically verified from the repo + the built output.
|
|
864
|
+
- **`FAIL: <finding>`** — a Contract is violated, or the chapter is
|
|
865
|
+
unimplemented. The specific finding is printed.
|
|
866
|
+
- **`MANUAL: <what to check>`** — a judgment the script can't make (e.g. "is
|
|
867
|
+
the page prose really in the content data, or still in a template?").
|
|
868
|
+
- **`N/A`** — the chapter doesn't apply given the `CLAUDE.md` keys (`#ads` when
|
|
869
|
+
`ads: none`; the Part J chapters when `ai: none`, and `#agent-artifacts` /
|
|
870
|
+
`#usage-metering` unless the `ai:` value opts into them; the `article` half
|
|
871
|
+
of `#seo-meta` on a `content-type: tool` site).
|
|
872
|
+
- It ends with a summary line: `N pass · M fail · K manual`, then the list.
|
|
873
|
+
- **Not in `#the-gate`.** A `FAIL` is a known migration gap, not a regression —
|
|
874
|
+
gating on it would freeze all work until the whole migration is done, which
|
|
875
|
+
contradicts incremental adoption.
|
|
876
|
+
- It runs: **on demand**; **at build time** (cache-aware) feeding the audit
|
|
877
|
+
page's Compliance panel (`#audit-page`); and the **deploy pre-flight**
|
|
878
|
+
(`#ci-cd`) **warns** on any chapter that went `PASS → FAIL` since the last
|
|
879
|
+
deploy (a real regression of something previously compliant).
|
|
880
|
+
- **Standard-version drift is a row.** The command compares the project's pinned
|
|
881
|
+
`standard-version:` (`#adopting`) to the standard's own version header; if the
|
|
882
|
+
pin is behind, each `CHANGELOG` slug changed since becomes a
|
|
883
|
+
`MANUAL: re-check #<slug>` line.
|
|
884
|
+
- The mechanical tier **reuses existing machinery** — the repo-config
|
|
885
|
+
introspection already in the audit doctor and the built-output scan already in
|
|
886
|
+
the link/reference check — not a parallel implementation.
|
|
887
|
+
- **When the assistant runs it:** at onboarding (the `FAIL`s *are* the migration
|
|
888
|
+
backlog); whenever `standard-version:` is behind; before a release that spans
|
|
889
|
+
several chapters; and whenever asked "are we still in line?".
|
|
890
|
+
|
|
891
|
+
**Rationale.** The gate catches regressions against contracts you have already
|
|
892
|
+
implemented. It cannot tell you a whole chapter was never done, that the
|
|
893
|
+
standard gained a chapter since you adopted it, or that a `MANUAL`-only contract
|
|
894
|
+
has quietly drifted. This is the systematic "are we still in line" check — the
|
|
895
|
+
counterpart to `#the-gate`.
|
|
896
|
+
|
|
897
|
+
**Verify.** The audit page's Compliance panel shows the per-chapter grid and the
|
|
898
|
+
summary bar counts non-`PASS` chapters. Rolling the `standard-version` pin back
|
|
899
|
+
by hand lists the intervening `CHANGELOG` slugs as `MANUAL`.
|
|
900
|
+
|
|
901
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
902
|
+
|
|
903
|
+
## ci-cd — CI/CD & deploy philosophy
|
|
904
|
+
|
|
905
|
+
**Contract.** Merging and deploying are decoupled. Production ships through one
|
|
906
|
+
sanctioned, no-bypass command on a deliberate cadence. The default working mode
|
|
907
|
+
is **solo** — one developer, no pull-request ceremony — and the model holds with
|
|
908
|
+
no PRs at all.
|
|
909
|
+
|
|
910
|
+
**Specs.**
|
|
911
|
+
- A push or merge to the default branch does **not** deploy. Merged work
|
|
912
|
+
accumulates on `main`.
|
|
913
|
+
- **Committed git hooks**, two tiers, activated by a setup step (not a manual
|
|
914
|
+
install): a fast pre-commit (lint/format) and a full pre-push (the whole
|
|
915
|
+
gate). On a solo project these hooks — not code review — are the enforcement
|
|
916
|
+
point: a failing gate physically blocks the push.
|
|
917
|
+
- **Branching & review — solo is the default.** Work on a short-lived branch for
|
|
918
|
+
anything non-trivial, let the pre-push hook run the gate, fast-forward `main`,
|
|
919
|
+
push. Open a **pull request** only for a concrete reason — you want the CI
|
|
920
|
+
backstop to vet a branch before it reaches `main`, or the rendered diff, or a
|
|
921
|
+
paper trail — never as self-approval ceremony. A project with more than one
|
|
922
|
+
committer inverts this: PRs become the norm and carry a second-person review.
|
|
923
|
+
- **Push `main` after each merge** — not for review, for the offsite copy and so
|
|
924
|
+
the CI backstop actually runs. Never pushing at all is a worse failure than
|
|
925
|
+
any branching-model choice.
|
|
926
|
+
- A **CI backstop** runs the same gate on every push (and any PR), on a clean
|
|
927
|
+
machine independent of your laptop — its job is catching an environment
|
|
928
|
+
difference (fresh install, pinned runtime, no local caches), not gating a
|
|
929
|
+
merge you already gated locally. Where it cannot be a required status check it
|
|
930
|
+
is advisory; the pre-push hook and the deploy command are the real
|
|
931
|
+
enforcement.
|
|
932
|
+
- **Production deploy** is one sanctioned command that: checks it is on `main`,
|
|
933
|
+
re-runs the gate, builds a production artifact with **your** deploy-target
|
|
934
|
+
switch set, runs a pre-flight that **hard-fails** on a cost- or SEO-breaking
|
|
935
|
+
mismatch and **warns** on the rest — including any `#compliance` chapter that
|
|
936
|
+
went `PASS → FAIL` since the last deploy — then ships. **No `--force`, no
|
|
937
|
+
`--no-verify`, no env override.**
|
|
938
|
+
- Deploys are deliberate — one deploy batches everything since the last deploy.
|
|
939
|
+
You own the cadence.
|
|
940
|
+
- Preview builds are non-production and un-indexable (`#noindex`).
|
|
941
|
+
- The deploy-target switch is set by the deploy command, never inferred from a
|
|
942
|
+
host-injected variable.
|
|
943
|
+
|
|
944
|
+
**Rationale.** The specific mechanism varies — a static host that bills per build
|
|
945
|
+
wants a pre-built artifact upload with the host's own builds turned off; a
|
|
946
|
+
platform that deploys by `git push` to a release remote is equally fine as long
|
|
947
|
+
as the gate runs first and there is no bypass. The "a push must never trigger a
|
|
948
|
+
build" wording some hosts force is a cost artifact, not the principle. The
|
|
949
|
+
constant is: deploys are gated, sanctioned, and on your schedule.
|
|
950
|
+
|
|
951
|
+
For a solo developer the whole model holds with **no pull requests**: the
|
|
952
|
+
pre-push hook is the gate, CI is a clean-room second opinion, `bin/deploy` is the
|
|
953
|
+
release. A PR is a tool to reach for when it buys something specific, not a
|
|
954
|
+
required step. Multi-committer projects add the PR-and-review layer on top —
|
|
955
|
+
nothing below it changes.
|
|
956
|
+
|
|
957
|
+
**Verify.** A push doesn't deploy. The pre-push hook is *installed* (not just
|
|
958
|
+
committed) after the setup step. The deploy command has no skip flag. The audit
|
|
959
|
+
page's infra tab confirms the host isn't auto-building where that costs.
|
|
960
|
+
|
|
961
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
962
|
+
|
|
963
|
+
## secrets — Secrets & environment
|
|
964
|
+
|
|
965
|
+
**Contract.** Secrets are never committed and never reach build output. Each
|
|
966
|
+
context gets the minimum it needs.
|
|
967
|
+
|
|
968
|
+
**Specs.**
|
|
969
|
+
- **Local:** a git-ignored env file, loaded by the shell or a loader. Commit an
|
|
970
|
+
`.env.example` with keys and dummy values. Never commit a real one.
|
|
971
|
+
- **CI:** repo secrets, scoped minimally. Deploy credentials are kept separate
|
|
972
|
+
from any read-only introspection tokens (e.g. the audit doctor's host/CI API
|
|
973
|
+
tokens).
|
|
974
|
+
- **Runtime:** anything a static page needs at runtime is baked in and therefore
|
|
975
|
+
public — no secret goes into a data file or template. Server stacks read
|
|
976
|
+
secrets from the runtime environment only.
|
|
977
|
+
- A **model-provider API key** (`#llm-integration`) is a runtime secret with
|
|
978
|
+
extra handling: a distinct key per environment, a low provider-side spend cap
|
|
979
|
+
on the non-prod keys, scoped to the models/endpoints used where the provider
|
|
980
|
+
supports it, and rotatable without a redeploy.
|
|
981
|
+
- A secret value never surfaces into build output or onto the audit page —
|
|
982
|
+
presence-as-boolean only.
|
|
983
|
+
- If a secret has been committed historically: purge it from git history and
|
|
984
|
+
rotate it.
|
|
985
|
+
|
|
986
|
+
**Verify.** The audit page shows secret **presence** only. A history scan for
|
|
987
|
+
committed secrets is clean.
|
|
988
|
+
|
|
989
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
990
|
+
|
|
991
|
+
## observability — Observability & post-launch operations
|
|
992
|
+
|
|
993
|
+
**Contract.** After launch, the standard operational SEO and performance loop
|
|
994
|
+
runs; production error/APM monitoring is in scope above the static tier.
|
|
995
|
+
|
|
996
|
+
**Specs.**
|
|
997
|
+
- One-time then occasional (not part of the build, but part of "done"):
|
|
998
|
+
- verify the production domain in Google Search Console and Bing Webmaster
|
|
999
|
+
Tools; submit `sitemap.xml` in each
|
|
1000
|
+
- watch Core Web Vitals in Search Console (real-user data, a ranking signal).
|
|
1001
|
+
If the site runs ads (`#ads`), CLS and LCP are the metrics they regress —
|
|
1002
|
+
the reserved ad-slot dimensions are what hold CLS
|
|
1003
|
+
- after a release that adds pages, check indexing coverage a week later
|
|
1004
|
+
- re-run the Rich Results Test after JSON-LD changes; re-check OG rendering
|
|
1005
|
+
after brand or share-image changes
|
|
1006
|
+
- re-run `#compliance` before a release that spans several chapters, and
|
|
1007
|
+
whenever the pinned `standard-version` falls behind — a stale
|
|
1008
|
+
`standard-version` is itself a `MANUAL` row it reports
|
|
1009
|
+
- A static site needs no APM. A server/DB application (`#beyond-this-standard`)
|
|
1010
|
+
runs an APM and a health check.
|
|
1011
|
+
- If the project calls a model API (`#llm-integration`), per-request token/cost
|
|
1012
|
+
telemetry and a spend-anomaly alert are part of this loop; if it generates
|
|
1013
|
+
artifacts (`#agent-artifacts`) or meters usage (`#usage-metering`), the
|
|
1014
|
+
per-artifact cost figure and the billing-reconciliation drift report join the
|
|
1015
|
+
audit page.
|
|
1016
|
+
|
|
1017
|
+
**Verify.** The audit page's manual checklist tracks the search-console /
|
|
1018
|
+
sitemap-submission items with `verifiedOn` dates.
|
|
1019
|
+
|
|
1020
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
1021
|
+
|
|
1022
|
+
---
|
|
1023
|
+
|
|
1024
|
+
# Part J — AI & agent capabilities (opt-in)
|
|
1025
|
+
|
|
1026
|
+
These three chapters apply **only** when a project has the specific need each
|
|
1027
|
+
names — the same need-gated opt-in as `#beyond-this-standard` and `#ads`, and
|
|
1028
|
+
**not** a consequence of which framework was chosen. A project that calls no
|
|
1029
|
+
model-provider API is unaffected: all three are `N/A` (`#compliance`), no
|
|
1030
|
+
`CLAUDE.md` key, no code. The triggers are independent — a project may need one
|
|
1031
|
+
without the others.
|
|
1032
|
+
|
|
1033
|
+
## llm-integration — LLM / agent API integration
|
|
1034
|
+
|
|
1035
|
+
**Contract.** In scope the moment the project calls a model provider's API at all
|
|
1036
|
+
— chat, completion, embeddings, agentic tool-use, transcription, first-party or
|
|
1037
|
+
through a gateway. Every such call is server-side, keyed per environment, bounded
|
|
1038
|
+
by an explicit timeout and retry policy, rate-limited on your own side, metered
|
|
1039
|
+
for token cost per request, wrapped in a moderation/safety layer sized to the
|
|
1040
|
+
subject's risk, disclosed in plain language to end users, and checked against the
|
|
1041
|
+
provider's usage policy for the project's subject matter.
|
|
1042
|
+
|
|
1043
|
+
**Specs.**
|
|
1044
|
+
- **Server-side only.** The provider key is a secret (`#secrets`) read from the
|
|
1045
|
+
runtime environment — never shipped to the browser, never in a data file or
|
|
1046
|
+
build output. The browser calls your endpoint; your endpoint calls the
|
|
1047
|
+
provider. This is the `#third-parties` step-2 rule: a key-bearing call is never
|
|
1048
|
+
client-side.
|
|
1049
|
+
- **Key handling per environment (`#secrets`).** A distinct key per environment
|
|
1050
|
+
(dev / staging / prod); a low provider-side spend cap on the non-prod keys;
|
|
1051
|
+
the production key scoped to only the models and endpoints used where the
|
|
1052
|
+
provider supports scoped keys; rotation without a redeploy.
|
|
1053
|
+
- **Declared third party (`#third-parties`, `#privacy`).** The provider origin is
|
|
1054
|
+
in the manifest with its `purpose` and a `dataSeen` that states what leaves
|
|
1055
|
+
your origin (user prompt content and any context you attach) and the
|
|
1056
|
+
provider's retention / training posture. Where the provider offers a
|
|
1057
|
+
no-training or zero-retention mode for API traffic, that mode is the one in
|
|
1058
|
+
use. The privacy page renders the entry.
|
|
1059
|
+
- **Timeout, retry, rate limits.** Every call has an explicit timeout. Retries
|
|
1060
|
+
use capped exponential backoff with jitter and a maximum attempt count, and
|
|
1061
|
+
cover only the safe-to-retry failures (429, 5xx, connection reset) — never a
|
|
1062
|
+
content, auth, or validation error. A provider `Retry-After` is honoured. The
|
|
1063
|
+
app enforces its **own** per-user / per-session request ceiling independent of
|
|
1064
|
+
the provider's quota, and degrades to a clear message when a ceiling or a
|
|
1065
|
+
timeout is hit — never a hang, never an unbounded queue.
|
|
1066
|
+
- **Cost / token observability (`#observability`).** Every call records, per
|
|
1067
|
+
request: model, input tokens, output tokens, computed cost, latency, and
|
|
1068
|
+
outcome (ok / timeout / refusal / error). Token counts come from the provider
|
|
1069
|
+
response where it returns them, not an estimate. The record is queryable by
|
|
1070
|
+
day and by user/session, and a cost anomaly (a spike, a runaway agent loop) is
|
|
1071
|
+
alertable.
|
|
1072
|
+
- **Moderation / safety layer, sized to the subject.** Input and output pass a
|
|
1073
|
+
safety check proportional to the domain's risk and the audience:
|
|
1074
|
+
- a low-risk internal or authenticated-staff tool may rely on the provider's
|
|
1075
|
+
built-in safety alone;
|
|
1076
|
+
- a public, unauthenticated, or minor-accessible surface adds an explicit
|
|
1077
|
+
moderation pass (a moderation endpoint, a classifier, or a policy prompt) on
|
|
1078
|
+
user input and on model output, plus prompt-injection mitigation on any
|
|
1079
|
+
tool-calling / agentic path (untrusted retrieved content can never escalate
|
|
1080
|
+
tool permissions or exfiltrate context);
|
|
1081
|
+
- a regulated or high-harm subject (medical, legal, financial guidance) adds
|
|
1082
|
+
the domain disclaimer, explicit scope limits in the system prompt, and a
|
|
1083
|
+
refusal or human-review path for out-of-scope requests.
|
|
1084
|
+
The chosen level and its reasoning are recorded in `CLAUDE.md`.
|
|
1085
|
+
- **AI disclosure to end users.** Wherever a user reads model-generated content
|
|
1086
|
+
or converses with a model, a plain, visible statement says so (e.g. "Responses
|
|
1087
|
+
are AI-generated and can be wrong"). Not buried in a policy page. If the model
|
|
1088
|
+
presents as a persona, the disclosure is more prominent, not less.
|
|
1089
|
+
- **Provider usage-policy fit.** Before launch, the project's subject matter and
|
|
1090
|
+
intended use are checked against the provider's usage / acceptable-use policy
|
|
1091
|
+
(prohibited-use categories, output-use and attribution terms, any
|
|
1092
|
+
consumer-vs-API distinctions). A mismatch is resolved before shipping. The
|
|
1093
|
+
check is re-run when the subject expands or the provider revises its terms; the
|
|
1094
|
+
date of the last check is on `#observability`'s manual checklist.
|
|
1095
|
+
- **The response transform is a pure tested function (`#domain-tests`).** Parsing
|
|
1096
|
+
model output — JSON extraction, tool-call dispatch, citation resolution — is a
|
|
1097
|
+
pure function tested against captured fixtures, along with the timeout,
|
|
1098
|
+
refusal, and malformed-output paths. The network call itself is not
|
|
1099
|
+
unit-tested.
|
|
1100
|
+
|
|
1101
|
+
**Rationale.** A model API is simultaneously a third party that sees user
|
|
1102
|
+
content, a metered cost centre that can run away, and a source of output that can
|
|
1103
|
+
be wrong, unsafe, or against the provider's terms. Each of those needs a
|
|
1104
|
+
deliberate control and none of them is the provider's default.
|
|
1105
|
+
|
|
1106
|
+
**Verify.** The provider origin is in the manifest and on the privacy page. Grep
|
|
1107
|
+
confirms no provider key is reachable from client code. A per-request cost/token
|
|
1108
|
+
record exists and the audit page shows spend by day. `CLAUDE.md` records the
|
|
1109
|
+
moderation level and the last usage-policy-check date. The disclosure is visible
|
|
1110
|
+
on the AI surface in a fresh browser.
|
|
1111
|
+
|
|
1112
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
1113
|
+
|
|
1114
|
+
## agent-artifacts — Agent-generated downloadable artifacts
|
|
1115
|
+
|
|
1116
|
+
**Contract.** In scope only when the project lets a user generate a downloadable
|
|
1117
|
+
file through model-driven code execution or an equivalent mechanism (a chart, a
|
|
1118
|
+
spreadsheet, a document, an archive). Code execution is sandboxed; artifacts have
|
|
1119
|
+
bounded storage with an enforced expiry; delivery is authorization-checked and
|
|
1120
|
+
defensively typed; and each artifact carries its own generation cost as an
|
|
1121
|
+
observability signal.
|
|
1122
|
+
|
|
1123
|
+
**Specs.**
|
|
1124
|
+
- **Sandboxed execution.** Model-generated or model-directed code runs in an
|
|
1125
|
+
isolated sandbox with no route to the app's network, secrets, database, or
|
|
1126
|
+
filesystem beyond a scratch directory, and with enforced CPU, memory,
|
|
1127
|
+
wall-clock, and output-size limits. The sandbox is torn down after each run. A
|
|
1128
|
+
shared long-lived interpreter process is not a sandbox.
|
|
1129
|
+
- **Storage & expiry.** Artifacts are written to object storage — not the app
|
|
1130
|
+
server's local disk, not the database as blobs. Each has a TTL (a default in
|
|
1131
|
+
hours to a few days, recorded in `CLAUDE.md`) after which a scheduled job
|
|
1132
|
+
deletes it. Total artifact storage per user/session is capped.
|
|
1133
|
+
- **Delivery security.** The download route is authorization-checked on every
|
|
1134
|
+
request (the requester owns the artifact or its session) — an unguessable URL
|
|
1135
|
+
is not sufficient unless the artifact is explicitly public and non-sensitive.
|
|
1136
|
+
Responses set `Content-Type` from a server-side allowlist,
|
|
1137
|
+
`Content-Disposition: attachment`, `X-Content-Type-Options: nosniff`, and a
|
|
1138
|
+
restrictive CSP — or are served from an isolated origin — so a generated
|
|
1139
|
+
HTML/SVG file cannot execute in the app's origin. User-influenced filenames are
|
|
1140
|
+
sanitised.
|
|
1141
|
+
- **Per-artifact cost (`#observability`).** Each artifact records the model cost
|
|
1142
|
+
and the sandbox compute cost to produce it, attributable to a user/session and
|
|
1143
|
+
queryable alongside the `#llm-integration` per-request figures. A user
|
|
1144
|
+
generating artifacts in a loop is visible and rate-limitable.
|
|
1145
|
+
- **Clean degradation.** A sandbox failure, timeout, or oversized result returns
|
|
1146
|
+
a clear error — never a partial or corrupt file, never a hang.
|
|
1147
|
+
|
|
1148
|
+
**Rationale.** Executing model-authored code is the highest-privilege thing an AI
|
|
1149
|
+
feature does; an unsandboxed run is a remote-code-execution surface. A generated
|
|
1150
|
+
file served from the app origin with a guessed content type is a stored-XSS
|
|
1151
|
+
surface. Both are opt-in risks that earn their own controls.
|
|
1152
|
+
|
|
1153
|
+
**Verify.** Review plus a test confirm a hostile snippet in the sandbox cannot
|
|
1154
|
+
reach secrets or the database. A download request without authorization is
|
|
1155
|
+
rejected. `Content-Disposition` and `nosniff` are set on artifact responses. The
|
|
1156
|
+
expiry job runs and old artifacts are gone. The audit page shows artifact count
|
|
1157
|
+
and cost by day.
|
|
1158
|
+
|
|
1159
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
1160
|
+
|
|
1161
|
+
## usage-metering — Usage-based cost metering
|
|
1162
|
+
|
|
1163
|
+
**Contract.** In scope only when the project's pricing is not flat — a per-user
|
|
1164
|
+
or per-session allowance, metered overage, or pay-as-you-go tied to AI usage.
|
|
1165
|
+
Usage is recorded per subject as an auditable event log, plan-tier limits are
|
|
1166
|
+
enforced server-side before the spend is incurred, and the recorded usage is
|
|
1167
|
+
reconciled against the payment processor.
|
|
1168
|
+
|
|
1169
|
+
**Specs.**
|
|
1170
|
+
- **Per-subject event log.** Every metered event (a request, a token batch, an
|
|
1171
|
+
artifact) is written as an immutable row keyed to the user or session, with
|
|
1172
|
+
timestamp, quantity, unit, computed cost, and a link to the originating
|
|
1173
|
+
`#llm-integration` call. Period aggregates are derived from those rows, never
|
|
1174
|
+
kept only as an in-place counter.
|
|
1175
|
+
- **Plan-tier enforcement is server-side and pre-spend.** The allowance check
|
|
1176
|
+
runs on your server before the provider call is made — a client-side or
|
|
1177
|
+
after-the-fact check is not enforcement. Over the limit does one of: a hard
|
|
1178
|
+
block with an upgrade path; a metered-overage path the user has explicitly
|
|
1179
|
+
agreed to; or a graceful degrade (a smaller model, a queue) — the choice is
|
|
1180
|
+
recorded in `CLAUDE.md`. Tier limits are read from config/data, not hardcoded
|
|
1181
|
+
across scattered conditionals.
|
|
1182
|
+
- **Reconciliation with the payment processor.** A scheduled job compares
|
|
1183
|
+
recorded usage to what the processor (Stripe et al.) was told to bill —
|
|
1184
|
+
metered usage vs. reported usage vs. invoiced amount — and flags drift beyond a
|
|
1185
|
+
threshold for review. Usage reported to the processor is idempotent (a retry
|
|
1186
|
+
never double-bills). Processor webhooks (`subscription.updated`, payment
|
|
1187
|
+
failure) update local tier state.
|
|
1188
|
+
- **The user can see their usage.** A usage view shows consumption against the
|
|
1189
|
+
allowance for the current period, computed from the same rows billing uses —
|
|
1190
|
+
not a separate, drift-prone display counter.
|
|
1191
|
+
- **Metering survives partial failure.** A provider call that succeeded but whose
|
|
1192
|
+
metering write failed is reconciled (the usage is not lost); a metering write
|
|
1193
|
+
for a call that then failed is reversed or netted. Both paths are tested.
|
|
1194
|
+
- **Boundaries are pure tested functions (`#domain-tests`).** At the limit, one
|
|
1195
|
+
over, a tier change mid-period, proration, and a refund are each covered.
|
|
1196
|
+
|
|
1197
|
+
**Rationale.** Usage-based pricing puts a correctness-critical number — what the
|
|
1198
|
+
user owes — on the same path as a flaky external call. Without an auditable
|
|
1199
|
+
per-event log and processor reconciliation, billing silently drifts from reality
|
|
1200
|
+
and neither side can tell.
|
|
1201
|
+
|
|
1202
|
+
**Verify.** Usage rows exist per event and the aggregates reconcile to them. A
|
|
1203
|
+
request over the tier limit is blocked or metered server-side — tested. The
|
|
1204
|
+
reconciliation job runs and its drift report is on the audit page. The
|
|
1205
|
+
user-facing usage figure matches the billing record.
|
|
1206
|
+
|
|
1207
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
1208
|
+
|
|
1209
|
+
---
|
|
1210
|
+
|
|
1211
|
+
# Part K — Meta
|
|
1212
|
+
|
|
1213
|
+
## generated-asset-freshness — Keeping generated assets fresh
|
|
1214
|
+
|
|
1215
|
+
**Contract.** A generated asset (OG card, favicon, tokens stylesheet) cannot be
|
|
1216
|
+
allowed to go stale. Regeneration is layered so no single tier is a
|
|
1217
|
+
single point of failure.
|
|
1218
|
+
|
|
1219
|
+
**Specs.**
|
|
1220
|
+
1. **A build step that always runs** regenerates on every build and every
|
|
1221
|
+
incremental rebuild. If a build ran, the assets are correct.
|
|
1222
|
+
2. **An editor/agent hook** regenerates the moment a source-of-truth file
|
|
1223
|
+
(`#brand-source` and its inputs) is edited, even with no dev server running,
|
|
1224
|
+
and nudges the operator to review the audit page.
|
|
1225
|
+
3. **A `CLAUDE.md` instruction** covers the judgment cases the hook's
|
|
1226
|
+
path-matching can't (e.g. a page-title edit), and tells the assistant to run
|
|
1227
|
+
the one gate before reporting work complete.
|
|
1228
|
+
- The Claude Code `PostToolUse` hook **pattern** is the same on every stack —
|
|
1229
|
+
match an edit to a source-of-truth file, run the regen command, surface a
|
|
1230
|
+
message. Only the command and the watched-file globs are per-stack.
|
|
1231
|
+
- `CLAUDE.md` states the `#brand-source` single-source rule so the assistant
|
|
1232
|
+
never hardcodes a brand value.
|
|
1233
|
+
|
|
1234
|
+
**Verify.** Edit a brand token with no server running → the hook regenerates and
|
|
1235
|
+
the audit page reflects it after a rebuild.
|
|
1236
|
+
|
|
1237
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
1238
|
+
|
|
1239
|
+
## beyond-this-standard — Beyond this standard
|
|
1240
|
+
|
|
1241
|
+
**Contract.** This standard covers content sites, tool sites, and their forms.
|
|
1242
|
+
When a project needs more, that is a deliberate threshold crossing, not a drift.
|
|
1243
|
+
|
|
1244
|
+
**Specs.**
|
|
1245
|
+
- **Signals you are past the static envelope:** a database; user accounts /
|
|
1246
|
+
authentication; server-side application logic; server-side rendering per
|
|
1247
|
+
request; per-user state; background jobs; bolting a datastore (e.g. "Netlify
|
|
1248
|
+
DB") onto a static stack.
|
|
1249
|
+
- **An LLM / agent surface** is its own opt-in axis (Part J), gated by need, not
|
|
1250
|
+
by framework. A single stateless model call from an otherwise-static site is a
|
|
1251
|
+
serverless function (`#third-parties`); server-managed conversations,
|
|
1252
|
+
streaming, code-execution artifacts, or usage metering put you past the
|
|
1253
|
+
envelope. Either way the controls are `#llm-integration` and its siblings.
|
|
1254
|
+
- **What carries over unchanged:** SEO (`seo-*`, `structured-data`,
|
|
1255
|
+
`sitemap-robots`, `noindex`), `a11y`, `domain-tests`, `the-gate`, `ci-cd`
|
|
1256
|
+
philosophy, `secrets`, `brand-source`, `content-model`'s validation contract,
|
|
1257
|
+
`privacy` posture, `security-headers`, `observability`.
|
|
1258
|
+
- **What changes:** the deploy model (a running server, migrations), the content
|
|
1259
|
+
model (DB-backed content joins externalized files), and rendering (SSR is
|
|
1260
|
+
native, not a prerender workaround).
|
|
1261
|
+
- If the "yes" is substantial, the right framework is probably **Elixir/Phoenix**
|
|
1262
|
+
— `stacks/phoenix.md` is the binding of this standard for when a database is in
|
|
1263
|
+
scope. A small "we need one dynamic thing" is often better served by a single
|
|
1264
|
+
serverless function (`#third-parties`) than by changing frameworks.
|
|
1265
|
+
|
|
1266
|
+
**Verify.** The project's `CLAUDE.md` `scope:` key records `static+forms` or
|
|
1267
|
+
`+db` / `+auth` / `+ssr`.
|
|
1268
|
+
|
|
1269
|
+
**Implementation.** For `+db` and beyond, use `stacks/phoenix.md`.
|
|
1270
|
+
|
|
1271
|
+
## adopting — Adopting this standard
|
|
1272
|
+
|
|
1273
|
+
**Contract.** Every project runs the stack-selection flow at the start and
|
|
1274
|
+
records the result. "The gate passing" is the definition of compliant.
|
|
1275
|
+
|
|
1276
|
+
**Specs.**
|
|
1277
|
+
|
|
1278
|
+
**Stack-selection flow** — run at project start (or when onboarding an existing
|
|
1279
|
+
project), before non-trivial work. Ask five questions with defaults:
|
|
1280
|
+
|
|
1281
|
+
1. **Framework?** Eleventy + Netlify *(default)* · Elixir/Phoenix *(when a
|
|
1282
|
+
database or server-side state is in scope — `#beyond-this-standard`)* · other
|
|
1283
|
+
*(→ no binding exists; use this file as principles and do what is idiomatic
|
|
1284
|
+
for the technology)*. `stacks/sveltekit.md` is a **parked** stub — its
|
|
1285
|
+
friction points and open questions are written, the per-section "how" is not;
|
|
1286
|
+
it is not on offer here until a real project forces those decisions, so
|
|
1287
|
+
treat SvelteKit as "other" for now.
|
|
1288
|
+
2. **CSS system?** Pico classless *(default)* · Pico class-based · Tailwind ·
|
|
1289
|
+
Tailwind + component library · vanilla tokens. (Options are bound per stack
|
|
1290
|
+
in the impl doc's `styling` section.)
|
|
1291
|
+
3. **Anything outside "static + forms"?** — a database, auth, server-side logic,
|
|
1292
|
+
SSR, background jobs, per-user state. *Default: No.* Yes →
|
|
1293
|
+
`#beyond-this-standard`; a substantial yes → revisit Q1 (probably Phoenix).
|
|
1294
|
+
4. **Display advertising?** *Default: No.* Yes → `#ads`; record `ads: <provider>`
|
|
1295
|
+
(e.g. `adsense`). This brings a consent banner, a widened CSP, an `ads.txt`,
|
|
1296
|
+
and a CWV cost — it is a deliberate choice, not a default.
|
|
1297
|
+
5. **Does the project call an LLM / agent API?** *Default: No.* Yes → Part J;
|
|
1298
|
+
record `ai: llm-api`. Then two independent follow-ups: does a user generate
|
|
1299
|
+
downloadable files via model-driven code execution? (→ add `+artifacts`,
|
|
1300
|
+
`#agent-artifacts`); is pricing usage-based rather than flat? (→ add
|
|
1301
|
+
`+metering`, `#usage-metering`). This gate is by real need, not by framework —
|
|
1302
|
+
a plain CRUD app that calls no model is unaffected.
|
|
1303
|
+
|
|
1304
|
+
Follow-ups when relevant: production domain; content type (`tool` vs `article` —
|
|
1305
|
+
`article` implies feeds); publishing rate for content sites.
|
|
1306
|
+
|
|
1307
|
+
**Starting point.** Where a copy-and-modify template exists for the chosen stack
|
|
1308
|
+
(`templates/<stack>/` — see the impl doc and `README.md`), starting from it *is*
|
|
1309
|
+
the new-project path: the template is already compliant, so a new project begins
|
|
1310
|
+
at "fill in brand + content" instead of "assemble the machinery". The impl doc's
|
|
1311
|
+
numbered new-project checklist is then two things: the by-hand equivalent for a
|
|
1312
|
+
stack with no template, and the annotated inventory of what the template
|
|
1313
|
+
contains. Only `eleventy-netlify` has a template today; the others follow the
|
|
1314
|
+
checklist. The recommended input to the flow is a filled
|
|
1315
|
+
`templates/concept-brief.md` (the idea, the answers above, a concept-level
|
|
1316
|
+
brand, the content model, layout notes) — `README.md` has the hand-off.
|
|
1317
|
+
|
|
1318
|
+
**Record** the result near the top of the project's `.claude/CLAUDE.md` /
|
|
1319
|
+
`AGENTS.md` as a `## House style` section — a directive paragraph plus a data
|
|
1320
|
+
block. The recommended text:
|
|
1321
|
+
|
|
1322
|
+
```markdown
|
|
1323
|
+
## House style
|
|
1324
|
+
|
|
1325
|
+
This project follows the web house style at <path or repo>. **Use it for all
|
|
1326
|
+
work unless a request explicitly says otherwise.** Before implementing
|
|
1327
|
+
anything, check whether a chapter in `core.md` or `stacks/<framework>.md`
|
|
1328
|
+
governs it and follow that chapter — the slugs are the index (a `<script>` →
|
|
1329
|
+
`#security-headers` + `#client-logic`; a new page → `#seo-meta` +
|
|
1330
|
+
`#internal-links`; anything brand → `#brand-source`; a third party →
|
|
1331
|
+
`#third-parties`).
|
|
1332
|
+
|
|
1333
|
+
- A conflict between the current code and a chapter is a **migration gap** —
|
|
1334
|
+
surface it; don't quietly fix it inside an unrelated task and don't quietly
|
|
1335
|
+
route around it.
|
|
1336
|
+
- Never silently diverge from a **Contract**. A deliberate deviation is
|
|
1337
|
+
recorded in this section with its reason.
|
|
1338
|
+
- Where the house style is silent, do what is idiomatic for the stack.
|
|
1339
|
+
- Run the gate (`<stack check command>`) before reporting any change complete —
|
|
1340
|
+
a green gate is the definition of done.
|
|
1341
|
+
- Run `<stack compliance command>` (`#compliance`) at onboarding — its `FAIL`s
|
|
1342
|
+
are the migration backlog — and again whenever `standard-version` is behind
|
|
1343
|
+
the standard's current version.
|
|
1344
|
+
|
|
1345
|
+
- framework: eleventy-netlify
|
|
1346
|
+
- css: pico-classless
|
|
1347
|
+
- scope: static+forms # or: +db, +auth, +ssr — see core.md#beyond-this-standard
|
|
1348
|
+
- ads: none # or: adsense — see core.md#ads
|
|
1349
|
+
- ai: none # or: llm-api (+artifacts if model-driven file downloads, +metering if usage-priced) — see core.md#llm-integration
|
|
1350
|
+
- production-url: https://example.com
|
|
1351
|
+
- content-type: tool # tool | article (article => feeds)
|
|
1352
|
+
- publishing-rate: ~5 pages/week
|
|
1353
|
+
- standard-version: 2026-09-01 # optional pin
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
If the section is absent, the assistant's first action is to run the flow above
|
|
1357
|
+
and propose it. Each impl doc's `adopting` section fills in the gate command and
|
|
1358
|
+
adds any stack-specific `CLAUDE.md` prose (e.g. the generated-assets note).
|
|
1359
|
+
|
|
1360
|
+
**Onboarding an existing project** — after recording the section, run
|
|
1361
|
+
`#compliance`. Every `FAIL` and `MANUAL` becomes a migration-backlog item,
|
|
1362
|
+
worked incrementally (the impl doc's migration path is the ordered version of
|
|
1363
|
+
that backlog). The project is not expected to be all-`PASS` on day one; it is
|
|
1364
|
+
expected to know exactly where it isn't.
|
|
1365
|
+
|
|
1366
|
+
**Change checklist (stack-agnostic)** — run before merging to `main`, and again
|
|
1367
|
+
before `bin/deploy`. (On a multi-committer project this is also the PR-review
|
|
1368
|
+
checklist.)
|
|
1369
|
+
- the gate is green locally (and in CI once pushed); the audit page summary bar
|
|
1370
|
+
is clean
|
|
1371
|
+
- new/changed content is in data, not markup; the schema is updated; `title` +
|
|
1372
|
+
`meta_description` present and in range
|
|
1373
|
+
- a new page has 2–4 related links, resolves an `og:image`, is in a collection
|
|
1374
|
+
or deliberately excluded, has a directory-style permalink
|
|
1375
|
+
- a touched formula has a pure module + a test encoding the page's worked example
|
|
1376
|
+
- a touched brand token / stylesheet / card template / icon generator has had
|
|
1377
|
+
the audit page reviewed
|
|
1378
|
+
- an added external origin passed the decision test, is in the manifest, the CSP
|
|
1379
|
+
is widened to match, the failure/no-JS fallback is tested, the privacy page is
|
|
1380
|
+
updated — all in the same change
|
|
1381
|
+
- a changed permalink ships a `301`
|
|
1382
|
+
- new interactive UI: keyboard + reduced-motion + labels
|
|
1383
|
+
- a changed dashboard setting bumped its `verifiedOn`
|
|
1384
|
+
- if a migration-backlog item was the point of the change, `#compliance` now
|
|
1385
|
+
reports it `PASS` (or `MANUAL` with a note)
|
|
1386
|
+
- commit messages follow convention; the change is one logical unit
|
|
1387
|
+
|
|
1388
|
+
Each impl doc adds a concrete new-project checklist and an ordered, dependency-
|
|
1389
|
+
aware migration path.
|
|
1390
|
+
|
|
1391
|
+
**Verify.** The `CLAUDE.md` `## House style` section exists (the directive
|
|
1392
|
+
paragraph *and* the data block) and is loaded each session.
|
|
1393
|
+
|
|
1394
|
+
**Implementation.** See your stack's impl doc, same slug.
|
|
1395
|
+
|
|
1396
|
+
## out-of-scope — Cross-cutting out of scope
|
|
1397
|
+
|
|
1398
|
+
Named so they are a conscious "not now", not an oversight. Add coverage when a
|
|
1399
|
+
project actually needs it.
|
|
1400
|
+
|
|
1401
|
+
- **Internationalization** — multiple locales, `hreflang`, per-locale sitemaps.
|
|
1402
|
+
Revisit URL and canonical strategy if added.
|
|
1403
|
+
- **Analytics beyond privacy-first or consent-gated** — out by **policy**
|
|
1404
|
+
(`#privacy`), not merely scope.
|
|
1405
|
+
- **Ad-density and editorial ad policy** — `#ads` covers the wiring (consent,
|
|
1406
|
+
CSP, CLS, `ads.txt`, non-prod gating); how many units per page, where they go,
|
|
1407
|
+
and which pages carry them is the provider's policy plus editorial judgment,
|
|
1408
|
+
recorded in `CLAUDE.md`, not specified here.
|
|
1409
|
+
- **A design-token pipeline** (Style Dictionary et al.) — `#brand-source` as one
|
|
1410
|
+
file is the small-project version; graduate only when tokens outgrow it.
|
|
1411
|
+
- **Server-side application logic as a way to run the app** — that is
|
|
1412
|
+
`#beyond-this-standard`; a thin key-hiding proxy is the one carve-out
|
|
1413
|
+
(`#third-parties`).
|
|
1414
|
+
- **ML infrastructure** — model fine-tuning or training pipelines, a
|
|
1415
|
+
vector-store / RAG retrieval layer, an evals harness, a prompt-versioning
|
|
1416
|
+
system. `#llm-integration` covers *calling* a model API; building the
|
|
1417
|
+
machinery around it is its own design per project.
|
|
1418
|
+
|
|
1419
|
+
Each impl doc keeps its own stack-specific out-of-scope list.
|
|
1420
|
+
|
|
1421
|
+
## anti-patterns — Stack-agnostic anti-patterns
|
|
1422
|
+
|
|
1423
|
+
- A brand colour / font / logo hardcoded outside the brand source.
|
|
1424
|
+
- Content strings living in markup when they're really data.
|
|
1425
|
+
- A committed `.env`; a secret in a data file or template.
|
|
1426
|
+
- Hand-maintained `sitemap.xml` / `robots.txt` / `feed.xml`.
|
|
1427
|
+
- Any third-party analytics that loads before consent; GA/GTM without a CMP
|
|
1428
|
+
gate; analytics that sets a cross-site cookie.
|
|
1429
|
+
- An ad loader that runs before consent; ads on a non-production build; a
|
|
1430
|
+
hand-edited `ads.txt` left to drift; an ad slot with no reserved dimensions
|
|
1431
|
+
(CLS); a CMP the ad provider doesn't certify; auto-ads that rewrite page
|
|
1432
|
+
layout; `'unsafe-eval'` or a widened `default-src` added to satisfy an ad
|
|
1433
|
+
script.
|
|
1434
|
+
- A model-provider API key in client code or a data file; an LLM call made from
|
|
1435
|
+
the browser.
|
|
1436
|
+
- An LLM feature with no per-request token/cost record, no app-side per-user
|
|
1437
|
+
request ceiling, or unbounded agent retries — a runaway-cost surface.
|
|
1438
|
+
- Model-generated code executed outside a sandbox; a generated file served from
|
|
1439
|
+
the app's own origin with a guessed `Content-Type` and no `attachment`
|
|
1440
|
+
disposition.
|
|
1441
|
+
- AI-generated content shown to users with no visible disclosure that it is AI.
|
|
1442
|
+
- Shipping an AI feature without checking the provider's usage policy against the
|
|
1443
|
+
project's subject matter.
|
|
1444
|
+
- Usage-based billing with no auditable per-event log, or plan limits enforced
|
|
1445
|
+
only client-side or after the spend; usage reported to the payment processor
|
|
1446
|
+
non-idempotently.
|
|
1447
|
+
- A content page with no related links (a dead end).
|
|
1448
|
+
- Re-implementing what native `<dialog>` / `<summary>` already do.
|
|
1449
|
+
- Transitions/animations not gated behind `prefers-reduced-motion`.
|
|
1450
|
+
- Treating the automated a11y scan as full accessibility coverage.
|
|
1451
|
+
- Canonical / OG / sitemap URLs that vary by deploy context, or are relative.
|
|
1452
|
+
- An `og:image` that is relative, a `data:` URI, an SVG, hotlinked, or absent.
|
|
1453
|
+
- OG / meta tags hand-written per page instead of one data-driven implementation.
|
|
1454
|
+
- An inline `<script>`, `on*=` handler, or `style=` attribute in the output —
|
|
1455
|
+
the strict CSP blocks all of them; the wiring belongs in a loaded file.
|
|
1456
|
+
- A custom web font shipped without checking its subset covers the site's text.
|
|
1457
|
+
- A third `PROJECT.md`-style rules doc.
|
|
1458
|
+
- CI or a git hook running a hand-picked subset of the gate.
|
|
1459
|
+
- Putting `#compliance` in the gate (a migration gap would then block all work),
|
|
1460
|
+
or treating one of its `FAIL`s as a regression to fix immediately rather than
|
|
1461
|
+
a backlog item.
|
|
1462
|
+
- Working through a migration backlog without re-running `#compliance`, so it's
|
|
1463
|
+
unknown what's actually left or what regressed.
|
|
1464
|
+
- A `standard-version` pin left behind the standard's current version with no
|
|
1465
|
+
`CHANGELOG`-delta re-check.
|
|
1466
|
+
- A permalink changed with no `301` from the old path.
|
|
1467
|
+
- A default branch other than `main`.
|
|
1468
|
+
- Deploying by pushing to a branch instead of the sanctioned gated command.
|
|
1469
|
+
- An `immutable` cache header on an asset whose filename isn't content-hashed.
|
|
1470
|
+
- Relying on the assistant to *remember* to regenerate assets instead of a hook.
|
|
1471
|
+
- More than one file reading environment variables for the site.
|
|
1472
|
+
- An audit page with no summary bar, a secret value on it, or a client-side
|
|
1473
|
+
password treated as access control.
|
|
1474
|
+
|
|
1475
|
+
---
|
|
1476
|
+
|
|
1477
|
+
# Appendix — Slug registry
|
|
1478
|
+
|
|
1479
|
+
The canonical set of CORE slugs. An impl doc reuses a slug **only** for the
|
|
1480
|
+
mirrored chapter; impl-only chapters take slugs that appear nowhere here.
|
|
1481
|
+
|
|
1482
|
+
`repo-hygiene` · `runtime-pin` · `config-idiom` · `content-model` · `rendering` ·
|
|
1483
|
+
`assets` · `styling` · `client-logic` · `theme` · `seo-urls` · `seo-meta` ·
|
|
1484
|
+
`og-image` · `structured-data` · `sitemap-robots` · `noindex` · `brand-source` ·
|
|
1485
|
+
`audit-page` · `security-headers` · `caching` · `third-parties` · `privacy` ·
|
|
1486
|
+
`ads` · `internal-links` · `forms` · `domain-tests` · `a11y` · `the-gate` · `compliance` ·
|
|
1487
|
+
`ci-cd` · `secrets` · `observability` · `llm-integration` · `agent-artifacts` ·
|
|
1488
|
+
`usage-metering` · `generated-asset-freshness` · `beyond-this-standard` ·
|
|
1489
|
+
`adopting` · `out-of-scope` · `anti-patterns`
|
|
1490
|
+
|
|
1491
|
+
**Shared-concept drift guard** — defined only here; an impl doc may state the
|
|
1492
|
+
*how*, never restate the *what*: `the-gate`, `ci-cd`, `third-parties`,
|
|
1493
|
+
`brand-source`, `noindex`, `content-model`, `beyond-this-standard`.
|