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

  1. Create utility files:

    • [ ] src/utils/storage.ts - localStorage wrapper

    • [ ] src/constants/index.ts - Storage keys enum

  2. Create hooks:

    • [ ] src/hooks/useStorage.tsx - Reactive localStorage hook

    • [ ] src/hooks/useCookiesPolicy.ts - Consent state management

  3. Create API route:

    • [ ] src/app/api/is-gdpr-country/route.ts - Geolocation check

  4. Create context:

    • [ ] src/context/cookie-policy.tsx - Main context provider

  5. Create components:

    • [ ] src/components/CookieConsent/CookieConsent.tsx - UI banner

    • [ ] src/components/TrackersProvider/TrackersProvider.tsx - Script loader

  6. Create Providers wrapper:

    • [ ] src/context/Providers.tsx - Assembles all providers

  7. Update layout:

    • [ ] Wrap app with <Providers> in root layout

  8. 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

  1. Default Behavior: Defaults to requiring consent (safer for GDPR compliance)

  2. Non-GDPR Countries: Users from non-GDPR countries never see the banner, and trackers load immediately

  3. Consent Duration: 1 year expiry, matching most regulatory requirements

  4. Cookie Cleanup: When declining, all cookies are deleted and page reloads to stop active trackers

  5. SSR Compatibility: All client-side code is properly wrapped with 'use client' directives

  6. Dynamic Import: The CookieConsent component is dynamically imported with ssr: false to prevent hydration mismatches:

    export const CookieConsent = dynamic(() => import('@/components/CookieConsent'), { ssr: false });