@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.
@@ -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`.