Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ import { ContentStatus } from "@/components/content-status";
import Scripts from "@/components/scripts";
import type { Metadata, ResolvingMetadata } from "next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { cookies } from "next/headers";
import NextScript from "next/script";
import {
CONSENT_COOKIE_NAME,
parseConsentCookie,
GTM_ID,
buildConsentDefaultScript,
buildGtmLoaderScript,
} from "@/lib/consent";
import "./globals.css";

import { default as OurLayout } from "@/components/layout";
Expand Down Expand Up @@ -64,6 +73,9 @@ export default async function RootLayout({ children }) {
const pageMap = await getPageMap();
const enableSearch = process.env.NEXT_PUBLIC_ENABLE_SEARCH_BLOG_INTEGRATION === "true";
const starCount = await getStarCount();
const cookieStore = await cookies();
const consent = parseConsentCookie(cookieStore.get(CONSENT_COOKIE_NAME)?.value);
const consentDefaultScript = buildConsentDefaultScript(consent);

const navbar = (
<Navbar
Expand Down Expand Up @@ -97,8 +109,31 @@ export default async function RootLayout({ children }) {
// Sandworm background — stone-975 (dark) / stone-025 (light) — so every
// docs page matches the branded home instead of Nextra's neutral #111.
backgroundColor={{ dark: "#0c050f", light: "#f8f6f8" }}
/>
>
{/* Google Consent Mode v2 defaults — must run before GTM */}
<script dangerouslySetInnerHTML={{ __html: consentDefaultScript }} />

{/* Google Tag Manager */}
{GTM_ID && (
<NextScript
id="gtm-script"
strategy="afterInteractive"
dangerouslySetInnerHTML={{ __html: buildGtmLoaderScript(GTM_ID) }}
/>
)}
</Head>
<body>
{/* GTM noscript fallback */}
{GTM_ID && (
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
height="0"
width="0"
style={{ display: "none", visibility: "hidden" }}
/>
</noscript>
)}
<Layout
banner={
<Banner dismissible={false}>
Expand Down
106 changes: 106 additions & 0 deletions lib/consent/gtm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { ConsentPreferences } from "./types";

export const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID;

// EU/EEA/UK — Google's own region list for its regional consent default.
const EU_EEA_UK_REGIONS = [
"AT",
"BE",
"BG",
"HR",
"CY",
"CZ",
"DK",
"EE",
"FI",
"FR",
"DE",
"GR",
"HU",
"IE",
"IT",
"LV",
"LT",
"LU",
"MT",
"NL",
"PL",
"PT",
"RO",
"SK",
"SI",
"ES",
"SE",
"IS",
"LI",
"NO",
"GB",
];

/**
* Builds the inline Google Consent Mode v2 default script that must run
* before GTM loads.
*
* This app is a read-only consumer of the `az-consent` cookie set by the
* marketing site (see ./storage) — it never shows its own banner. When the
* cookie already reflects a decision, apply it directly as the Consent Mode
* default so there's no flash of denied-then-granted. Otherwise fall back
* to the same region-based default the marketing site uses before a
* decision has been made.
*/
export function buildConsentDefaultScript(consent: ConsentPreferences | null): string {
if (consent) {
return `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
analytics_storage: '${consent.statistics ? "granted" : "denied"}',
ad_storage: '${consent.marketing ? "granted" : "denied"}',
ad_user_data: '${consent.marketing ? "granted" : "denied"}',
ad_personalization: '${consent.marketing ? "granted" : "denied"}',
functionality_storage: '${consent.preferences ? "granted" : "denied"}',
personalization_storage: '${consent.preferences ? "granted" : "denied"}',
security_storage: 'granted'
});
`;
}

return `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}

// Global default: granted (non-EU visitors)
gtag('consent', 'default', {
analytics_storage: 'granted',
ad_storage: 'granted',
ad_user_data: 'granted',
ad_personalization: 'granted',
functionality_storage: 'granted',
personalization_storage: 'granted',
security_storage: 'granted'
});

// EU/EEA/UK override: denied until explicit consent
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
functionality_storage: 'denied',
personalization_storage: 'denied',
wait_for_update: 1000,
region: ${JSON.stringify(EU_EEA_UK_REGIONS)}
});
`;
}

/** Builds the standard GTM loader snippet for the given container ID. */
export function buildGtmLoaderScript(gtmId: string): string {
return `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${gtmId}');
`;
}
1 change: 1 addition & 0 deletions lib/consent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export {
CONSENT_COOKIE_NAME,
} from "./storage";
export { isEUVisitor } from "./eu-detection";
export { GTM_ID, buildConsentDefaultScript, buildGtmLoaderScript } from "./gtm";
export type { ConsentPreferences } from "./types";
Loading