Placing a compliant consent banner on your page while loading Google Analytics, Facebook Pixel, or Hotjar unconditionally in layout.tsx is security theater — the compliance UI exists but the data collection proceeds regardless of what the user clicks. ePrivacy Directive Art. 5(3) and GDPR Art. 6(1)(a) both require that non-essential cookies and tracking technologies activate only after the user's informed consent. Unconditional script loading in root layout files is the most common technical GDPR violation in Next.js applications, and it is trivially detectable from the browser network panel. Google Tag Manager compounds this risk because tags added via the GTM UI bypass the codebase entirely and may fire before any consent check.
Medium because unconditional tracking script loading makes all consent-based processing unlawful regardless of banner UX quality — the scripts process data before any consent signal is evaluated.
Configure Google Consent Mode v2 to deny all non-essential storage by default, before the analytics library initializes. Update consent grants only after explicit user action.
// lib/consent-init.ts — call as early as possible in app bootstrap
export function initConsentMode() {
if (typeof window === 'undefined') return
window.dataLayer = window.dataLayer ?? []
function gtag(..._args: unknown[]) { window.dataLayer.push(arguments) }
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
})
}
// In ConsentBanner's save() — update grants after user accepts
function applyConsent(choices: ConsentChoices) {
window.gtag?.('consent', 'update', {
analytics_storage: choices.analytics ? 'granted' : 'denied',
ad_storage: choices.marketing ? 'granted' : 'denied',
})
}
For GTM: configure a Consent Initialization trigger and set all non-essential tags to fire only when analytics_storage: 'granted'. Remove any unconditional <Script> tags for analytics or advertising from app/layout.tsx.
ID: gdpr-readiness.consent-management.conditional-script-loading
Severity: medium
What to look for: Search the codebase for all third-party script loading patterns. Check layout files (layout.tsx, _app.tsx, index.html, +layout.svelte) for unconditional script tags or <Script> components pointing to analytics, advertising, or tracking URLs. Check whether GA4, Facebook Pixel, Hotjar, LinkedIn Insight Tag, Segment, or similar tools are initialized inside a consent check or fired unconditionally. Look for Google Consent Mode v2 configuration — the gtag('consent', 'default', ...) call that sets all non-essential storage to 'denied' before the GA4 script loads. Check for useEffect hooks that initialize tracking on component mount without checking the stored consent value. Look for tag managers (GTM) — these are high-risk because tags added via the GTM UI bypass the codebase and may not be consent-gated. Count all instances found and enumerate each.
Pass criteria: Non-essential scripts (analytics, advertising, tracking) load only after the user has granted consent for their category. Google Consent Mode v2 is configured to default to denied for all non-essential storage. Any tag manager in use has consent-trigger firing rules applied to all non-essential tags. No analytics or tracking scripts load unconditionally. At least 1 implementation must be confirmed.
Fail criteria: Analytics or tracking scripts are imported and initialized unconditionally in root layout files. No consent check before gtag('config', ...) or equivalent. GTM present with no consent-mode configuration. Scripts load on first page view for all users regardless of consent status.
Skip (N/A) when: Application has no non-essential scripts — no analytics, no advertising pixels, no third-party tracking of any kind.
Detail on fail: Example: "Google Analytics gtag loaded unconditionally in layout.tsx. No consent mode configuration. Fires for all users including those who have not consented." or "GTM container present with no Consent Mode trigger. Tags in GTM container may fire before consent.".
Remediation: Configure Google Consent Mode v2 as a consent-first initialization. In Next.js, use the Script component with strategy="beforeInteractive" to set defaults before the GA4 script loads:
// lib/consent-init.ts
// Call this function as early as possible in your app bootstrap,
// before any analytics library initializes.
export function initConsentMode() {
if (typeof window === 'undefined') return
window.dataLayer = window.dataLayer ?? []
// eslint-disable-next-line prefer-rest-params
function gtag(..._args: unknown[]) { window.dataLayer.push(arguments) }
// Default: deny all non-essential storage until user explicitly consents
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
})
}
// In your ConsentBanner's save() function, update after user accepts:
function applyConsent(choices: ConsentChoices) {
window.gtag?.('consent', 'update', {
analytics_storage: choices.analytics ? 'granted' : 'denied',
ad_storage: choices.marketing ? 'granted' : 'denied',
})
}
For GTM: configure a Consent Initialization trigger in GTM's built-in consent settings and set all non-essential tags to only fire when analytics_storage: 'granted' or ad_storage: 'granted'.