Skip to content

feat: GA4 consent banner — load Google only after opt-in#65

Merged
goetchstone merged 1 commit into
mainfrom
feat/ga4-consent-banner
Jun 16, 2026
Merged

feat: GA4 consent banner — load Google only after opt-in#65
goetchstone merged 1 commit into
mainfrom
feat/ga4-consent-banner

Conversation

@goetchstone

Copy link
Copy Markdown
Owner

Summary

You're keeping GA4, so this adds a consent banner that actually does its job — GA4 does not load until the visitor opts in. A banner that lets Google load anyway is the useless kind; this isn't that.

Behavior (verified in-browser, end to end)

State Banner GA4
No choice yet shown not loaded
Decline hidden, persisted never loads — zero Google, no cookies (gtag undefined, no script in DOM)
Accept hidden, persisted loads with the configured measurement ID
Reopen via footer "Cookie choices" shown again re-decidable
  • Equal-weight Accept / Decline — no dark pattern (and no buried reject).
  • Choice stored in localStorage, not a cookie, so storing the choice itself needs no consent.
  • "Cookie choices" footer link self-hides until a choice exists — no dead link on cookieless/none deployments (your CCPA-style "Your Privacy Choices" path).
  • Only applies to GA4. Plausible/Umami stay direct (cookieless → no consent needed).
  • measurementId is validated against ^[A-Za-z0-9-]+$ before being interpolated into the script tag.

Privacy policy corrected

The page previously claimed "No third-party analytics or tracking scripts," "No advertising cookies," and "No behavioral tracking" — all false the moment GA4 is on. Rewritten to be truthful: documents GA4, that it's opt-in only, essential vs. analytics cookies, and how to change the choice. (A false privacy policy on a privacy-positioning firm is worse than no banner.)

Files

  • components/site/consent-analytics.tsx (new) — gate + banner
  • components/site/cookie-settings-link.tsx (new) — footer reopener
  • components/site/analytics.tsx — ga4 branch → consent gate
  • components/site/site-footer.tsx — adds the reopener
  • app/privacy/page.tsx — truthful analytics/cookies section

Verification

  • Browser: full accept/decline/reopen flow confirmed; decline loads no Google; no console errors
  • npx tsc --noEmit ✓ · npx vitest run ✓ (45) · npm run build

To activate

Set analytics_provider=ga4 + analytics_site_id=G-XXXXXXXXXX in /dashboard/settings. Until then the banner stays dormant (nothing to consent to). Note: akritos.com still has no DMARC record published — separate from this.

Test plan

  • CI passes
  • After deploy + setting GA4 in Settings: banner appears for new visitors; decline → no GA4 in network tab; accept → GA4 fires; "Cookie choices" reopens it

Keep GA4 but gate it properly: the Google script does NOT load until
the visitor accepts. Declining loads nothing and sets no cookies
(verified in-browser: gtag undefined, no googletagmanager script).

- components/site/consent-analytics.tsx — client gate + bottom banner.
  Equal-weight Accept/Decline (no dark pattern), choice persisted in
  localStorage (not a cookie), reopenable via footer. measurementId
  validated before script interpolation.
- components/site/cookie-settings-link.tsx — footer 'Cookie choices'
  reopener that self-hides until a choice exists (no dead link on
  cookieless/none deployments).
- analytics.tsx — ga4 branch now renders the consent gate; Plausible/
  Umami stay direct (cookieless, no consent needed).
- privacy page — corrected the now-false 'no third-party analytics /
  no cookies' claims; documents GA4, the opt-in, essential vs analytics
  cookies, and how to change the choice.

Verified end-to-end in browser: no-choice shows banner + no GA4;
decline loads zero Google and persists; reopen works; accept injects
GA4 with the right measurement ID. tsc + 45 tests + build all pass.
@goetchstone goetchstone merged commit 4d79ea6 into main Jun 16, 2026
@goetchstone goetchstone deleted the feat/ga4-consent-banner branch June 16, 2026 10:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants