Cookie Consent for Next.js: The Complete Guide
Next.js has some specific considerations for cookie consent that don't apply to plain HTML sites. The App Router's server/client component split, the next/script component, and hydration timing all affect how you implement consent. Here's how to do it correctly.
Adding the Consent Script
The getconsent.io script needs to load before any other third-party scripts. In a Next.js App Router project, add it to your root layout using the Script component from next/script:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Script
src="https://getconsent.io/v1.js"
data-site-id="your-site-id"
strategy="beforeInteractive"
/>
{children}
</body>
</html>
);
}
The strategy="beforeInteractive" property is critical. It ensures the consent script loads and executes before the page becomes interactive — meaning it can block non-essential cookies before any other script has a chance to set them.
Gating Google Analytics with the Script Component
For scripts that should only load after consent, you have two approaches. The first uses the consent event listener in a client component:
// components/analytics.tsx
"use client";
import { useEffect, useState } from "react";
import Script from "next/script";
export function Analytics() {
const [consentGiven, setConsentGiven] = useState(false);
useEffect(() => {
function handleConsent(e: CustomEvent) {
if (e.detail.analytics) {
setConsentGiven(true);
}
}
window.addEventListener(
"consent",
handleConsent as EventListener
);
return () => {
window.removeEventListener(
"consent",
handleConsent as EventListener
);
};
}, []);
if (!consentGiven) return null;
return (
<>
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"
strategy="afterInteractive"
/>
<Script id="gtag-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXX');
`}
</Script>
</>
);
}
Then include <Analytics /> in your root layout. Since it's a client component, it won't affect the server rendering of your pages.
The Vanilla JS Approach
The second approach skips React entirely and uses a plain <script> block. This can be simpler if you don't need React state:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Script
src="https://getconsent.io/v1.js"
data-site-id="your-site-id"
strategy="beforeInteractive"
/>
<Script id="consent-handler" strategy="afterInteractive">
{`
window.addEventListener("consent", function(e) {
if (e.detail.analytics) {
var s = document.createElement("script");
s.src = "https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX";
s.async = true;
document.head.appendChild(s);
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag("js", new Date());
gtag("config", "G-XXXXXXX");
}
});
`}
</Script>
{children}
</body>
</html>
);
}
This approach keeps everything in server components and avoids the overhead of a client component for analytics gating.
Server-Side Considerations
Cookie consent is inherently a client-side concern — cookies are set by the browser, and consent is given by the browser user. However, there are server-side implications:
Server Components and Cookies
If you read cookies in server components (e.g., using cookies() from next/headers), be aware that these cookies are sent by the browser on every request — including non-essential cookies that were set before consent was implemented. The consent script handles the browser side, but your server code should also respect consent status.
API Routes and Analytics
If you're doing server-side analytics or tracking in API routes, those are not covered by the consent banner. Server-side tracking (e.g., sending events to analytics from a route handler) needs its own consent check. You can read the consent cookie from the request headers:
// app/api/track/route.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const cookieStore = await cookies();
const consent = cookieStore.get("consent_status");
if (!consent || !JSON.parse(consent.value).analytics) {
return NextResponse.json({ tracked: false });
}
// Proceed with server-side analytics
return NextResponse.json({ tracked: true });
}
Middleware Considerations
If you use Next.js middleware (e.g., for A/B testing or personalization), ensure your middleware doesn't set non-essential cookies before consent is given. Check the consent cookie in your middleware before setting any tracking cookies:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const consent = request.cookies.get("consent_status");
const response = NextResponse.next();
if (consent) {
const status = JSON.parse(consent.value);
if (status.functional) {
// Safe to set functional cookies like A/B test variants
response.cookies.set("ab_variant", "B", {
maxAge: 60 * 60 * 24 * 30,
});
}
}
return response;
}
Static Export Compatibility
If you're using output: "export" for a static Next.js site, the getconsent.io script works without any changes. Since it's a client-side script loaded via a regular <script> tag, it's compatible with any hosting — Vercel, Netlify, Cloudflare Pages, or a plain S3 bucket.
Common Pitfalls in Next.js
- Using
strategy="lazyOnload"for the consent script. This delays loading until after the page is idle, which means other scripts could fire before consent is collected. Always usebeforeInteractive. - Importing analytics packages at the top level of server components. Some analytics SDKs set cookies on import. Move all analytics initialization to client components or inline scripts gated behind consent.
- Forgetting about
<Image>analytics. Next.js's Image component doesn't set cookies, but if you're using a third-party image CDN that does, those need consent gating too.
Next.js projects benefit from the same consent pattern as any other site — the key is getting the loading order right. Use beforeInteractive for the consent script, gate everything else behind the consent event, and be mindful of server-side cookie usage.