Next.js cookie consent with Google Consent Mode v2
Two things make cookie consent in Next.js harder than the tutorials admit: script loading order, and Google Consent Mode. Get the order wrong and your analytics either fires before consent (a GDPR problem) or never receives a consent signal (a data problem). This guide sets up both correctly, with the CLI or by hand.
The fast path: create-consentify
npx create-consentify@latest --gcmThe CLI detects Next.js App Router or Pages Router, wires the consent provider, and with --gcm adds the Google Consent Mode v2 defaults. It also supports Vite, Remix, Astro, and plain HTML. If you would rather see every moving part, keep reading.
Manual setup: the widget tag
Add the widget to your root layout with next/script. The strategy matters:
// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://consentify.dev/widget.js"
data-token="ct_your_site_token"
data-gcm="true"
strategy="afterInteractive"
/>
</body>
</html>
);
}Do not use strategy="lazyOnload". Lazy loading defers the widget until after everything else, which means blocked scripts stay blocked long after the visitor consented on a previous visit, and Google tags run before any consent default reaches them. afterInteractive keeps the widget early without blocking first paint.
Blocking your tracking scripts
A consent banner that only records choices is not compliance - the scripts must actually wait. Mark each tracker inert with type="text/plain" and name its category:
<Script
type="text/plain"
data-consentify="analytics"
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"
strategy="afterInteractive"
/>Browsers ignore text/plain scripts entirely. When the visitor grants the analytics category, the widget swaps the type and the script executes in its original order. Scripts in a category the site marks required run immediately.
Google Consent Mode v2
Since March 2024, Google tags need consent signals (ad_storage, ad_user_data, ad_personalization, analytics_storage) or measurement degrades. The ordering rule: defaults must be set before the Google tag loads. That is what data-gcm="true" on the widget tag does - it arms gtag('consent', 'default', ...) with everything denied at parse time, then pushes updates the moment the visitor decides. Turning it on from the dashboard config instead works, but the config arrives by fetch - too late if the Google tag is not blocked. Use the attribute.
The npm alternative
For UI-less consent state in React components, @consentify/core and @consentify/react:
import { createConsentify } from "@consentify/core";
export const consentify = await createConsentify({
siteId: "your-site-id",
endpoints: {
config: "https://consentify.dev/api/sdk",
ingest: "https://consentify.dev/api/sdk",
},
});
// anywhere in your app
if (consentify.isGranted("analytics")) {
loadAnalytics();
}Checklist before you ship
- Network tab: no tracker requests before a consent decision
- Accept, reload: trackers fire; reject, reload: they do not
window.dataLayershows a consent default entry before the Google tag loads- Banner shows Accept and Reject with equal prominence
- Visitors with Global Privacy Control get no banner and no trackers
Check your own site