Cookie Consent & Analytics Implementation Guide
Last updated: February 14, 2026
This document describes how to implement a GDPR-compliant cookie consent system with conditional analytics script loading that respects geographic restrictions (GDPR countries).
Architecture Overview
┌─────────────────────────────────────────────────────────────────────┐
│ Root Layout │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ CookiePolicyContextProvider │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ - Fetches GDPR country status via /api/is-gdpr-country │ │ │
│ │ │ - Manages consent expiry state │ │ │
│ │ │ - Exposes showCookieConsent & setIsOpenCookiePolicySettings│ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ CookieConsent │ │ TrackersProvider │ │ │
│ │ │ (Banner UI) │ │ (Conditional Script Load) │ │ │
│ │ └─────────────────────┘ └─────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Key Components
1. Storage Utilities (src/utils/storage.ts)
A wrapper around localStorage that stores all app data under a single key.
export enum StorageName {
LOCAL_STORAGE = 'localStorage',
SESSION_STORAGE = 'sessionStorage',
}
const STORE_KEY = 'UNIFORM_DATA'; // Change to your app name
const getStorage = (storageName: StorageName) => (typeof window !== 'undefined' ? window[storageName] : null);
export const getStorageItem = <T>(
name: string | string[],
storageName: StorageName = StorageName.LOCAL_STORAGE
): T | null => {
const storage = getStorage(storageName);
if (!storage) return null;
const localConfigStr: string | null = storage.getItem(STORE_KEY);
if (!localConfigStr) return null;
const localConfig = JSON.parse(localConfigStr);
if (Array.isArray(name)) {
return name.reduce((acc, key) => {
acc[key] = localConfig[key];
return acc;
}, {} as any) as T;
}
return localConfig[name] ?? null;
};
export const setStorageItem = (modifier: object, storageName: StorageName = StorageName.LOCAL_STORAGE): void => {
const storage = getStorage(storageName);
if (!storage) return;
const localConfigStr: string | null = storage.getItem(STORE_KEY);
const localConfig = localConfigStr ? JSON.parse(localConfigStr) : {};
const nextLocalConfig = { ...localConfig, ...modifier };
storage.setItem(STORE_KEY, JSON.stringify(nextLocalConfig));
};
2. Storage Constants
export const enum StorageKeys {
COOKIES_POLICY = 'COOKIES_POLICY',
// ... other keys
}
3. useStorage Hook (src/hooks/useStorage.tsx)
A reactive hook that syncs state with localStorage and broadcasts changes across tabs/components.
'use client';
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react';
import { getStorageItem, setStorageItem, StorageName } from '@/utils/storage';
type SetValue<T> = Dispatch<SetStateAction<T>>;
function useStorage<T>(
key: string,
initialValue: T,
storage: StorageName = StorageName.LOCAL_STORAGE
): [T, SetValue<T>] {
const [value, setStoredValue] = useState<T>(getStorageItem(key, storage) || initialValue);
// Listen for changes from other components/tabs
const listener = useCallback(
(e: Event) => {
const nextValue = getStorageItem(e.type, storage);
setStoredValue(nextValue as T);
},
[storage]
);
useEffect(() => {
window.addEventListener(key, listener, false);
return () => window.removeEventListener(key, listener);
}, [key, listener]);
const setValue: SetValue<T> = useCallback(
nextValue => {
const valueToStore = nextValue instanceof Function ? nextValue(value) : nextValue;
setStorageItem({ [key]: valueToStore }, storage);
// Dispatch custom event to notify other subscribers
window.dispatchEvent(new Event(key));
},
[key, value, storage]
);
return [value, setValue];
}
export default useStorage;
4. useCookiesPolicy Hook (src/hooks/useCookiesPolicy.ts)
Manages the consent state with expiration logic.
import { useMemo } from 'react';
import useStorage from '@/hooks/useStorage';
import { StorageKeys } from '@/constants';
interface CookiePolicy {
allow: boolean;
timestamp: string; // ISO date string - when consent expires
}
const useCookiesPolicy = () => {
const [cookiesPolicy, setCookiesPolicy] = useStorage<CookiePolicy>(StorageKeys.COOKIES_POLICY, {
allow: false,
timestamp: '',
});
// Check if consent has expired
const isExpired = useMemo(() => {
if (!cookiesPolicy.timestamp) return true;
const now = new Date();
const expires = new Date(cookiesPolicy.timestamp);
return now.getTime() > expires.getTime();
}, [cookiesPolicy.timestamp]);
return {
isAllowed: cookiesPolicy.allow,
isExpired,
setCookiesPolicy,
};
};
export default useCookiesPolicy;
5. GDPR Country Detection API (src/app/api/is-gdpr-country/route.ts)
Server-side geolocation check. Uses Vercel’s geolocation function (or your hosting provider’s equivalent).
import { NextResponse } from 'next/server';
import { geolocation } from '@vercel/functions';
// List of countries requiring consent (GDPR + similar regulations)
const GDPR_COUNTRIES = [
'AL',
'AD',
'AM',
'AT',
'AZ',
'BE',
'BA',
'BG',
'HR',
'CY',
'CZ',
'DK',
'EE',
'FI',
'FR',
'GE',
'DE',
'GR',
'HU',
'IS',
'IE',
'IT',
'KZ',
'LV',
'LI',
'LT',
'LU',
'MT',
'MD',
'MC',
'ME',
'MK',
'NL',
'NO',
'PL',
'PT',
'RO',
'SM',
'RS',
'SK',
'SI',
'ES',
'SE',
'CH',
'TR',
'UA',
'GB',
'VA',
// Additional privacy-conscious regions
'CN',
'BR',
'ZA',
'AE',
];
export async function GET(request: Request) {
const { country } = geolocation(request);
// Default to requiring consent if country unknown
const isGDPRCountry = !country || GDPR_COUNTRIES.includes(country);
return NextResponse.json({ isGDPRCountry });
}
6. Cookie Policy Context (src/context/cookie-policy.tsx)
The main context that orchestrates everything.
import React, {
createContext,
Dispatch,
SetStateAction,
useEffect,
useMemo,
useState
} from 'react';
import useCookiesPolicy from '@/hooks/useCookiesPolicy';
export const CookiePolicyContext = createContext<{
isGDPRCountry: boolean;
isOpenCookiePolicySettings: boolean;
isLoading: boolean;
setIsOpenCookiePolicySettings: Dispatch<SetStateAction<boolean>>;
showCookieConsent: boolean;
}>({
isGDPRCountry: false,
isOpenCookiePolicySettings: false,
isLoading: true,
setIsOpenCookiePolicySettings: () => null,
showCookieConsent: false,
});
const CookiePolicyContextProvider = ({ children }: { children: React.ReactNode }) => {
const [isLoading, setIsLoading] = useState(true);
// Default to true (safer - assumes GDPR until confirmed otherwise)
const [isGDPRCountry, setIsGDPRCountry] = useState(true);
const { isExpired } = useCookiesPolicy();
// Allows user to reopen settings from footer link
const [isOpenCookiePolicySettings, setIsOpenCookiePolicySettings] = useState(false);
// Logic for when to show the consent banner
const showCookieConsent = useMemo(() => {
return (
!isLoading &&
(isOpenCookiePolicySettings || (isGDPRCountry && isExpired))
);
}, [isLoading, isOpenCookiePolicySettings, isGDPRCountry, isExpired]);
// Fetch GDPR status on mount
useEffect(() => {
fetch('/api/is-gdpr-country', {
next: { revalidate: 3600 * 24 }, // Cache for 1 day
})
.then(res => res.json())
.then(data => setIsGDPRCountry(data.isGDPRCountry))
.finally(() => setIsLoading(false));
}, []);
return (
<CookiePolicyContext.Provider
value={{
isGDPRCountry,
isLoading,
isOpenCookiePolicySettings,
setIsOpenCookiePolicySettings,
showCookieConsent,
}}
>
{children}
</CookiePolicyContext.Provider>
);
};
export default CookiePolicyContextProvider;
7. Cookie Consent Component (src/components/CookieConsent/CookieConsent.tsx)
The UI banner that allows users to accept or decline cookies.
'use client';
import React, { FC, useCallback, useContext } from 'react';
import { CookiePolicyContext } from '@/context/cookie-policy';
import useCookiesPolicy from '@/hooks/useCookiesPolicy';
interface CookieConsentProps {
text: string;
title?: string;
allowButtonText: string;
declineButtonText: string;
}
const CookieConsent: FC<CookieConsentProps> = ({
text,
title,
allowButtonText,
declineButtonText,
}) => {
const { isAllowed, isExpired, setCookiesPolicy } = useCookiesPolicy();
const { isOpenCookiePolicySettings, setIsOpenCookiePolicySettings, showCookieConsent } =
useContext(CookiePolicyContext);
const handleAllowCookiesButtonClick = useCallback(() => {
const date = new Date();
// Set expiry 1 year from now
const expires = new Date(date.setFullYear(date.getFullYear() + 1));
setCookiesPolicy({ allow: true, timestamp: expires.toISOString() });
setIsOpenCookiePolicySettings(false);
}, [setCookiesPolicy, setIsOpenCookiePolicySettings]);
const handleDeclineCookiesButtonClick = useCallback(() => {
// Remove all existing cookies
const cookies = document.cookie.split(';');
cookies.forEach(cookie => {
const eqPos = cookie.indexOf('=');
const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie;
document.cookie = name.trim() + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/';
});
const date = new Date();
const expires = new Date(date.setFullYear(date.getFullYear() + 1));
setCookiesPolicy({ allow: false, timestamp: expires.toISOString() });
setIsOpenCookiePolicySettings(false);
// Reload to remove active trackers if user was previously opted in
if (isOpenCookiePolicySettings && !isExpired && isAllowed) {
window.location.reload();
}
}, [isAllowed, isExpired, isOpenCookiePolicySettings, setCookiesPolicy, setIsOpenCookiePolicySettings]);
if (!showCookieConsent) return null;
return (
<div className="fixed bottom-0 left-0 w-full z-[9999] bg-white shadow-lg p-4 md:p-6">
<div className="max-w-6xl mx-auto flex flex-col md:flex-row items-center justify-between gap-4">
<div>
{title && <h2 className="text-lg font-semibold mb-2">{title}</h2>}
<p dangerouslySetInnerHTML={{ __html: text }} />
</div>
<div className="flex gap-2 shrink-0">
<button
onClick={handleAllowCookiesButtonClick}
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
{allowButtonText}
</button>
<button
onClick={handleDeclineCookiesButtonClick}
className="px-6 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
>
{declineButtonText}
</button>
</div>
</div>
</div>
);
};
export default CookieConsent;
8. Trackers Provider (src/components/TrackersProvider/TrackersProvider.tsx)
Conditionally loads analytics scripts based on consent state and geography.
'use client';
import React, { memo, useContext } from 'react';
import Script from 'next/script';
import { CookiePolicyContext } from '@/context/cookie-policy';
import useCookiesPolicy from '@/hooks/useCookiesPolicy';
interface Props {
gtmId?: string;
}
const TrackersProvider: React.FC<Required<Props> & { isGDPRCountry: boolean }> = memo(
({ isGDPRCountry, gtmId }) => {
const { isAllowed, isExpired } = useCookiesPolicy();
// Don't load trackers if in GDPR country and user hasn't consented or consent expired
if (isGDPRCountry && (!isAllowed || isExpired)) {
return null;
}
return (
<>
{/* Google Tag Manager */}
{gtmId && (
<Script
id="gtm"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `(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}');`,
}}
/>
)}
{/* Add other analytics scripts here */}
</>
);
}
);
TrackersProvider.displayName = 'TrackersProvider';
// HOC to inject GDPR context
const TrackersProviderHOC: React.FC<Props> = ({ gtmId = '' }) => {
const { isGDPRCountry } = useContext(CookiePolicyContext);
return (
<TrackersProvider
isGDPRCountry={isGDPRCountry}
gtmId={gtmId}
/>
);
};
export default TrackersProviderHOC;
9. Providers Wrapper (src/context/Providers.tsx)
Assembles all providers in the correct order.
'use client';
import React from 'react';
import TrackersProvider from '@/components/TrackersProvider';
import CookiePolicyContextProvider from './cookie-policy';
const Providers = ({ children }: { children: React.ReactNode }) => (
<CookiePolicyContextProvider>
{children}
<TrackersProvider
gtmId={process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGEMENT_ID}
/>
</CookiePolicyContextProvider>
);
export default Providers;
10. Root Layout Integration (src/app/layout.tsx)
import Providers from '@/context/Providers';
export default async function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Implementation Checklist
Create utility files:
[ ]
src/utils/storage.ts- localStorage wrapper[ ]
src/constants/index.ts- Storage keys enum
Create hooks:
[ ]
src/hooks/useStorage.tsx- Reactive localStorage hook[ ]
src/hooks/useCookiesPolicy.ts- Consent state management
Create API route:
[ ]
src/app/api/is-gdpr-country/route.ts- Geolocation check
Create context:
[ ]
src/context/cookie-policy.tsx- Main context provider
Create components:
[ ]
src/components/CookieConsent/CookieConsent.tsx- UI banner[ ]
src/components/TrackersProvider/TrackersProvider.tsx- Script loader
Create Providers wrapper:
[ ]
src/context/Providers.tsx- Assembles all providers
Update layout:
[ ] Wrap app with
<Providers>in root layout
Environment variables:
[ ]
NEXT_PUBLIC_GOOGLE_TAG_MANAGEMENT_ID- GTM container ID
Flow Diagram
User visits site
│
▼
┌──────────────────────────┐
│ CookiePolicyContextProvider│
│ - Defaults isGDPRCountry=true│
│ - Fetches /api/is-gdpr-country│
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ showCookieConsent logic │
│ │
│ Show banner if: │
│ - Not loading │
│ - AND (settings open OR │
│ (GDPR country AND │
│ consent expired)) │
└──────────────────────────┘
│
├───────────────────────────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ User clicks │ │ TrackersProvider │
│ "Allow" or │ │ checks: │
│ "Decline" │ │ - Not GDPR OR │
└──────────────────┘ │ (allowed AND │
│ │ not expired) │
▼ └──────────────────┘
┌──────────────────┐ │
│ setCookiesPolicy │ ▼
│ - Sets allow/deny│ ┌──────────────────┐
│ - Sets expiry │ │ Load scripts: │
│ (1 year) │ │ - GTM │
└──────────────────┘ │ - Others │
│ │ │
▼ └──────────────────┘
Banner hides
Adding a “Cookie Settings” Link
To allow users to change their preferences, add a link/button (typically in footer):
import { useContext } from 'react';
import { CookiePolicyContext } from '@/context/cookie-policy';
const CookieSettingsLink = () => {
const { setIsOpenCookiePolicySettings } = useContext(CookiePolicyContext);
return (
<button onClick={() => setIsOpenCookiePolicySettings(true)}>
Cookie Settings
</button>
);
};
Important Notes
Default Behavior: Defaults to requiring consent (safer for GDPR compliance)
Non-GDPR Countries: Users from non-GDPR countries never see the banner, and trackers load immediately
Consent Duration: 1 year expiry, matching most regulatory requirements
Cookie Cleanup: When declining, all cookies are deleted and page reloads to stop active trackers
SSR Compatibility: All client-side code is properly wrapped with
'use client'directivesDynamic Import: The CookieConsent component is dynamically imported with
ssr: falseto prevent hydration mismatches:export const CookieConsent = dynamic(() => import('@/components/CookieConsent'), { ssr: false });