@piercebarney/whs-eleventy 2026.10.8 → 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/lib/compliance.js +27 -4
- package/package.json +1 -1
- package/standard/CHANGELOG.md +96 -0
- package/standard/core.md +19 -6
package/lib/compliance.js
CHANGED
|
@@ -525,6 +525,15 @@ const CHECKS = {
|
|
|
525
525
|
/["']slug["']\s*:\s*["']privacy["']/.test(read("src/content/pages.json") || "");
|
|
526
526
|
if (!builtPath && !source) return { status: FAIL, note: "no privacy page" };
|
|
527
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
|
+
|
|
528
537
|
// Prefer checking the built page's own rendered HTML for every declared
|
|
529
538
|
// third party's name — what a user actually sees, independent of which
|
|
530
539
|
// source file or templating approach produced it. Falls back to the
|
|
@@ -580,10 +589,24 @@ const CHECKS = {
|
|
|
580
589
|
const leak = siteHtml().filter((f) => AD_CODE.test(fs.readFileSync(f, "utf8")));
|
|
581
590
|
if (!prod && leak.length)
|
|
582
591
|
return { status: FAIL, note: `ad code in ${leak.length} non-production page(s)` };
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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 };
|
|
587
610
|
},
|
|
588
611
|
|
|
589
612
|
"internal-links": () => {
|
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"
|
package/standard/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,102 @@ 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
|
+
|
|
24
120
|
## 2026-10-08 [breaking]
|
|
25
121
|
|
|
26
122
|
### core: rendering
|
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
|
|
@@ -660,6 +660,13 @@ generated from the origin manifest, never hand-written.
|
|
|
660
660
|
- **Display advertising (`#ads`)** always requires a provider-certified CMP and
|
|
661
661
|
always sets cookies — it is never the privacy-first path. A site that runs ads
|
|
662
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.
|
|
663
670
|
- The privacy page renders from the origin manifest (`#third-parties`) — the
|
|
664
671
|
list of who sees visitor data and why. No hand-written prose enumerating
|
|
665
672
|
services that goes stale when a dependency changes.
|
|
@@ -675,7 +682,8 @@ generated from the origin manifest, never hand-written.
|
|
|
675
682
|
use.
|
|
676
683
|
|
|
677
684
|
**Verify.** The privacy page's third-party list is generated. Review confirms
|
|
678
|
-
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.
|
|
679
687
|
|
|
680
688
|
**Implementation.** See your stack's impl doc, same slug.
|
|
681
689
|
|
|
@@ -699,7 +707,11 @@ ad directives, and documented.
|
|
|
699
707
|
non-personalized only where the provider supports it). Whether the prompt
|
|
700
708
|
shows to every visitor or only to visitors in regulated regions is a project
|
|
701
709
|
decision recorded in `CLAUDE.md`. What is **non-compliant** regardless: the ad
|
|
702
|
-
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.
|
|
703
715
|
- **Declared (`#third-parties`).** Every ad-provider origin (loader, ad-serving,
|
|
704
716
|
ad iframes, the CMP, quality/verification endpoints) is in the origin manifest
|
|
705
717
|
with `purpose: advertising` and its `dataSeen`. The privacy page renders it
|
|
@@ -750,8 +762,9 @@ the declared ad origins; any `'unsafe-*'` exception noted; the loader gated on
|
|
|
750
762
|
link/reference check (`#internal-links`) fails on any
|
|
751
763
|
`adsbygoogle` / ad-provider reference in a non-production build and on a missing
|
|
752
764
|
`ads.txt` in the production output. Manually, in a fresh browser on production:
|
|
753
|
-
decline consent → no ad requests in the network panel; accept → ads load;
|
|
754
|
-
|
|
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.
|
|
755
768
|
|
|
756
769
|
**Implementation.** See your stack's impl doc, same slug.
|
|
757
770
|
|
|
@@ -1480,7 +1493,7 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
|
|
|
1480
1493
|
- production-url: https://example.com
|
|
1481
1494
|
- content-type: tool # tool | article (article => feeds)
|
|
1482
1495
|
- publishing-rate: ~5 pages/week
|
|
1483
|
-
- standard-version: 2026-10-
|
|
1496
|
+
- standard-version: 2026-10-09 # recommended — enables standard-version drift tracking (#compliance)
|
|
1484
1497
|
```
|
|
1485
1498
|
|
|
1486
1499
|
If the section is absent, the assistant's first action is to run the flow above
|