Cookie Consent for Next.js, Gatsby, and Static Sites: A Developer's Integration Guide
The Static Site Consent Problem
Modern JavaScript frameworks like Next.js, Gatsby, and Nuxt.js introduced a paradigm shift in how web pages are built and delivered. Pages are pre-rendered at build time or on the server, then hydrated on the client. This creates a unique challenge for cookie consent: the consent banner must be ready before any tracking scripts execute, but the page itself may already be rendered and cached at the edge.
Traditional CMPs were designed for server-rendered PHP or simple HTML pages where the document loads linearly from top to bottom. In a framework world with code splitting, lazy loading, and streaming server-side rendering, the assumptions break. Getting consent right in these environments requires understanding the rendering pipeline.
Why Timing Matters More Than You Think
In a standard HTML page, placing a CMP script in the <head> before other scripts is straightforward. In Next.js App Router or Gatsby, the situation is more complex:
- Pre-rendered HTML arrives first: The browser receives complete HTML from the CDN or server. If any inline scripts or third-party tags are embedded in that HTML, they may execute before your consent logic loads.
- Hydration gap: React hydration happens after the HTML is painted. If your consent component is a React component, it does not exist in a functional state until hydration completes. During this gap, Google tags or analytics scripts could fire without consent.
- Edge caching complications: If you use ISR (Incremental Static Regeneration) or edge functions, the HTML is cached. You cannot dynamically inject consent-dependent logic into cached HTML without a client-side mechanism.
The core principle is this: consent must be established at the script level, not the component level. A React component that renders a consent banner is too late if it only becomes interactive after hydration.
Next.js App Router Integration
Next.js 13+ with the App Router introduced a new way to handle scripts. Here is the recommended approach for consent integration:
Step 1: Load the CMP Script in the Root Layout
Use the Next.js Script component with the beforeInteractive strategy in your root layout.tsx. This tells Next.js to inject the script into the initial HTML document, before hydration begins:
The beforeInteractive strategy is critical. The default afterInteractive strategy loads scripts after hydration, which is too late for consent. With beforeInteractive, the CMP script is included in the server-rendered HTML and executes as the page loads.
Step 2: Set Default Consent Before Google Tags
Before your Google Tag Manager or gtag.js snippet, include an inline script that sets default consent states. This ensures that even if GTM loads before the CMP banner appears, it respects the denied defaults:
This inline script should be placed in the <head> of your root layout, before the CMP and GTM scripts. In Next.js, you can use a regular <script> tag inside the <head> element of your layout for this purpose.
Step 3: Handle Route Changes
In single-page application navigation, the CMP script loads once but route changes do not trigger a full page reload. Your CMP must persist across client-side navigations. FlexyConsent handles this automatically — once loaded, it remains active across all route changes without re-initialization.
Next.js Pages Router Integration
For projects still using the Pages Router, the approach is similar but uses _document.tsx instead of the root layout. Place the CMP script in the <Head> component of your custom Document class. The beforeInteractive strategy works the same way in the Pages Router.
The key difference is that _document.tsx only renders on the server, so any consent logic here is guaranteed to be in the initial HTML payload.
Gatsby Static Site Integration
Gatsby generates fully static HTML at build time. There is no server-side rendering at request time, which simplifies some aspects but complicates others:
- Use
gatsby-ssr.tsxto inject the CMP script into the<head>of every page. TheonRenderBodyAPI lets you add scripts to the head that will be present in every static HTML file. - Avoid Gatsby plugins that lazy-load consent: Some community plugins wrap consent in React components that only mount after hydration. This creates the timing gap discussed earlier.
- Place consent defaults inline: Use
setHeadComponentsingatsby-ssr.tsxto add an inline script setting default consent states. This script will be in the static HTML and execute immediately.
Gatsby's build-time approach means every HTML file on your CDN will include the consent script. This is actually ideal — there is no server logic to fail or cache incorrectly.
Nuxt.js Considerations
Nuxt.js (Vue-based) has its own patterns. In Nuxt 3, use the useHead composable or the nuxt.config.ts app head configuration to add the CMP script globally. Nuxt supports a body: false option (which places scripts in the head) and an async attribute for non-blocking loading.
For Nuxt's server-side rendering mode, the same principle applies: the CMP script must be in the initial HTML response, not dynamically injected by a Vue component after mount.
Avoiding Layout Shift
Consent banners are notorious for causing Cumulative Layout Shift (CLS), a Core Web Vital that affects SEO rankings. When a banner pops in after the page renders, it pushes content down or overlays it unexpectedly.
Strategies to minimize CLS from consent banners:
- Use a bottom-positioned banner: Banners at the bottom of the viewport do not shift page content. This is the most CLS-friendly approach.
- Reserve space: If you must use a top banner, reserve the vertical space in your CSS so the page layout accounts for the banner before it renders.
- Avoid modal overlays on load: Full-screen consent walls that appear after the page has rendered cause perceived layout instability. If you need a wall, render it as part of the initial page state.
- Load the CMP synchronously in head: When the CMP is loaded as a render-blocking script in the head, the banner can appear as part of the initial paint rather than popping in later.
FlexyConsent's Framework-Agnostic Approach
FlexyConsent was designed to work with any framework — or no framework at all — by operating at the script level rather than the component level. Here is why this matters:
- Single async script tag: One
<script>tag in the<head>is all that is needed. No npm packages to install, no framework-specific wrappers, no build configuration. - Consent defaults fire immediately: The script sets Consent Mode V2 defaults as its first action, before any callback or DOM manipulation. This means Google tags respect consent from the very first millisecond.
- No DOM dependency: The consent logic does not wait for React, Vue, or Svelte to hydrate. It operates independently of the framework lifecycle.
- Works with SSG, SSR, ISR, and CSR: Because it is a plain script, it functions identically whether the page was statically generated, server-rendered, incrementally regenerated, or client-side rendered.
Developer tip: The simplest test for correct CMP integration is to open your browser's Network tab, filter by Google domains, and reload the page. No Google requests should fire before the consent default command appears in the console. If they do, your CMP is loading too late.
FlexyConsent's free plan supports unlimited pageviews and works with Next.js, Gatsby, Nuxt, Astro, SvelteKit, Remix, and plain HTML. The integration is the same across all of them: one script tag, properly placed.