@piercebarney/whs-eleventy 2026.10.7 → 2026.10.9
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 +3 -1
- package/cli.js +2 -0
- package/lib/compliance.js +37 -5
- package/lib/content-check.js +5 -4
- package/lib/doctor.js +2 -2
- package/lib/html.js +61 -0
- package/package.json +2 -1
- package/standard/CHANGELOG.md +195 -0
- package/standard/core.md +33 -10
package/README.md
CHANGED
|
@@ -7,9 +7,10 @@ the stack runs, so a fix lands once and `npm update` carries it everywhere.
|
|
|
7
7
|
```
|
|
8
8
|
whs compliance [--strict] codebase-vs-standard conformance sweep
|
|
9
9
|
whs doctor [--live|--deploy-preflight] infrastructure drift check
|
|
10
|
+
whs html html-validate (standard preset) over every _site/ page
|
|
10
11
|
whs links built-output link / CSP / JSON-LD integrity
|
|
11
12
|
whs a11y axe-core WCAG 2.1 A/AA scan of _site/
|
|
12
|
-
whs content-check lint · validate · build · links (no browser)
|
|
13
|
+
whs content-check lint · validate · build · html · links (no browser)
|
|
13
14
|
whs --version tooling + bundled-standard version
|
|
14
15
|
```
|
|
15
16
|
|
|
@@ -29,6 +30,7 @@ Then in `package.json`:
|
|
|
29
30
|
```json
|
|
30
31
|
{
|
|
31
32
|
"scripts": {
|
|
33
|
+
"html": "whs html",
|
|
32
34
|
"links": "whs links",
|
|
33
35
|
"a11y": "whs a11y",
|
|
34
36
|
"doctor": "whs doctor",
|
package/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// whs status [--hook] standard-version drift, at a glance
|
|
5
5
|
// whs compliance [--strict] codebase-vs-standard conformance sweep
|
|
6
6
|
// whs doctor [--live|--deploy-preflight] infrastructure drift check
|
|
7
|
+
// whs html html-validate (standard preset) over every _site/ page
|
|
7
8
|
// whs links built-output link / CSP / JSON-LD integrity
|
|
8
9
|
// whs a11y axe-core WCAG 2.1 A/AA scan of _site/
|
|
9
10
|
// whs content-check the content-relevant slice of the gate
|
|
@@ -20,6 +21,7 @@ const COMMANDS = {
|
|
|
20
21
|
status: "status.js",
|
|
21
22
|
compliance: "compliance.js",
|
|
22
23
|
doctor: "doctor.js",
|
|
24
|
+
html: "html.js",
|
|
23
25
|
links: "check-links.js",
|
|
24
26
|
a11y: "a11y.js",
|
|
25
27
|
"content-check": "content-check.js",
|
package/lib/compliance.js
CHANGED
|
@@ -217,6 +217,12 @@ const CHECKS = {
|
|
|
217
217
|
|
|
218
218
|
rendering: () => {
|
|
219
219
|
if (!has("src/404.njk")) return { status: FAIL, note: "no src/404.njk" };
|
|
220
|
+
// core.md#rendering requires every built page to be valid HTML — mirrors
|
|
221
|
+
// the a11y chapter's own "is the scan actually wired into the gate" check
|
|
222
|
+
// below, rather than trusting that the stage exists just because the
|
|
223
|
+
// package ships one.
|
|
224
|
+
if (!/\bhtml\b/.test(JSON.parse(read("package.json") || "{}").scripts?.check || ""))
|
|
225
|
+
return { status: FAIL, note: "no html stage in the gate" };
|
|
220
226
|
// A page whose <main> is empty/near-empty in the built output is the
|
|
221
227
|
// classic "content only appears after client JS runs" regression the
|
|
222
228
|
// Contract forbids — a crude but real content-presence check, distinct
|
|
@@ -232,7 +238,10 @@ const CHECKS = {
|
|
|
232
238
|
status: FAIL,
|
|
233
239
|
note: `${thin.length} page(s) with near-empty <main> — content may be client-rendered only`,
|
|
234
240
|
};
|
|
235
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
status: PASS,
|
|
243
|
+
note: "server-rendered HTML with real content; 404 page present; html-validate stage in the gate",
|
|
244
|
+
};
|
|
236
245
|
},
|
|
237
246
|
|
|
238
247
|
assets: () => {
|
|
@@ -516,6 +525,15 @@ const CHECKS = {
|
|
|
516
525
|
/["']slug["']\s*:\s*["']privacy["']/.test(read("src/content/pages.json") || "");
|
|
517
526
|
if (!builtPath && !source) return { status: FAIL, note: "no privacy page" };
|
|
518
527
|
|
|
528
|
+
// GPC (core.md#privacy): once ads are on, the built privacy page must say
|
|
529
|
+
// GPC is honoured — checked ahead of the disclosure branches below so it
|
|
530
|
+
// applies no matter which of them resolves the rest of the check.
|
|
531
|
+
if (adsMode !== "none" && builtPath) {
|
|
532
|
+
const html = read(builtPath) || "";
|
|
533
|
+
if (!/Global Privacy Control/i.test(html))
|
|
534
|
+
return { status: FAIL, note: "privacy page does not state GPC is honoured" };
|
|
535
|
+
}
|
|
536
|
+
|
|
519
537
|
// Prefer checking the built page's own rendered HTML for every declared
|
|
520
538
|
// third party's name — what a user actually sees, independent of which
|
|
521
539
|
// source file or templating approach produced it. Falls back to the
|
|
@@ -571,10 +589,24 @@ const CHECKS = {
|
|
|
571
589
|
const leak = siteHtml().filter((f) => AD_CODE.test(fs.readFileSync(f, "utf8")));
|
|
572
590
|
if (!prod && leak.length)
|
|
573
591
|
return { status: FAIL, note: `ad code in ${leak.length} non-production page(s)` };
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
592
|
+
|
|
593
|
+
// GPC (core.md#ads): once a production build actually carries a live ad
|
|
594
|
+
// unit, the built site.js must set data-restrict-data-processing for a
|
|
595
|
+
// GPC visitor — checked against the built copy, not the source, so a
|
|
596
|
+
// stale copy-in shows up here rather than only in a manual browser check.
|
|
597
|
+
const INS_UNIT = /<ins[^>]+adsbygoogle/;
|
|
598
|
+
const adsActive = prod && siteHtml().some((f) => INS_UNIT.test(fs.readFileSync(f, "utf8")));
|
|
599
|
+
let note =
|
|
600
|
+
"wired; verify on production: decline consent => no ad requests, accept => ads + CLS ok";
|
|
601
|
+
if (adsActive) {
|
|
602
|
+
const builtJs = read("_site/site.js") || "";
|
|
603
|
+
const gpcHandled =
|
|
604
|
+
/navigator\.globalPrivacyControl/.test(builtJs) &&
|
|
605
|
+
/data-restrict-data-processing/.test(builtJs);
|
|
606
|
+
if (!gpcHandled) return { status: FAIL, note: "ads active but no GPC handling in built JS" };
|
|
607
|
+
note += "; GPC handled";
|
|
608
|
+
}
|
|
609
|
+
return { status: MANUAL, note };
|
|
578
610
|
},
|
|
579
611
|
|
|
580
612
|
"internal-links": () => {
|
package/lib/content-check.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The content-relevant slice of the gate, for an agent editing content
|
|
2
|
-
// (CONTENT.md). Runs lint → validate → build → links — no browser, so
|
|
3
|
-
// (that's backstopped by `npm run check` in bin/deploy before
|
|
2
|
+
// (CONTENT.md). Runs lint → validate → build → html → links — no browser, so
|
|
3
|
+
// no a11y (that's backstopped by `npm run check` in bin/deploy before
|
|
4
|
+
// anything ships).
|
|
4
5
|
//
|
|
5
6
|
// whs content-check
|
|
6
7
|
//
|
|
@@ -79,7 +80,7 @@ function main() {
|
|
|
79
80
|
console.log(`\nℹ ${reqs.length} open request(s) in requests/ awaiting the build arm.\n`);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
const steps = ["lint", "validate", "build", "links"];
|
|
83
|
+
const steps = ["lint", "validate", "build", "html", "links"];
|
|
83
84
|
for (const s of steps) {
|
|
84
85
|
process.stdout.write(`content-check: npm run ${s}\n`);
|
|
85
86
|
try {
|
|
@@ -89,7 +90,7 @@ function main() {
|
|
|
89
90
|
process.exit(1);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
|
-
console.log("\ncontent-check: ok — lint · validate · build · links");
|
|
93
|
+
console.log("\ncontent-check: ok — lint · validate · build · html · links");
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
module.exports = { strayFiles, parsePorcelain, splitStray, RESERVED_SET };
|
package/lib/doctor.js
CHANGED
|
@@ -34,7 +34,7 @@ const loadSite = () => projectRequire("src/_data/site.js");
|
|
|
34
34
|
// feeds the /audit/ page's live-header check via _data/headerExpect.js.
|
|
35
35
|
const HEADER_EXPECT = asRegexMap();
|
|
36
36
|
|
|
37
|
-
const GATE_STAGES = ["lint", "validate", "test", "build", "links", "a11y"];
|
|
37
|
+
const GATE_STAGES = ["lint", "validate", "test", "build", "html", "links", "a11y"];
|
|
38
38
|
|
|
39
39
|
function readText(rel) {
|
|
40
40
|
try {
|
|
@@ -122,7 +122,7 @@ function runRepoChecks({ preflight = false } = {}) {
|
|
|
122
122
|
const absent = GATE_STAGES.filter((s) => !check.includes(s));
|
|
123
123
|
add(
|
|
124
124
|
"gate-stages",
|
|
125
|
-
"`npm run check` runs lint · validate · test · build · links · a11y",
|
|
125
|
+
"`npm run check` runs lint · validate · test · build · html · links · a11y",
|
|
126
126
|
check !== "" && absent.length === 0,
|
|
127
127
|
"warn",
|
|
128
128
|
absent.length ? `not referenced: ${absent.join(", ")}` : "complete",
|
package/lib/html.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// HTML validity — the `html` step of the gate (whs html). Walks the built
|
|
2
|
+
// _site/ recursively (every *.html — error pages and /audit/ included, not
|
|
3
|
+
// just the sitemap) and runs html-validate's `html-validate:standard` preset
|
|
4
|
+
// against each file, exiting non-zero on any error.
|
|
5
|
+
//
|
|
6
|
+
// `standard`, not `recommended`: recommended layers in style opinions (e.g.
|
|
7
|
+
// uppercase DOCTYPE) that core.md#rendering's Contract never asked for — spec
|
|
8
|
+
// conformance only (core.md#rendering: "no stray end tags, no bad nesting, no
|
|
9
|
+
// duplicate ids"). An inline `<!-- [html-validate-disable-next] -->` comment
|
|
10
|
+
// is the escape hatch for a rule that's a false positive on a specific line;
|
|
11
|
+
// no per-project config file — one fixed config keeps every project on the
|
|
12
|
+
// stack checked the same way.
|
|
13
|
+
|
|
14
|
+
const fs = require("node:fs");
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
const { HtmlValidate } = require("html-validate");
|
|
17
|
+
|
|
18
|
+
const SITE = "_site";
|
|
19
|
+
|
|
20
|
+
function walkHtml(dir) {
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
const p = path.join(dir, e.name);
|
|
24
|
+
if (e.isDirectory()) out.push(...walkHtml(p));
|
|
25
|
+
else if (e.name.endsWith(".html")) out.push(p);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function main() {
|
|
31
|
+
if (!fs.existsSync(SITE)) {
|
|
32
|
+
console.error("html: _site/ missing — build first");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const files = walkHtml(SITE);
|
|
37
|
+
if (!files.length) {
|
|
38
|
+
console.error("html: no *.html files found under _site/");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hv = new HtmlValidate({ extends: ["html-validate:standard"] });
|
|
43
|
+
const { valid, results } = await hv.validateMultipleFiles(files);
|
|
44
|
+
|
|
45
|
+
const failing = results.filter((r) => r.messages.length);
|
|
46
|
+
if (!valid || failing.length) {
|
|
47
|
+
const totalErrors = failing.reduce((n, r) => n + r.errorCount, 0);
|
|
48
|
+
console.error(`\nhtml: ${totalErrors} error(s) across ${failing.length} page(s):\n`);
|
|
49
|
+
for (const r of failing) {
|
|
50
|
+
console.error(` ${path.relative(SITE, r.filePath)}`);
|
|
51
|
+
for (const m of r.messages) {
|
|
52
|
+
console.error(` [${m.ruleId}] ${m.line}:${m.column} — ${m.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(` html: ${files.length} pages, valid per the WHATWG HTML Living Standard`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piercebarney/whs-eleventy",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.9",
|
|
4
4
|
"description": "The web house style's Eleventy + Netlify tooling — the compliance sweep, the infra doctor, the link/CSP integrity check, and the a11y scan, shared by every project on the stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"whs": "cli.js"
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"axe-core": "^4.13.0",
|
|
35
|
+
"html-validate": "^11.16.0",
|
|
35
36
|
"node-html-parser": "^9.0.2",
|
|
36
37
|
"sirv": "^3.0.2"
|
|
37
38
|
},
|
package/standard/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,201 @@ enforced, new *opt-in* chapters/capabilities, tooling-only changes — is
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## 2026-10-09 [breaking]
|
|
25
|
+
|
|
26
|
+
### core: privacy
|
|
27
|
+
|
|
28
|
+
`#privacy`'s Specs now require **Global Privacy Control**: a site running
|
|
29
|
+
display ads or analytics that sells/shares personal data honours the GPC
|
|
30
|
+
signal — `navigator.globalPrivacyControl` in the browser, or the `Sec-GPC:
|
|
31
|
+
1` request header where a server renders — as an automatic opt-out of sale,
|
|
32
|
+
sharing, and targeted advertising. No click required, and sharing analytics
|
|
33
|
+
does not load for a GPC visitor. The privacy page must state that GPC is
|
|
34
|
+
honoured wherever it applies. Verify gains: review confirms the privacy
|
|
35
|
+
page states this. Required by US state privacy laws (e.g. Montana's amended
|
|
36
|
+
MCDPA, California's CCPA regs), which apply by where the visitor lives, not
|
|
37
|
+
the site's own location. Decided by the owner in
|
|
38
|
+
`feedback/2026-09-26-external-standards-survey.md` ("Sequencing" step 3,
|
|
39
|
+
Decisions item 2).
|
|
40
|
+
|
|
41
|
+
**Breaking:** a fully compliant ad-running or sharing-analytics site before
|
|
42
|
+
this change had no GPC handling and no privacy-page statement about it —
|
|
43
|
+
neither was required. Both are required now, and `whs compliance`'s
|
|
44
|
+
`privacy` check (see `packages/whs-eleventy` below) `FAIL`s without the
|
|
45
|
+
statement once ads are active.
|
|
46
|
+
|
|
47
|
+
### core: ads
|
|
48
|
+
|
|
49
|
+
`#ads`'s consent-gate spec now extends to the US-state opt-out: a visitor
|
|
50
|
+
sending GPC gets restricted/non-personalized ad requests automatically, and
|
|
51
|
+
the certified CMP also presents the US-state "Do Not Sell or Share My
|
|
52
|
+
Personal Information" opt-out. Verify gains a manual check alongside the
|
|
53
|
+
existing decline/accept checks: browser with GPC on → ad requests
|
|
54
|
+
restricted.
|
|
55
|
+
|
|
56
|
+
**Breaking:** a fully compliant ad-running site before this change served
|
|
57
|
+
personalized ads to a GPC visitor and had no US-state opt-out in its CMP —
|
|
58
|
+
neither was required. Both are required now, and `whs compliance`'s `ads`
|
|
59
|
+
check (see `packages/whs-eleventy` below) `FAIL`s without the built-JS GPC
|
|
60
|
+
handling once a live ad unit exists.
|
|
61
|
+
|
|
62
|
+
### packages/whs-eleventy
|
|
63
|
+
|
|
64
|
+
`lib/compliance.js`'s `ads` check: once a production build carries a live
|
|
65
|
+
`<ins … adsbygoogle>` unit, it now `FAIL`s "ads active but no GPC handling
|
|
66
|
+
in built JS" unless the built `_site/site.js` matches both
|
|
67
|
+
`navigator.globalPrivacyControl` and `data-restrict-data-processing`;
|
|
68
|
+
otherwise it appends "GPC handled" to the existing `MANUAL` note. The
|
|
69
|
+
`privacy` check now `FAIL`s "privacy page does not state GPC is honoured"
|
|
70
|
+
when ads are on and the built privacy page doesn't match
|
|
71
|
+
`/Global Privacy Control/i`. New tests in `test/compliance-checks.test.js`
|
|
72
|
+
cover both branches (ads active + GPC present → no `FAIL`; ads active + GPC
|
|
73
|
+
missing → `FAIL`; privacy statement missing while ads are on → `FAIL`).
|
|
74
|
+
|
|
75
|
+
### templates/eleventy-netlify
|
|
76
|
+
|
|
77
|
+
`static/site.js`'s unit-push loop now sets
|
|
78
|
+
`data-restrict-data-processing="1"` on each `<ins>` immediately before its
|
|
79
|
+
push when `navigator.globalPrivacyControl === true`.
|
|
80
|
+
`src/_includes/thirdparty-disclosure.njk`'s `if ads.pub` block gains a
|
|
81
|
+
paragraph stating GPC is honoured as an automatic opt-out, with a mention of
|
|
82
|
+
the "Do Not Sell or Share My Personal Information" link for visitors whose
|
|
83
|
+
browser doesn't send it. `src/_data/adsAudit.js` adds a `gpcHandled` check
|
|
84
|
+
(same `readFileSync` pattern as the existing CSP check) and `src/audit.njk`
|
|
85
|
+
surfaces it as a new Ads-panel row plus a manual-checklist line.
|
|
86
|
+
`add-ons/ads/README.md` step 5 adds the **US state regulations** Privacy &
|
|
87
|
+
messaging message alongside the GDPR one; the baseline-machinery list notes
|
|
88
|
+
the GPC handling now in `site.js`; step 7's manual verify list gains the GPC
|
|
89
|
+
check.
|
|
90
|
+
|
|
91
|
+
### stacks/eleventy-netlify.md
|
|
92
|
+
|
|
93
|
+
Documents the mechanism above under `#privacy` and `#ads`, and adds a
|
|
94
|
+
correction while touching the push-loop bullet: the prior text claimed the
|
|
95
|
+
push was "guarded on the loader having defined `window.adsbygoogle`" — the
|
|
96
|
+
code has never done that (it sets `window.adsbygoogle = window.adsbygoogle
|
|
97
|
+
|| []` itself and pushes in a `try`/`catch`); the bullet now describes the
|
|
98
|
+
actual mechanism instead of the stale claim.
|
|
99
|
+
|
|
100
|
+
### stacks/sveltekit-netlify.md, stacks/phoenix.md
|
|
101
|
+
|
|
102
|
+
Document the same `#privacy`/`#ads` mechanism for each stack's own template
|
|
103
|
+
shape — no template change in either (neither builds ads today).
|
|
104
|
+
`stacks/sveltekit-netlify.md` notes the unit-push code sets the attribute
|
|
105
|
+
client-side, or `hooks.server.ts` can read `Sec-GPC` for a server-rendered
|
|
106
|
+
route. `stacks/phoenix.md` documents both a client-side hook/`app.js` check
|
|
107
|
+
and a server-side option: a plug reading `Sec-GPC: 1` into a
|
|
108
|
+
`conn.assigns.gpc?` so server-rendered ad units and sharing analytics can
|
|
109
|
+
respect it without reaching the client-side hook at all.
|
|
110
|
+
|
|
111
|
+
**Adopter action (eleventy, ad-running or sharing-analytics projects):**
|
|
112
|
+
`npm update @piercebarney/whs-eleventy`; re-copy the `static/site.js` push
|
|
113
|
+
loop (adds the GPC attribute) and the `thirdparty-disclosure.njk` GPC
|
|
114
|
+
paragraph from this repo; in the AdSense dashboard, enable the "US state
|
|
115
|
+
regulations" Privacy & messaging message alongside the existing GDPR one;
|
|
116
|
+
re-pin `standard-version` to `2026-10-09` once done. Projects with `ads:
|
|
117
|
+
none` and no sale/sharing analytics: pin update only, no code change
|
|
118
|
+
required.
|
|
119
|
+
|
|
120
|
+
## 2026-10-08 [breaking]
|
|
121
|
+
|
|
122
|
+
### core: rendering
|
|
123
|
+
|
|
124
|
+
`#rendering`'s Contract never required HTML *validity* — only that content
|
|
125
|
+
render without client JS and that server/client rendering logic not be
|
|
126
|
+
duplicated. It now adds: every built page — including error pages and
|
|
127
|
+
`/audit/` — must be valid HTML per the **WHATWG HTML Living Standard** (the
|
|
128
|
+
spec W3C itself defers to). Spec conformance only (no stray end tags, no bad
|
|
129
|
+
nesting, no duplicate `id`s) — no style opinions such as DOCTYPE case or
|
|
130
|
+
attribute quoting. Rationale: browsers repair malformed markup silently, so
|
|
131
|
+
invalid structure otherwise ships unseen, and WCAG 2.2 dropped its 4.1.1
|
|
132
|
+
Parsing criterion, so an axe scan no longer catches it either. Decided by
|
|
133
|
+
the owner in `feedback/2026-09-26-external-standards-survey.md`
|
|
134
|
+
("Sequencing" step 2, row 3).
|
|
135
|
+
|
|
136
|
+
### core: the-gate
|
|
137
|
+
|
|
138
|
+
`#the-gate`'s drift-guarded stage order gains an **HTML validity** step,
|
|
139
|
+
placed after `build` and before `link/reference integrity`: lint/format
|
|
140
|
+
check → content validation → domain tests → build → HTML validity →
|
|
141
|
+
link/reference integrity → a11y scan.
|
|
142
|
+
|
|
143
|
+
**Breaking:** a project that was fully compliant before this change (a gate
|
|
144
|
+
that ran build → links → a11y with no HTML-validity step) is no longer
|
|
145
|
+
compliant after it — `#the-gate` now requires an HTML-validity stage that
|
|
146
|
+
didn't exist as a requirement before, and `whs compliance` / `whs doctor`
|
|
147
|
+
(see `packages/whs-eleventy` below) now `FAIL`/warn on its absence rather
|
|
148
|
+
than silently passing.
|
|
149
|
+
|
|
150
|
+
**Adopter action (eleventy):** `npm update @piercebarney/whs-eleventy`, add
|
|
151
|
+
`"html": "whs html"` to `package.json` scripts, add `npm run html` to
|
|
152
|
+
`check` (after `build`, before `links`), then fix any invalid markup the new
|
|
153
|
+
check reports. Re-pin `standard-version` to `2026-10-08` once done.
|
|
154
|
+
|
|
155
|
+
### packages/whs-eleventy
|
|
156
|
+
|
|
157
|
+
New `whs html` command (`lib/html.js`): walks every `*.html` file under the
|
|
158
|
+
built `_site/` recursively (error pages and `/audit/` included, not just the
|
|
159
|
+
sitemap) and runs it through `html-validate`'s `html-validate:standard`
|
|
160
|
+
preset (spec conformance only — not `recommended`'s style opinions), exiting
|
|
161
|
+
non-zero on any error. New dependency `html-validate ^11.16.0`. `whs
|
|
162
|
+
compliance`'s `#rendering` check now `FAIL`s if the project's `check` script
|
|
163
|
+
doesn't reference an `html` stage. `whs doctor`'s `GATE_STAGES` list and its
|
|
164
|
+
`gate-stages` check gain `html`. `whs content-check`'s lighter slice
|
|
165
|
+
(`lint → validate → build → links`) gains `html`, run before `links`. Tests
|
|
166
|
+
added for the new compliance/content-check behavior.
|
|
167
|
+
|
|
168
|
+
### templates/eleventy-netlify
|
|
169
|
+
|
|
170
|
+
Wires the new stage: `package.json` gains an `"html": "whs html"` script and
|
|
171
|
+
inserts `npm run html` into `check` between `build` and `links`; `ci.yml`'s
|
|
172
|
+
inline gate-order comment updated to match. `CLAUDE.md`, `README.md`, and
|
|
173
|
+
`CONTENT.md` document the new stage in their gate-order prose.
|
|
174
|
+
`standard-version` re-pinned to `2026-10-08`.
|
|
175
|
+
|
|
176
|
+
### templates/sveltekit-netlify
|
|
177
|
+
|
|
178
|
+
Implements the chapter for the first time (no prior HTML-validity tooling
|
|
179
|
+
existed for this stack): new `html-validate` devDependency, a
|
|
180
|
+
`.htmlvalidate.json` extending `html-validate:standard`, a new `"html":
|
|
181
|
+
"html-validate \"build/**/*.html\""` script run recursively over every
|
|
182
|
+
built page, and `npm run html` added to `check` after `build`. `README.md`,
|
|
183
|
+
`TEMPLATE.md`, and `ci.yml`'s inline gate-order comment updated to match.
|
|
184
|
+
This stack has no static error/404 page yet (`adapter-static`, no
|
|
185
|
+
`fallback`, no `+error.svelte`), so `core.md#rendering`'s "error pages"
|
|
186
|
+
clause has nothing to check today — the recursive glob will pick one up
|
|
187
|
+
automatically the day this binding adds one.
|
|
188
|
+
|
|
189
|
+
### stacks/eleventy-netlify.md, stacks/sveltekit-netlify.md
|
|
190
|
+
|
|
191
|
+
Each `#rendering` Core-contract pointer line now mentions the "every built
|
|
192
|
+
page is valid HTML" requirement; each documents its stack's real mechanism
|
|
193
|
+
(`whs html` / `html-validate` respectively) and its place in `#the-gate`'s
|
|
194
|
+
now-seven-step order.
|
|
195
|
+
|
|
196
|
+
### stacks/phoenix.md
|
|
197
|
+
|
|
198
|
+
Documented as a **`TODO`** in `#rendering`/`#the-gate` — no template change,
|
|
199
|
+
no tooling exists yet. The candidate approach is recorded (a ConnCase/
|
|
200
|
+
LiveView test fetching each sitemap route through html-validate or the Nu
|
|
201
|
+
Html Checker, wired as a `mix html_validity` alias). The `mix check`
|
|
202
|
+
placeholder-step snippet's order is also corrected while touching this
|
|
203
|
+
section: it previously listed `a11y` before `links.check`, ahead of where
|
|
204
|
+
`#the-gate`'s HTML-validity → links → a11y order calls for; it's now
|
|
205
|
+
`html_validity → links.check → a11y`, fixing that pre-existing drift as well
|
|
206
|
+
as adding the new placeholder step.
|
|
207
|
+
|
|
208
|
+
**Gaps, not fixed by this change:** Phoenix has no HTML-validity tooling —
|
|
209
|
+
`TODO`, same pattern as its existing `a11y`/`links.check` gaps. SvelteKit's
|
|
210
|
+
template has no 404/error page today, so nothing exists yet for its new
|
|
211
|
+
`html` step to check on that front.
|
|
212
|
+
|
|
213
|
+
**Adopter action (sveltekit):** `npm install` (picks up `html-validate` +
|
|
214
|
+
`.htmlvalidate.json`), add `"html": "html-validate \"build/**/*.html\""` to
|
|
215
|
+
scripts and into `check` after `build`, re-pin `standard-version`.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
24
219
|
## 2026-10-07 [breaking]
|
|
25
220
|
|
|
26
221
|
### core: a11y
|
package/standard/core.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Web project house style — CORE (stack-agnostic)
|
|
2
2
|
|
|
3
|
-
**Version:** 2026-10-
|
|
3
|
+
**Version:** 2026-10-09 · **Status:** active
|
|
4
4
|
|
|
5
5
|
This is the stack-agnostic contract every web project follows, regardless of
|
|
6
6
|
framework, host, or CSS system. It says **what** must be true, with concrete
|
|
@@ -173,7 +173,8 @@ validation status.
|
|
|
173
173
|
|
|
174
174
|
**Contract.** Core content and navigation work with no client JavaScript. Client
|
|
175
175
|
JS enhances; it never gates the essentials. Any logic that renders on both the
|
|
176
|
-
server and the client is written once.
|
|
176
|
+
server and the client is written once. Every built page is valid HTML per the
|
|
177
|
+
WHATWG HTML Living Standard.
|
|
177
178
|
|
|
178
179
|
**Specs.**
|
|
179
180
|
- The server-rendered (or prerendered) HTML is complete: a crawler or a
|
|
@@ -186,9 +187,17 @@ server and the client is written once.
|
|
|
186
187
|
- Nothing that matters (core content, primary CTA) blocks on a network fetch.
|
|
187
188
|
Live data arrives after first paint and degrades to a clear fallback state
|
|
188
189
|
(see `#third-parties`).
|
|
190
|
+
- Every built page — including error pages and `/audit/` — is valid HTML per
|
|
191
|
+
the **WHATWG HTML Living Standard** (the spec W3C itself defers to): no stray
|
|
192
|
+
end tags, no bad nesting, no duplicate `id`s. Browsers repair malformed
|
|
193
|
+
markup silently, so invalid structure otherwise ships unseen; WCAG 2.2
|
|
194
|
+
dropped its 4.1.1 Parsing criterion, so an axe scan no longer catches it
|
|
195
|
+
either. The check is spec conformance only — no style opinions (DOCTYPE
|
|
196
|
+
case, attribute quoting, and the like).
|
|
189
197
|
|
|
190
198
|
**Verify.** Load a key page with JS disabled: content and links present.
|
|
191
|
-
Grep for duplicated render logic.
|
|
199
|
+
Grep for duplicated render logic. An HTML validator runs over every built
|
|
200
|
+
page — including error pages and `/audit/` — in the gate.
|
|
192
201
|
|
|
193
202
|
**Implementation.** See your stack's impl doc, same slug.
|
|
194
203
|
|
|
@@ -651,6 +660,13 @@ generated from the origin manifest, never hand-written.
|
|
|
651
660
|
- **Display advertising (`#ads`)** always requires a provider-certified CMP and
|
|
652
661
|
always sets cookies — it is never the privacy-first path. A site that runs ads
|
|
653
662
|
has a consent banner by necessity.
|
|
663
|
+
- **Global Privacy Control.** A site running display ads or analytics that
|
|
664
|
+
sells/shares personal data honours the GPC signal — `navigator.globalPrivacyControl`
|
|
665
|
+
in the browser, or the `Sec-GPC: 1` request header where a server renders —
|
|
666
|
+
as an automatic opt-out of sale, sharing, and targeted advertising: no click
|
|
667
|
+
required, and sharing analytics does not load for a GPC visitor. The privacy
|
|
668
|
+
page states that GPC is honoured. Required by US state privacy laws, which
|
|
669
|
+
apply by where the visitor lives, not the site's own location.
|
|
654
670
|
- The privacy page renders from the origin manifest (`#third-parties`) — the
|
|
655
671
|
list of who sees visitor data and why. No hand-written prose enumerating
|
|
656
672
|
services that goes stale when a dependency changes.
|
|
@@ -666,7 +682,8 @@ generated from the origin manifest, never hand-written.
|
|
|
666
682
|
use.
|
|
667
683
|
|
|
668
684
|
**Verify.** The privacy page's third-party list is generated. Review confirms
|
|
669
|
-
any analytics is privacy-first or gated behind a CMP
|
|
685
|
+
any analytics is privacy-first or gated behind a CMP, and that the privacy
|
|
686
|
+
page states GPC is honoured wherever it applies.
|
|
670
687
|
|
|
671
688
|
**Implementation.** See your stack's impl doc, same slug.
|
|
672
689
|
|
|
@@ -690,7 +707,11 @@ ad directives, and documented.
|
|
|
690
707
|
non-personalized only where the provider supports it). Whether the prompt
|
|
691
708
|
shows to every visitor or only to visitors in regulated regions is a project
|
|
692
709
|
decision recorded in `CLAUDE.md`. What is **non-compliant** regardless: the ad
|
|
693
|
-
loader firing and setting cookies before the CMP has resolved.
|
|
710
|
+
loader firing and setting cookies before the CMP has resolved. The same gate
|
|
711
|
+
extends to the US-state opt-out (`#privacy`): a visitor sending GPC gets
|
|
712
|
+
restricted / non-personalized ad requests automatically, and the certified
|
|
713
|
+
CMP also presents the US-state "Do Not Sell or Share My Personal
|
|
714
|
+
Information" opt-out.
|
|
694
715
|
- **Declared (`#third-parties`).** Every ad-provider origin (loader, ad-serving,
|
|
695
716
|
ad iframes, the CMP, quality/verification endpoints) is in the origin manifest
|
|
696
717
|
with `purpose: advertising` and its `dataSeen`. The privacy page renders it
|
|
@@ -741,8 +762,9 @@ the declared ad origins; any `'unsafe-*'` exception noted; the loader gated on
|
|
|
741
762
|
link/reference check (`#internal-links`) fails on any
|
|
742
763
|
`adsbygoogle` / ad-provider reference in a non-production build and on a missing
|
|
743
764
|
`ads.txt` in the production output. Manually, in a fresh browser on production:
|
|
744
|
-
decline consent → no ad requests in the network panel; accept → ads load;
|
|
745
|
-
|
|
765
|
+
decline consent → no ad requests in the network panel; accept → ads load; with
|
|
766
|
+
GPC on (`navigator.globalPrivacyControl`) → ad requests are restricted /
|
|
767
|
+
non-personalized; check CLS in the browser's own tools.
|
|
746
768
|
|
|
747
769
|
**Implementation.** See your stack's impl doc, same slug.
|
|
748
770
|
|
|
@@ -886,8 +908,9 @@ enforcement point runs that same command — never a hand-picked subset.
|
|
|
886
908
|
|
|
887
909
|
**Specs.**
|
|
888
910
|
- One gate command runs, ordered cheapest-first: lint/format check → content
|
|
889
|
-
validation → domain tests → build →
|
|
890
|
-
(Optionally a performance budget once the site has a
|
|
911
|
+
validation → domain tests → build → HTML validity → link/reference
|
|
912
|
+
integrity → a11y scan. (Optionally a performance budget once the site has a
|
|
913
|
+
baseline.)
|
|
891
914
|
- The same command is invoked by: the pre-push git hook, the CI workflow, the
|
|
892
915
|
deploy script, and the assistant before it reports work complete.
|
|
893
916
|
- There is **no "CI-only" variant** and no partial local run. If the gate
|
|
@@ -1470,7 +1493,7 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
|
|
|
1470
1493
|
- production-url: https://example.com
|
|
1471
1494
|
- content-type: tool # tool | article (article => feeds)
|
|
1472
1495
|
- publishing-rate: ~5 pages/week
|
|
1473
|
-
- standard-version: 2026-10-
|
|
1496
|
+
- standard-version: 2026-10-09 # recommended — enables standard-version drift tracking (#compliance)
|
|
1474
1497
|
```
|
|
1475
1498
|
|
|
1476
1499
|
If the section is absent, the assistant's first action is to run the flow above
|