← back to Interiordesignershowroom
GA4 under strict CSP: allow-list gtag.js origin + self-host inline bootstrap (/js/ga.js, deferred)
0e28b9327f92fdfe1b671240c740e70a86a2b8f2 · 2026-08-03 12:52:57 -0700 · Steve Abrams
The parallel GA4 injection (bc6f4d9) added an external googletagmanager.com script
+ an INLINE gtag config to the layout head — both blocked by C11's script-src 'self'
(dead analytics on next reload). Fix keeps the CSP strict: allow-list
www.googletagmanager.com for the library, move the inline bootstrap to a self-hosted
deferred /js/ga.js so 'self' admits it (no 'unsafe-inline' reintroduced). connect-src
left intentionally open (documented) so GA4 regional beacons keep working — enumerating
them is brittle. Cody-gated, verified live (CSP header, ga.js 200+deferred, 0 inline gtag).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M lib/render.jsA public/js/ga.jsM server.js
Diff
commit 0e28b9327f92fdfe1b671240c740e70a86a2b8f2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 12:52:57 2026 -0700
GA4 under strict CSP: allow-list gtag.js origin + self-host inline bootstrap (/js/ga.js, deferred)
The parallel GA4 injection (bc6f4d9) added an external googletagmanager.com script
+ an INLINE gtag config to the layout head — both blocked by C11's script-src 'self'
(dead analytics on next reload). Fix keeps the CSP strict: allow-list
www.googletagmanager.com for the library, move the inline bootstrap to a self-hosted
deferred /js/ga.js so 'self' admits it (no 'unsafe-inline' reintroduced). connect-src
left intentionally open (documented) so GA4 regional beacons keep working — enumerating
them is brittle. Cody-gated, verified live (CSP header, ga.js 200+deferred, 0 inline gtag).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
lib/render.js | 2 +-
public/js/ga.js | 10 ++++++++++
server.js | 6 +++++-
3 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/lib/render.js b/lib/render.js
index ada9b7e..65f6310 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -267,7 +267,7 @@ function layout({ title, description, canonical, jsonld, image, body, activeNav,
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Google tag (gtag.js) — GA4 property "Interior Designers Showroom" (G-DH9G5HL7YJ) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DH9G5HL7YJ"></script>
-<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-DH9G5HL7YJ');</script>
+<script src="${v('/js/ga.js')}" defer></script>
<title>${esc(title)} · ${esc(SITE.name)}</title>
<meta name="description" content="${esc(description || SITE.tagline)}">
<link rel="canonical" href="${esc(canonical || SITE.url)}">
diff --git a/public/js/ga.js b/public/js/ga.js
new file mode 100644
index 0000000..63a7094
--- /dev/null
+++ b/public/js/ga.js
@@ -0,0 +1,10 @@
+// GA4 bootstrap for interiordesignershowroom.com (property G-DH9G5HL7YJ). Self-hosted
+// (not inline) so the public storefront's strict `script-src 'self'` CSP admits it
+// WITHOUT re-opening 'unsafe-inline' — the C11 hardening stays intact. The gtag.js
+// LIBRARY still loads from https://www.googletagmanager.com (that origin is in
+// script-src for exactly this reason); this file only defines dataLayer + config so
+// the measurement queue drains once the async library arrives.
+window.dataLayer = window.dataLayer || [];
+function gtag() { dataLayer.push(arguments); }
+gtag('js', new Date());
+gtag('config', 'G-DH9G5HL7YJ');
diff --git a/server.js b/server.js
index 29fc3ed..a85aa1d 100644
--- a/server.js
+++ b/server.js
@@ -29,7 +29,11 @@ app.use((req, res, next) => {
// (behind auth, low XSS risk) so its one inline onsubmit confirm still works without
// surgery on the concurrently-edited admin shell. ld+json is data, unaffected by script-src.
const isAdmin = req.path === '/admin' || req.path.indexOf('/admin/') === 0;
- const scriptSrc = isAdmin ? "script-src 'self' 'unsafe-inline'" : "script-src 'self'";
+ const scriptSrc = isAdmin ? "script-src 'self' 'unsafe-inline'" : "script-src 'self' https://www.googletagmanager.com";
+ // NOTE: connect-src is intentionally OMITTED (and there is no default-src), so cross-origin
+ // fetch/beacon is unrestricted — this is what lets GA4 send measurement to www.google-analytics.com
+ // and the region*.google-analytics.com endpoints. If a default-src is EVER added here, you MUST
+ // also add an explicit connect-src for those GA hosts or analytics beacons die silently.
res.set('Content-Security-Policy', `${scriptSrc}; frame-ancestors 'self'; object-src 'none'; base-uri 'none'`);
next();
});
← c988220 deploy: add push-full-kamatera.sh (full-source sync, avoids
·
back to Interiordesignershowroom
·
chore: bump v0.4.2 (session close) — GA4-under-CSP fix; qual 160817d →