@piercebarney/whs-eleventy 2026.9.22 → 2026.9.24

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.
@@ -14,20 +14,32 @@
14
14
  const fs = require("node:fs");
15
15
  const path = require("node:path");
16
16
  const { parse } = require("node-html-parser");
17
- const { projectRequire, tryProjectRequire } = require("./_project.js");
17
+ const { tryProjectRequire } = require("./_project.js");
18
18
 
19
19
  const SITE = "_site";
20
- const site = projectRequire("src/_data/site.js");
21
- const thirdparties = projectRequire("src/_data/thirdparties.js");
20
+ const site = tryProjectRequire("src/_data/site.js", null);
21
+ const thirdparties = tryProjectRequire("src/_data/thirdparties.js", null);
22
+
23
+ const errors = [];
24
+ const err = (file, msg) => errors.push(`${file}: ${msg}`);
25
+
26
+ // Reported exactly once, here — every origin-dependent check below skips
27
+ // silently on a missing site/thirdparties, relying on these two lines to
28
+ // explain why, rather than one err() per guarded call site.
29
+ if (!site) err("src/_data/site.js", "missing — cannot verify links against the site's own origin");
30
+ if (!thirdparties) {
31
+ err("src/_data/thirdparties.js", "missing — cannot verify declared third-party origins");
32
+ }
33
+ const canCheckOrigins = !!(site && thirdparties);
22
34
 
23
35
  // The /glossary/#slug check runs only when add-ons/glossary/ is applied
24
36
  // (it ships src/_data/glossary.js). No file → no glossary → the check is inert.
25
37
  const glossary = tryProjectRequire("src/_data/glossary.js");
26
38
  const glossarySlugs = new Set(glossary ? glossary.map((t) => t.slug) : []);
27
39
  const declaredOrigins = thirdparties
28
- .flatMap((t) => [t.origin, ...(t.cspOrigins || [])])
29
- .filter(Boolean);
30
- const allowedOrigins = new Set([site.url, ...declaredOrigins]);
40
+ ? thirdparties.flatMap((t) => [t.origin, ...(t.cspOrigins || [])]).filter(Boolean)
41
+ : [];
42
+ const allowedOrigins = new Set([...(site ? [site.url] : []), ...declaredOrigins]);
31
43
 
32
44
  // Privacy/ad opt-out destinations that policy prose links to, plus the
33
45
  // search-engine webmaster consoles the /audit/ page links to. All are
@@ -55,9 +67,6 @@ const REQUIRED_LD = {
55
67
  Article: ["headline"],
56
68
  };
57
69
 
58
- const errors = [];
59
- const err = (file, msg) => errors.push(`${file}: ${msg}`);
60
-
61
70
  // ---- CSP <-> origin manifest agreement -------------------------------------
62
71
  (function checkCsp() {
63
72
  let toml;
@@ -78,6 +87,7 @@ const err = (file, msg) => errors.push(`${file}: ${msg}`);
78
87
  }
79
88
  }),
80
89
  );
90
+ if (!thirdparties) return; // reported once, above, when the load failed
81
91
  // Every origin the CSP grants must be a declared party — matched against each
82
92
  // party's `origin` or its `cspOrigins` list. (The reverse isn't required: a
83
93
  // host / form processor is in thirdparties.js but is not a browser-resource
@@ -125,13 +135,14 @@ function outputHas(urlPath) {
125
135
  return fs.existsSync(asFile + "/index.html") || fs.existsSync(asFile + ".html");
126
136
  }
127
137
 
