Skip to content

Commit 4d45741

Browse files
feat: add GA4 via Cookiebot consent on every page
Cookiebot loads first (its own install guide: the very first script in <head>), then a Consent Mode v2 default-consent snippet, then the Google tag — Cookiebot's own Consent Mode guide requires that default snippet and that it precede gtag.js, so a consent decision (denied by default) reaches Google before its tag can fire. The block is byte-identical on index.html and every generated kb/ page (index.html is the source of truth, copied into generate-kb.js's head template). kb-generator.test.js's "no script but JSON-LD" check is updated to allow this fixed, known block instead of forbidding all script. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent b171613 commit 4d45741

35 files changed

Lines changed: 1208 additions & 8 deletions

.development/CURRENT-STATUS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ Small, any time:
217217
- ~~**SEO: the content is not in the served HTML**~~ — closed by item 3 (2026-09-06):
218218
the knowledge base is static HTML with a `TechArticle` per page and a sitemap; what
219219
remains is Search Console (the property is still not shared with the MCP account).
220-
- **Google Fonts is the last third-party blocking request** — self-host JetBrains Mono. **S**
220+
- **Google Fonts is the last third-party *blocking* request** — self-host JetBrains Mono.
221+
The site also loads two async third parties now (Cookiebot, Google tag), which do not
222+
block rendering. **S**
221223
- ~~**Push + PR flow**~~ — decided 2026-09-04 and written into `workflow.md` (PR #17):
222224
a PR per branch, merged from GitHub, so the `headless` check is a gate. One caveat
223225
learned the hard way (PR #32): a chained PR whose base branch is merged first must be

.development/scripts/generate-kb.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,42 @@ function chrome({ file, title, description, fullDescription = description, headi
552552
<head>
553553
<meta charset="UTF-8">
554554
<meta name="viewport" content="width=device-width, initial-scale=1.0">
555+
556+
<!-- Cookiebot loads first so its consent decision exists before any Google
557+
script can read it, then the Consent Mode default state, then Google's
558+
tag. This block is byte-identical on every page (source of truth:
559+
index.html, copied into the generator template). -->
560+
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="11322285-cc73-4d07-a7e8-be34dc027c4e" type="text/javascript" async></script>
561+
<!-- Consent Mode v2 default state: Cookiebot's own inline-implementation guide
562+
says a default must be set manually and must precede gtag.js — this is
563+
that snippet, verbatim, from
564+
https://support.cookiebot.com/hc/en-us/articles/360016047000-Implementing-Google-Consent-Mode -->
565+
<script data-cookieconsent="ignore">
566+
window.dataLayer = window.dataLayer || [];
567+
function gtag(){dataLayer.push(arguments);}
568+
gtag('consent', 'default', {
569+
'ad_personalization': 'denied',
570+
'ad_storage': 'denied',
571+
'ad_user_data': 'denied',
572+
'analytics_storage': 'denied',
573+
'functionality_storage': 'denied',
574+
'personalization_storage': 'denied',
575+
'security_storage': 'granted',
576+
'wait_for_update': 500,
577+
});
578+
gtag('set', 'ads_data_redaction', true);
579+
gtag('set', 'url_passthrough', false);
580+
</script>
581+
<!-- Google tag (gtag.js) -->
582+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DQR5VQ6VXX"></script>
583+
<script>
584+
window.dataLayer = window.dataLayer || [];
585+
function gtag(){dataLayer.push(arguments);}
586+
gtag('js', new Date());
587+
588+
gtag('config', 'G-DQR5VQ6VXX');
589+
</script>
590+
555591
<title>${escapeHtml(title)}</title>
556592
<meta name="description" content="${escapeHtml(description)}">
557593
<link rel="canonical" href="${url}">

index.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,41 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66

7+
<!-- Cookiebot loads first so its consent decision exists before any Google
8+
script can read it, then the Consent Mode default state, then Google's
9+
tag. This block is byte-identical on every page (source of truth:
10+
index.html, copied into the generator template). -->
11+
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="11322285-cc73-4d07-a7e8-be34dc027c4e" type="text/javascript" async></script>
12+
<!-- Consent Mode v2 default state: Cookiebot's own inline-implementation guide
13+
says a default must be set manually and must precede gtag.js — this is
14+
that snippet, verbatim, from
15+
https://support.cookiebot.com/hc/en-us/articles/360016047000-Implementing-Google-Consent-Mode -->
16+
<script data-cookieconsent="ignore">
17+
window.dataLayer = window.dataLayer || [];
18+
function gtag(){dataLayer.push(arguments);}
19+
gtag('consent', 'default', {
20+
'ad_personalization': 'denied',
21+
'ad_storage': 'denied',
22+
'ad_user_data': 'denied',
23+
'analytics_storage': 'denied',
24+
'functionality_storage': 'denied',
25+
'personalization_storage': 'denied',
26+
'security_storage': 'granted',
27+
'wait_for_update': 500,
28+
});
29+
gtag('set', 'ads_data_redaction', true);
30+
gtag('set', 'url_passthrough', false);
31+
</script>
32+
<!-- Google tag (gtag.js) -->
33+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DQR5VQ6VXX"></script>
34+
<script>
35+
window.dataLayer = window.dataLayer || [];
36+
function gtag(){dataLayer.push(arguments);}
37+
gtag('js', new Date());
38+
39+
gtag('config', 'G-DQR5VQ6VXX');
40+
</script>
41+
742
<title>RAID Sandbox — build an array, see the real data layout</title>
843
<meta name="description" content="Drag disks onto a canvas, build a RAID array, and see where data and parity actually land — layouts follow the Linux md kernel rules. Free, in your browser.">
944
<link rel="canonical" href="https://raid-sandbox.dev/">

kb/algorithm.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,42 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
7+
<!-- Cookiebot loads first so its consent decision exists before any Google
8+
script can read it, then the Consent Mode default state, then Google's
9+
tag. This block is byte-identical on every page (source of truth:
10+
index.html, copied into the generator template). -->
11+
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="11322285-cc73-4d07-a7e8-be34dc027c4e" type="text/javascript" async></script>
12+
<!-- Consent Mode v2 default state: Cookiebot's own inline-implementation guide
13+
says a default must be set manually and must precede gtag.js — this is
14+
that snippet, verbatim, from
15+
https://support.cookiebot.com/hc/en-us/articles/360016047000-Implementing-Google-Consent-Mode -->
16+
<script data-cookieconsent="ignore">
17+
window.dataLayer = window.dataLayer || [];
18+
function gtag(){dataLayer.push(arguments);}
19+
gtag('consent', 'default', {
20+
'ad_personalization': 'denied',
21+
'ad_storage': 'denied',
22+
'ad_user_data': 'denied',
23+
'analytics_storage': 'denied',
24+
'functionality_storage': 'denied',
25+
'personalization_storage': 'denied',
26+
'security_storage': 'granted',
27+
'wait_for_update': 500,
28+
});
29+
gtag('set', 'ads_data_redaction', true);
30+
gtag('set', 'url_passthrough', false);
31+
</script>
32+
<!-- Google tag (gtag.js) -->
33+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DQR5VQ6VXX"></script>
34+
<script>
35+
window.dataLayer = window.dataLayer || [];
36+
function gtag(){dataLayer.push(arguments);}
37+
gtag('js', new Date());
38+
39+
gtag('config', 'G-DQR5VQ6VXX');
40+
</script>
41+
642
<title>Placement algorithm — RAID Sandbox knowledge base</title>
743
<meta name="description" content="The rule that says, stripe by stripe, which member holds the parity and where the data starts, or where a mirror&#39;s copies go.">
844
<link rel="canonical" href="https://raid-sandbox.dev/kb/algorithm.html">

kb/backplane.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,42 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
7+
<!-- Cookiebot loads first so its consent decision exists before any Google
8+
script can read it, then the Consent Mode default state, then Google's
9+
tag. This block is byte-identical on every page (source of truth:
10+
index.html, copied into the generator template). -->
11+
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="11322285-cc73-4d07-a7e8-be34dc027c4e" type="text/javascript" async></script>
12+
<!-- Consent Mode v2 default state: Cookiebot's own inline-implementation guide
13+
says a default must be set manually and must precede gtag.js — this is
14+
that snippet, verbatim, from
15+
https://support.cookiebot.com/hc/en-us/articles/360016047000-Implementing-Google-Consent-Mode -->
16+
<script data-cookieconsent="ignore">
17+
window.dataLayer = window.dataLayer || [];
18+
function gtag(){dataLayer.push(arguments);}
19+
gtag('consent', 'default', {
20+
'ad_personalization': 'denied',
21+
'ad_storage': 'denied',
22+
'ad_user_data': 'denied',
23+
'analytics_storage': 'denied',
24+
'functionality_storage': 'denied',
25+
'personalization_storage': 'denied',
26+
'security_storage': 'granted',
27+
'wait_for_update': 500,
28+
});
29+
gtag('set', 'ads_data_redaction', true);
30+
gtag('set', 'url_passthrough', false);
31+
</script>
32+
<!-- Google tag (gtag.js) -->
33+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DQR5VQ6VXX"></script>
34+
<script>
35+
window.dataLayer = window.dataLayer || [];
36+
function gtag(){dataLayer.push(arguments);}
37+
gtag('js', new Date());
38+
39+
gtag('config', 'G-DQR5VQ6VXX');
40+
</script>
41+
642
<title>Backplane — RAID Sandbox knowledge base</title>
743
<meta name="description" content="The board the disks plug into, on the path between them and the HBA or the controller.">
844
<link rel="canonical" href="https://raid-sandbox.dev/kb/backplane.html">

kb/bbu.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,42 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
7+
<!-- Cookiebot loads first so its consent decision exists before any Google
8+
script can read it, then the Consent Mode default state, then Google's
9+
tag. This block is byte-identical on every page (source of truth:
10+
index.html, copied into the generator template). -->
11+
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="11322285-cc73-4d07-a7e8-be34dc027c4e" type="text/javascript" async></script>
12+
<!-- Consent Mode v2 default state: Cookiebot's own inline-implementation guide
13+
says a default must be set manually and must precede gtag.js — this is
14+
that snippet, verbatim, from
15+
https://support.cookiebot.com/hc/en-us/articles/360016047000-Implementing-Google-Consent-Mode -->
16+
<script data-cookieconsent="ignore">
17+
window.dataLayer = window.dataLayer || [];
18+
function gtag(){dataLayer.push(arguments);}
19+
gtag('consent', 'default', {
20+
'ad_personalization': 'denied',
21+
'ad_storage': 'denied',
22+
'ad_user_data': 'denied',
23+
'analytics_storage': 'denied',
24+
'functionality_storage': 'denied',
25+
'personalization_storage': 'denied',
26+
'security_storage': 'granted',
27+
'wait_for_update': 500,
28+
});
29+
gtag('set', 'ads_data_redaction', true);
30+
gtag('set', 'url_passthrough', false);
31+
</script>
32+
<!-- Google tag (gtag.js) -->
33+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DQR5VQ6VXX"></script>
34+
<script>
35+
window.dataLayer = window.dataLayer || [];
36+
function gtag(){dataLayer.push(arguments);}
37+
gtag('js', new Date());
38+
39+
gtag('config', 'G-DQR5VQ6VXX');
40+
</script>
41+
642
<title>Battery backup unit and flash cache protection — RAID Sandbox knowledge base</title>
743
<meta name="description" content="What lets a hardware RAID controller keep its write cache through a power cut: a battery that powers the cache memory until power returns, or, in newer designs, a supercapacitor that powers the controller just long enough to copy the cache to flash.">
844
<link rel="canonical" href="https://raid-sandbox.dev/kb/bbu.html">

kb/capacity.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,42 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
7+
<!-- Cookiebot loads first so its consent decision exists before any Google
8+
script can read it, then the Consent Mode default state, then Google's
9+
tag. This block is byte-identical on every page (source of truth:
10+
index.html, copied into the generator template). -->
11+
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="11322285-cc73-4d07-a7e8-be34dc027c4e" type="text/javascript" async></script>
12+
<!-- Consent Mode v2 default state: Cookiebot's own inline-implementation guide
13+
says a default must be set manually and must precede gtag.js — this is
14+
that snippet, verbatim, from
15+
https://support.cookiebot.com/hc/en-us/articles/360016047000-Implementing-Google-Consent-Mode -->
16+
<script data-cookieconsent="ignore">
17+
window.dataLayer = window.dataLayer || [];
18+
function gtag(){dataLayer.push(arguments);}
19+
gtag('consent', 'default', {
20+
'ad_personalization': 'denied',
21+
'ad_storage': 'denied',
22+
'ad_user_data': 'denied',
23+
'analytics_storage': 'denied',
24+
'functionality_storage': 'denied',
25+
'personalization_storage': 'denied',
26+
'security_storage': 'granted',
27+
'wait_for_update': 500,
28+
});
29+
gtag('set', 'ads_data_redaction', true);
30+
gtag('set', 'url_passthrough', false);
31+
</script>
32+
<!-- Google tag (gtag.js) -->
33+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DQR5VQ6VXX"></script>
34+
<script>
35+
window.dataLayer = window.dataLayer || [];
36+
function gtag(){dataLayer.push(arguments);}
37+
gtag('js', new Date());
38+
39+
gtag('config', 'G-DQR5VQ6VXX');
40+
</script>
41+
642
<title>Capacity — RAID Sandbox knowledge base</title>
743
<meta name="description" content="How much of the raw disk space an array leaves usable for data.">
844
<link rel="canonical" href="https://raid-sandbox.dev/kb/capacity.html">

kb/chunk.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,42 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
7+
<!-- Cookiebot loads first so its consent decision exists before any Google
8+
script can read it, then the Consent Mode default state, then Google's
9+
tag. This block is byte-identical on every page (source of truth:
10+
index.html, copied into the generator template). -->
11+
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="11322285-cc73-4d07-a7e8-be34dc027c4e" type="text/javascript" async></script>
12+
<!-- Consent Mode v2 default state: Cookiebot's own inline-implementation guide
13+
says a default must be set manually and must precede gtag.js — this is
14+
that snippet, verbatim, from
15+
https://support.cookiebot.com/hc/en-us/articles/360016047000-Implementing-Google-Consent-Mode -->
16+
<script data-cookieconsent="ignore">
17+
window.dataLayer = window.dataLayer || [];
18+
function gtag(){dataLayer.push(arguments);}
19+
gtag('consent', 'default', {
20+
'ad_personalization': 'denied',
21+
'ad_storage': 'denied',
22+
'ad_user_data': 'denied',
23+
'analytics_storage': 'denied',
24+
'functionality_storage': 'denied',
25+
'personalization_storage': 'denied',
26+
'security_storage': 'granted',
27+
'wait_for_update': 500,
28+
});
29+
gtag('set', 'ads_data_redaction', true);
30+
gtag('set', 'url_passthrough', false);
31+
</script>
32+
<!-- Google tag (gtag.js) -->
33+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DQR5VQ6VXX"></script>
34+
<script>
35+
window.dataLayer = window.dataLayer || [];
36+
function gtag(){dataLayer.push(arguments);}
37+
gtag('js', new Date());
38+
39+
gtag('config', 'G-DQR5VQ6VXX');
40+
</script>
41+
642
<title>Chunk, strip and stripe — RAID Sandbox knowledge base</title>
743
<meta name="description" content="The chunk is the piece of a stripe that lands on one disk: what Linux `md` calls a chunk and most controllers a strip.">
844
<link rel="canonical" href="https://raid-sandbox.dev/kb/chunk.html">

kb/drive-group.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,42 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
7+
<!-- Cookiebot loads first so its consent decision exists before any Google
8+
script can read it, then the Consent Mode default state, then Google's
9+
tag. This block is byte-identical on every page (source of truth:
10+
index.html, copied into the generator template). -->
11+
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="11322285-cc73-4d07-a7e8-be34dc027c4e" type="text/javascript" async></script>
12+
<!-- Consent Mode v2 default state: Cookiebot's own inline-implementation guide
13+
says a default must be set manually and must precede gtag.js — this is
14+
that snippet, verbatim, from
15+
https://support.cookiebot.com/hc/en-us/articles/360016047000-Implementing-Google-Consent-Mode -->
16+
<script data-cookieconsent="ignore">
17+
window.dataLayer = window.dataLayer || [];
18+
function gtag(){dataLayer.push(arguments);}
19+
gtag('consent', 'default', {
20+
'ad_personalization': 'denied',
21+
'ad_storage': 'denied',
22+
'ad_user_data': 'denied',
23+
'analytics_storage': 'denied',
24+
'functionality_storage': 'denied',
25+
'personalization_storage': 'denied',
26+
'security_storage': 'granted',
27+
'wait_for_update': 500,
28+
});
29+
gtag('set', 'ads_data_redaction', true);
30+
gtag('set', 'url_passthrough', false);
31+
</script>
32+
<!-- Google tag (gtag.js) -->
33+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DQR5VQ6VXX"></script>
34+
<script>
35+
window.dataLayer = window.dataLayer || [];
36+
function gtag(){dataLayer.push(arguments);}
37+
gtag('js', new Date());
38+
39+
gtag('config', 'G-DQR5VQ6VXX');
40+
</script>
41+
642
<title>Drive group — RAID Sandbox knowledge base</title>
743
<meta name="description" content="The set of physical disks a controller gathers together to work with.">
844
<link rel="canonical" href="https://raid-sandbox.dev/kb/drive-group.html">

kb/fault-tolerance.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,42 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
7+
<!-- Cookiebot loads first so its consent decision exists before any Google
8+
script can read it, then the Consent Mode default state, then Google's
9+
tag. This block is byte-identical on every page (source of truth:
10+
index.html, copied into the generator template). -->
11+
<script id="Cookiebot" src="https://consent.cookiebot.com/uc.js" data-cbid="11322285-cc73-4d07-a7e8-be34dc027c4e" type="text/javascript" async></script>
12+
<!-- Consent Mode v2 default state: Cookiebot's own inline-implementation guide
13+
says a default must be set manually and must precede gtag.js — this is
14+
that snippet, verbatim, from
15+
https://support.cookiebot.com/hc/en-us/articles/360016047000-Implementing-Google-Consent-Mode -->
16+
<script data-cookieconsent="ignore">
17+
window.dataLayer = window.dataLayer || [];
18+
function gtag(){dataLayer.push(arguments);}
19+
gtag('consent', 'default', {
20+
'ad_personalization': 'denied',
21+
'ad_storage': 'denied',
22+
'ad_user_data': 'denied',
23+
'analytics_storage': 'denied',
24+
'functionality_storage': 'denied',
25+
'personalization_storage': 'denied',
26+
'security_storage': 'granted',
27+
'wait_for_update': 500,
28+
});
29+
gtag('set', 'ads_data_redaction', true);
30+
gtag('set', 'url_passthrough', false);
31+
</script>
32+
<!-- Google tag (gtag.js) -->
33+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DQR5VQ6VXX"></script>
34+
<script>
35+
window.dataLayer = window.dataLayer || [];
36+
function gtag(){dataLayer.push(arguments);}
37+
gtag('js', new Date());
38+
39+
gtag('config', 'G-DQR5VQ6VXX');
40+
</script>
41+
642
<title>Fault tolerance — RAID Sandbox knowledge base</title>
743
<meta name="description" content="How many disks can fail, in the worst place, before data is lost.">
844
<link rel="canonical" href="https://raid-sandbox.dev/kb/fault-tolerance.html">

0 commit comments

Comments
 (0)