@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 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
- return {
584
- status: MANUAL,
585
- note: "wired; verify on production: decline consent => no ad requests, accept => ads + CLS ok",
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.8",
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"
@@ -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-08 · **Status:** active
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; check
754
- CLS in the browser's own tools.
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-08 # recommended — enables standard-version drift tracking (#compliance)
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