128
- function checkRef(file, pageUrl, raw, { external = true, isHyperlink = false } = {}) {
138
+ function checkRef(file, pageUrl, resolveBase, raw, { external = true, isHyperlink = false } = {}) {
129
139
  if (!raw) return;
130
140
  const v = raw.trim();
131
141
  if (v === "" || v.startsWith("data:") || v.startsWith("mailto:") || v.startsWith("tel:")) return;
132
142
 
133
143
  if (/^https?:\/\//i.test(v)) {
134
144
  if (!external) return;
145
+ if (!canCheckOrigins) return; // reported once, above, when the load failed
135
146
  let origin;
136
147
  try {
137
148
  origin = new URL(v).origin;
@@ -174,13 +185,15 @@ function checkRef(file, pageUrl, raw, { external = true, isHyperlink = false } =
174
185
  return;
175
186
  }
176
187
  // relative path — resolve against the page dir
177
- const resolved = path.posix.join(pageUrl, v);
188
+ const resolved = path.posix.join(resolveBase, v);
178
189
  if (!outputHas(resolved)) err(file, `relative reference does not resolve: ${v}`);
179
190
  }
180
191
 
181
192
  for (const f of htmlFiles) {
182
193
  const rel = "/" + path.relative(SITE, f).replace(/\\/g, "/");
183
194
  const pageUrl = rel.replace(/index\.html$/, "");
195
+ const dir = path.posix.dirname(rel);
196
+ const resolveBase = dir === "/" ? "/" : dir + "/";
184
197
  const root = parse(fs.readFileSync(f, "utf8"));
185
198
 
186
199
  // CSP-safety: the strict CSP (script-src 'self'; style-src 'self') rejects
@@ -205,7 +218,7 @@ for (const f of htmlFiles) {
205
218
 
206
219
  root.querySelectorAll("a[href]").forEach((a) => {
207
220
  const href = a.getAttribute("href");
208
- checkRef(f, pageUrl, href, { isHyperlink: true });
221
+ checkRef(f, pageUrl, resolveBase, href, { isHyperlink: true });
209
222
  if (/^https?:\/\//i.test(href || "")) {
210
223
  let origin;
211
224
  try {
@@ -213,7 +226,7 @@ for (const f of htmlFiles) {
213
226
  } catch {
214
227
  origin = "";
215
228
  }
216
- if (origin && origin !== site.url) {
229
+ if (canCheckOrigins && origin && origin !== site.url) {
217
230
  const rel2 = (a.getAttribute("rel") || "").toLowerCase();
218
231
  if (!/\bnoopener\b|\bnoreferrer\b/.test(rel2)) {
219
232
  err(f, `external <a> without rel="noopener": ${href}`);
@@ -222,12 +235,18 @@ for (const f of htmlFiles) {
222
235
  }
223
236
  });
224
237
 
225
- root.querySelectorAll("link[href]").forEach((l) => checkRef(f, pageUrl, l.getAttribute("href")));
226
- root.querySelectorAll("script[src]").forEach((s) => checkRef(f, pageUrl, s.getAttribute("src")));
227
- root.querySelectorAll("img[src]").forEach((i) => checkRef(f, pageUrl, i.getAttribute("src")));
238
+ root
239
+ .querySelectorAll("link[href]")
240
+ .forEach((l) => checkRef(f, pageUrl, resolveBase, l.getAttribute("href")));
241
+ root
242
+ .querySelectorAll("script[src]")
243
+ .forEach((s) => checkRef(f, pageUrl, resolveBase, s.getAttribute("src")));
244
+ root
245
+ .querySelectorAll("img[src]")
246
+ .forEach((i) => checkRef(f, pageUrl, resolveBase, i.getAttribute("src")));
228
247
  root
229
248
  .querySelectorAll('meta[property="og:image"], meta[name="twitter:image"]')
230
- .forEach((m) => checkRef(f, pageUrl, m.getAttribute("content")));
249
+ .forEach((m) => checkRef(f, pageUrl, resolveBase, m.getAttribute("content")));
231
250
 
232
251
  root.querySelectorAll('script[type="application/ld+json"]').forEach((s, i) => {
233
252
  let data;
@@ -256,6 +275,7 @@ for (const f of htmlFiles) {
256
275
  // dynamically-built URL), but it catches the common case: a hardcoded
257
276
  // external origin in client JS that was never added to the manifest.
258
277
  (function checkJsOrigins() {
278
+ if (!canCheckOrigins) return; // reported once, above, when the load failed
259
279
  const jsFiles = allFiles.filter((f) => f.endsWith(".js") && !f.endsWith("links-report.json"));
260
280
  const ORIGIN_LITERAL = /(?:fetch|new\s+URL)\s*\(\s*[`'"](https?:\/\/[^`'"\s]+)[`'"]/g;
261
281
  for (const f of jsFiles) {
package/lib/compliance.js CHANGED
@@ -198,6 +198,13 @@ const CHECKS = {
198
198
  },
199
199
 
200
200
  styling: () => {
201
+ const njkRoots = ["src", "static", "scripts"].map((d) => path.join(ROOT, d));
202
+ const anyNjk = njkRoots.some((r) => walk(r).some((f) => f.endsWith(".njk")));
203
+ if (!anyNjk)
204
+ return {
205
+ status: MANUAL,
206
+ note: "no .njk files found — can't verify against a non-.njk layout",
207
+ };
201
208
  const shift = grepSrc(/style=(["']).*?\1/, [".njk"]);
202
209
  return shift.length
203
210
  ? { status: FAIL, note: `inline style= in ${shift.join(", ")}` }
@@ -350,8 +357,14 @@ const CHECKS = {
350
357
  (f) =>
351
358
  /\.(css|js|njk)$/.test(f) && f !== "src/_data/brand.js" && f !== "src/tokens.css.njk",
352
359
  );
360
+ const existing = targets.filter((rel) => has(rel));
361
+ if (existing.length === 0)
362
+ return {
363
+ status: MANUAL,
364
+ note: "no static/style.css, scripts/og-card.js|icons.js, or src/ found — can't verify against a non-standard-layout project",
365
+ };
353
366
  const bad = [];
354
- for (const rel of targets) {
367
+ for (const rel of existing) {
355
368
  const t = read(rel);
356
369
  if (!t) continue;
357
370
  for (const line of t.split("\n")) {
@@ -489,6 +502,11 @@ const CHECKS = {
489
502
  },
490
503
 
491
504
  "domain-tests": () => {
505
+ if (!has("static/calc"))
506
+ return {
507
+ status: MANUAL,
508
+ note: "no static/calc/ — can't verify against a non-standard-layout project",
509
+ };
492
510
  const mods = walk(path.join(ROOT, "static/calc"))
493
511
  .filter((f) => f.endsWith(".js"))
494
512
  .map((f) => path.basename(f, ".js"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piercebarney/whs-eleventy",
3
- "version": "2026.9.22",
3
+ "version": "2026.9.24",
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"
@@ -21,6 +21,86 @@ enforced, new *opt-in* chapters/capabilities, tooling-only changes — is
21
21
 
22
22
  ---
23
23
 
24
+ ## 2026-09-24 [non-breaking]
25
+
26
+ ### packages/whs-eleventy
27
+
28
+ `check-links.js` resolved a relative reference (`favicon.svg`, `styles.css`,
29
+ etc.) against `pageUrl`, a literal `index.html`-stripped path — correct for
30
+ directory-style permalinks (`/foo/index.html` → `/foo/`) but wrong for a
31
+ flat, non-directory permalink like `privacy.html`, where `pageUrl` stayed
32
+ `/privacy.html` and `path.posix.join` then treated it as if it were a
33
+ directory, producing false `relative reference does not resolve` findings
34
+ for files that genuinely exist and resolve correctly in a real browser.
35
+ `pageUrl` is also used for same-page `#anchor` lookups, where the literal
36
+ value is exactly what's needed — so the fix doesn't touch `pageUrl` itself.
37
+ Instead `checkRef()` now takes a separate `resolveBase` (the directory
38
+ containing the page, via `path.posix.dirname`) used only for relative-
39
+ reference resolution, leaving anchor lookups untouched. Found while
40
+ covenant-in-color's adoption session ran the newly-fixed `npm run links` for
41
+ the first time and filed as
42
+ `feedback/2026-09-23-check-links-relative-resolution-non-directory-permalink.md`;
43
+ fixed in `7edbc68`.
44
+
45
+ This is the fourth `packages/whs-eleventy`-only fix shipped this week (the
46
+ third, 2026.9.23, already published and live) — `core.md`, `stacks/*.md`,
47
+ and `index.json` content are unchanged; only the doc pins and
48
+ `packages/whs-eleventy/package.json` (now `2026.9.24`) moved.
49
+
50
+ ---
51
+
52
+ ## 2026-09-23 — Republish 2026-09-22-2 as a clean bare-date version [non-breaking]
53
+
54
+ ### packages/whs-eleventy
55
+
56
+ No new content beyond what's already documented below. This bump republishes
57
+ everything already documented under the 2026-09-22-2 entry (the
58
+ `check-links.js` no-`src/`-layout crash fix) as a clean bare-date version,
59
+ specifically to trigger the npm publish of `@piercebarney/whs-eleventy` that
60
+ the 2026-09-22-2 same-day-suffix mechanism deliberately does not trigger on
61
+ its own (per that suffix's own documented exception in `CLAUDE.md`'s
62
+ Versions section: a suffixed pin gets no git tag and no npm publish).
63
+
64
+ This release also newly documents a second, separately-landed fix shipping
65
+ in the same publish: `styling`, `brand-source`, and `domain-tests` in
66
+ `lib/compliance.js` each built their scan-target list from hardcoded
67
+ standard-layout paths and treated "found zero files to scan" as "found zero
68
+ violations" — a false `PASS` on a project whose layout doesn't match the
69
+ standard yet, rather than the `MANUAL`/can't-verify result the other checks
70
+ correctly report in the same situation. Found on a real adoption and filed
71
+ as `feedback/2026-09-22-compliance-silent-pass-on-nonstandard-layout.md`;
72
+ fixed in `96aeac2` by checking whether each check's target(s) exist at all
73
+ before scanning — an existing-but-empty target (e.g. `static/calc/` with no
74
+ modules yet) still correctly reports `PASS`.
75
+
76
+ `core.md`, `stacks/*.md`, `index.json`, and every `standard-version:` pin
77
+ move from `2026-09-22-2` to `2026-09-23`; `packages/whs-eleventy/package.json`
78
+ moves to the corresponding npm version, `2026.9.23`. No `core.md` chapter,
79
+ `stacks/*.md`, or `index.json` content changed.
80
+
81
+ ---
82
+
83
+ ## 2026-09-22-2 [non-breaking]
84
+
85
+ ### packages/whs-eleventy
86
+
87
+ `check-links.js` had the same crash class as `doctor.js`'s already-fixed
88
+ `MODULE_NOT_FOUND`: an unconditional module-top-level
89
+ `projectRequire("src/_data/site.js")` (and `thirdparties.js`) that throws on
90
+ a project with no `src/` tree, instead of reporting the missing layout as a
91
+ finding — found while reconciling a real adoption's migration backlog and
92
+ filed as `feedback/2026-09-22-check-links-same-crash-unfixed.md`. Fixed in
93
+ `e73de7d`: the two missing-file conditions are now reported exactly once,
94
+ and each dependent origin-validation check (internal-link/anchor/asset/
95
+ JSON-LD) skips silently rather than crashing or false-positiving. This is a
96
+ same-day suffix release — per `CLAUDE.md`'s Versions section it deliberately
97
+ does **not** get a git tag or npm publish; `packages/whs-eleventy/package.json`
98
+ stays at `2026.9.22`, which does not yet include this fix, until a future
99
+ bare-date promotion release republishes it. No `core.md` chapter,
100
+ `stacks/*.md`, or `index.json` content changed.
101
+
102
+ ---
103
+
24
104
  ## 2026-09-22 [non-breaking]
25
105
 
26
106
  ### 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-09-22 · **Status:** active
3
+ **Version:** 2026-09-24 · **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
@@ -1467,7 +1467,7 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
1467
1467
  - production-url: https://example.com
1468
1468
  - content-type: tool # tool | article (article => feeds)
1469
1469
  - publishing-rate: ~5 pages/week
1470
- - standard-version: 2026-09-22 # recommended — enables standard-version drift tracking (#compliance)
1470
+ - standard-version: 2026-09-24 # recommended — enables standard-version drift tracking (#compliance)
1471
1471
  ```
1472
1472
 
1473
1473
  If the section is absent, the assistant's first action is to run the flow above