@piercebarney/whs-eleventy 2026.10.6 → 2026.10.8

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 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 { status: PASS, note: "server-rendered HTML with real content; 404 page present" };
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: () => {
@@ -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 no a11y
3
- // (that's backstopped by `npm run check` in bin/deploy before anything ships).
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.6",
3
+ "version": "2026.10.8",
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
  },
@@ -21,6 +21,142 @@ enforced, new *opt-in* chapters/capabilities, tooling-only changes — is
21
21
 
22
22
  ---
23
23
 
24
+ ## 2026-10-08 [breaking]
25
+
26
+ ### core: rendering
27
+
28
+ `#rendering`'s Contract never required HTML *validity* — only that content
29
+ render without client JS and that server/client rendering logic not be
30
+ duplicated. It now adds: every built page — including error pages and
31
+ `/audit/` — must be valid HTML per the **WHATWG HTML Living Standard** (the
32
+ spec W3C itself defers to). Spec conformance only (no stray end tags, no bad
33
+ nesting, no duplicate `id`s) — no style opinions such as DOCTYPE case or
34
+ attribute quoting. Rationale: browsers repair malformed markup silently, so
35
+ invalid structure otherwise ships unseen, and WCAG 2.2 dropped its 4.1.1
36
+ Parsing criterion, so an axe scan no longer catches it either. Decided by
37
+ the owner in `feedback/2026-09-26-external-standards-survey.md`
38
+ ("Sequencing" step 2, row 3).
39
+
40
+ ### core: the-gate
41
+
42
+ `#the-gate`'s drift-guarded stage order gains an **HTML validity** step,
43
+ placed after `build` and before `link/reference integrity`: lint/format
44
+ check → content validation → domain tests → build → HTML validity →
45
+ link/reference integrity → a11y scan.
46
+
47
+ **Breaking:** a project that was fully compliant before this change (a gate
48
+ that ran build → links → a11y with no HTML-validity step) is no longer
49
+ compliant after it — `#the-gate` now requires an HTML-validity stage that
50
+ didn't exist as a requirement before, and `whs compliance` / `whs doctor`
51
+ (see `packages/whs-eleventy` below) now `FAIL`/warn on its absence rather
52
+ than silently passing.
53
+
54
+ **Adopter action (eleventy):** `npm update @piercebarney/whs-eleventy`, add
55
+ `"html": "whs html"` to `package.json` scripts, add `npm run html` to
56
+ `check` (after `build`, before `links`), then fix any invalid markup the new
57
+ check reports. Re-pin `standard-version` to `2026-10-08` once done.
58
+
59
+ ### packages/whs-eleventy
60
+
61
+ New `whs html` command (`lib/html.js`): walks every `*.html` file under the
62
+ built `_site/` recursively (error pages and `/audit/` included, not just the
63
+ sitemap) and runs it through `html-validate`'s `html-validate:standard`
64
+ preset (spec conformance only — not `recommended`'s style opinions), exiting
65
+ non-zero on any error. New dependency `html-validate ^11.16.0`. `whs
66
+ compliance`'s `#rendering` check now `FAIL`s if the project's `check` script
67
+ doesn't reference an `html` stage. `whs doctor`'s `GATE_STAGES` list and its
68
+ `gate-stages` check gain `html`. `whs content-check`'s lighter slice
69
+ (`lint → validate → build → links`) gains `html`, run before `links`. Tests
70
+ added for the new compliance/content-check behavior.
71
+
72
+ ### templates/eleventy-netlify
73
+
74
+ Wires the new stage: `package.json` gains an `"html": "whs html"` script and
75
+ inserts `npm run html` into `check` between `build` and `links`; `ci.yml`'s
76
+ inline gate-order comment updated to match. `CLAUDE.md`, `README.md`, and
77
+ `CONTENT.md` document the new stage in their gate-order prose.
78
+ `standard-version` re-pinned to `2026-10-08`.
79
+
80
+ ### templates/sveltekit-netlify
81
+
82
+ Implements the chapter for the first time (no prior HTML-validity tooling
83
+ existed for this stack): new `html-validate` devDependency, a
84
+ `.htmlvalidate.json` extending `html-validate:standard`, a new `"html":
85
+ "html-validate \"build/**/*.html\""` script run recursively over every
86
+ built page, and `npm run html` added to `check` after `build`. `README.md`,
87
+ `TEMPLATE.md`, and `ci.yml`'s inline gate-order comment updated to match.
88
+ This stack has no static error/404 page yet (`adapter-static`, no
89
+ `fallback`, no `+error.svelte`), so `core.md#rendering`'s "error pages"
90
+ clause has nothing to check today — the recursive glob will pick one up
91
+ automatically the day this binding adds one.
92
+
93
+ ### stacks/eleventy-netlify.md, stacks/sveltekit-netlify.md
94
+
95
+ Each `#rendering` Core-contract pointer line now mentions the "every built
96
+ page is valid HTML" requirement; each documents its stack's real mechanism
97
+ (`whs html` / `html-validate` respectively) and its place in `#the-gate`'s
98
+ now-seven-step order.
99
+
100
+ ### stacks/phoenix.md
101
+
102
+ Documented as a **`TODO`** in `#rendering`/`#the-gate` — no template change,
103
+ no tooling exists yet. The candidate approach is recorded (a ConnCase/
104
+ LiveView test fetching each sitemap route through html-validate or the Nu
105
+ Html Checker, wired as a `mix html_validity` alias). The `mix check`
106
+ placeholder-step snippet's order is also corrected while touching this
107
+ section: it previously listed `a11y` before `links.check`, ahead of where
108
+ `#the-gate`'s HTML-validity → links → a11y order calls for; it's now
109
+ `html_validity → links.check → a11y`, fixing that pre-existing drift as well
110
+ as adding the new placeholder step.
111
+
112
+ **Gaps, not fixed by this change:** Phoenix has no HTML-validity tooling —
113
+ `TODO`, same pattern as its existing `a11y`/`links.check` gaps. SvelteKit's
114
+ template has no 404/error page today, so nothing exists yet for its new
115
+ `html` step to check on that front.
116
+
117
+ **Adopter action (sveltekit):** `npm install` (picks up `html-validate` +
118
+ `.htmlvalidate.json`), add `"html": "html-validate \"build/**/*.html\""` to
119
+ scripts and into `check` after `build`, re-pin `standard-version`.
120
+
121
+ ---
122
+
123
+ ## 2026-10-07 [breaking]
124
+
125
+ ### core: a11y
126
+
127
+ `core.md#a11y`'s Contract never named a WCAG version/level as its
128
+ conformance target — only "WCAG AA" for contrast. It now declares **WCAG 2.1
129
+ Level AA** explicitly as the target, anchored to US law (the ADA Title II
130
+ rule and US web-accessibility litigation/settlements reference WCAG 2.1 AA;
131
+ Section 508's WCAG 2.0 AA is already covered by it) rather than WCAG 2.2 AA
132
+ — the upgrade trigger to 2.2 AA is recorded, not scheduled, for once US rules
133
+ reference it. **Breaking:** a project that was previously compliant by
134
+ meeting the checklist plus passing the automated scan must now conform to
135
+ all of WCAG 2.1 AA, including criteria the checklist and scan don't
136
+ individually name (e.g. captions, reflow, keyboard operability everywhere)
137
+ — a real added requirement, not just a clarification of an already-met bar.
138
+ No tooling change: Eleventy's `whs a11y` scan already runs `wcag2a`/
139
+ `wcag2aa`/`wcag21a`/`wcag21aa` — it already matched this target. SvelteKit's
140
+ and Phoenix's scan gaps (no automated a11y tooling yet) are pre-existing and
141
+ unrelated to this change. Decided by the owner in
142
+ `feedback/2026-09-26-external-standards-survey.md` ("Decisions (2026-09-26)"
143
+ item 1); see that file for the full WCAG-version rationale, including why
144
+ 2.1 and not 2.2.
145
+
146
+ ### core: styling
147
+
148
+ The contrast line in `#styling` now also says "WCAG 2.1 AA" (was "WCAG AA"),
149
+ matching the version now named in `#a11y`.
150
+
151
+ ### stacks/eleventy-netlify.md, stacks/phoenix.md, stacks/sveltekit-netlify.md
152
+
153
+ Each of `stacks/eleventy-netlify.md`, `stacks/phoenix.md`, and
154
+ `stacks/sveltekit-netlify.md`'s `#a11y` Core-contract pointer line now
155
+ mentions WCAG 2.1 AA alongside the existing checklist/scan pointer — no
156
+ change to any stack's own mechanism.
157
+
158
+ ---
159
+
24
160
  ## 2026-10-06 [breaking]
25
161
 
26
162
  ### packages/whs-eleventy
package/standard/core.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Web project house style — CORE (stack-agnostic)
2
2
 
3
- **Version:** 2026-10-06 · **Status:** active
3
+ **Version:** 2026-10-08 · **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
 
@@ -244,7 +253,7 @@ contrast-correct at the token level.
244
253
  - **Strict-CSP-safe:** no `style-src 'unsafe-inline'`, no inline `style=`
245
254
  attributes in templates, no per-page `<style>` blocks. Utility-class systems
246
255
  (Tailwind) are fine; inline style strings are not.
247
- - Contrast meets WCAG AA. When a pair fails, fix the **token**, not the usage.
256
+ - Contrast meets WCAG 2.1 AA. When a pair fails, fix the **token**, not the usage.
248
257
  - Theme is applied via a data attribute set before first paint (`#theme`).
249
258
  - A second stylesheet only for a genuinely separable widget, justified in a
250
259
  comment.
@@ -834,8 +843,11 @@ states.
834
843
 
835
844
  ## a11y — Accessibility checklist
836
845
 
837
- **Contract.** Every page meets the checklist below. An automated a11y scan over
838
- every page runs in the gate — and is understood to catch regressions, not
846
+ **Contract.** Every page conforms to **WCAG 2.1 Level AA** — the level US
847
+ accessibility law references (the ADA Title II rule; Section 508's WCAG 2.0 AA
848
+ is covered by it). The checklist below is the house's concrete mechanism for
849
+ the parts of AA most often missed, not the whole of it. An automated a11y scan
850
+ over every page runs in the gate — and is understood to catch regressions, not
839
851
  everything.
840
852
 
841
853
  **Specs.**
@@ -858,7 +870,7 @@ everything.
858
870
  keyboard button — don't re-bind Enter/Space or add `role`/`tabindex`.
859
871
  - **Icons:** decorative → `aria-hidden="true"`; meaningful → `<title>` in the
860
872
  SVG or `aria-label` on the control.
861
- - **Contrast** meets WCAG AA; fix failures at the token (`#brand-source`), not
873
+ - **Contrast** meets WCAG 2.1 AA; fix failures at the token (`#brand-source`), not
862
874
  with a one-off override.
863
875
  - **Focus visibility:** a visible focus ring on every interactive element; if
864
876
  you override the default, keep one.
@@ -883,8 +895,9 @@ enforcement point runs that same command — never a hand-picked subset.
883
895
 
884
896
  **Specs.**
885
897
  - One gate command runs, ordered cheapest-first: lint/format check → content
886
- validation → domain tests → build → link/reference integrity → a11y scan.
887
- (Optionally a performance budget once the site has a baseline.)
898
+ validation → domain tests → build → HTML validity → link/reference
899
+ integrity → a11y scan. (Optionally a performance budget once the site has a
900
+ baseline.)
888
901
  - The same command is invoked by: the pre-push git hook, the CI workflow, the
889
902
  deploy script, and the assistant before it reports work complete.
890
903
  - There is **no "CI-only" variant** and no partial local run. If the gate
@@ -1467,7 +1480,7 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
1467
1480
  - production-url: https://example.com
1468
1481
  - content-type: tool # tool | article (article => feeds)
1469
1482
  - publishing-rate: ~5 pages/week
1470
- - standard-version: 2026-10-06 # recommended — enables standard-version drift tracking (#compliance)
1483
+ - standard-version: 2026-10-08 # recommended — enables standard-version drift tracking (#compliance)
1471
1484
  ```
1472
1485
 
1473
1486
  If the section is absent, the assistant's first action is to run the flow above