Skip to content
Learni
View all tutorials
Sécurité & Données

How to Implement a GDPR Privacy Policy in Next.js in 2026

Lire en français

Introduction

In 2026, GDPR compliance remains essential for any website handling personal data. A poorly implemented privacy policy can lead to fines up to 4% of global turnover. This advanced tutorial guides you through building a dynamic solution in Next.js 15+: a dedicated MDX-generated page, a persistent cookie consent banner, secure preference management via encrypted localStorage, and conditional analytics integration (like Plausible). Unlike static generators, this scalable, SEO-friendly, audit-ready approach treats your app like a vault—the policy is the user manual, the consent banner the lock, and the utils the internal mechanism. By the end, your site will be CNIL audit-ready. (128 words)

Prerequisites

  • Node.js 20+ and npm/yarn/pnpm
  • Advanced knowledge of Next.js App Router and TypeScript
  • Familiarity with GDPR (Arts. 13/14), cookies (ePrivacy Directive)
  • Tools: Vercel for deployment, Plausible.io for privacy-first analytics

Initialize the Next.js Project

terminal
npx create-next-app@15 privacy-rgpd-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd privacy-rgpd-app
npm install mdx-bundler crypto-js lucide-react
npm install -D @types/crypto-js

This script sets up a Next.js 15 project with TypeScript, Tailwind for quick styling, and MDX for dynamic policy rendering. crypto-js encrypts cookie preferences, lucide-react provides banner icons. Skip manual setups to save time; test with npm run dev.

Project Structure and Routing

Create the src/app/privacy folder for the dedicated page, src/components for the ConsentBanner, and src/lib for cookie utils. Add middleware to enforce HTTPS in production. App Router handles SSR for the policy, optimizing SEO with GDPR-specific meta tags (e.g., robots: noai).

Create the Privacy Policy Page (MDX)

src/app/privacy/page.tsx
import { Metadata } from 'next';
import PrivacyContent from '@/components/PrivacyContent';

export const metadata: Metadata = {
  title: 'Politique de Confidentialité - RGPD 2026',
  description: 'Politique complète des données personnelles conforme RGPD (UE 2016/679).',
  robots: { index: true, follow: true, googleBot: { index: true } },
};

export default function PrivacyPage() {
  return (
    <main className="min-h-screen py-20 px-4 max-w-4xl mx-auto">
      <h1 className="text-4xl font-bold mb-8">Politique de Confidentialité</h1>
      <PrivacyContent />
    </main>
  );
}

This SSR page loads MDX content via a dedicated component with SEO-optimized metadata for crawlers. It includes a responsive wrapper. Pitfall: Don't forget GDPR meta tags to avoid search engine blocks; link it in the global footer.

MDX Privacy Policy Content (Full Template)

src/components/PrivacyContent.tsx
import { MDXRemote } from 'next-mdx-remote/rsc';

const mdxSource = `
# Article 1: Informations générales

**Éditeur :** Learni Dev SARL, 1 Rue Exemple, 75001 Paris.
**DPO :** dpo@learnidev.com.

Nous collectons : nom, email, IP (anonymisée).

# Article 2: Bases légales (Art. 6 RGPD)
- Consentement explicite pour cookies marketing.
- Intérêt légitime pour analytics essentiels.

# Article 3: Cookies
| Type | Durée | Finalité |
|------|-------|----------|
| Essentiel | 1 an | Session |
| Analytics | 13 mois | Stats |

# Article 4: Droits (Art. 15-22)
Accès, rectification via [formulaire](mailto:dpo@learnidev.com).

# Article 5: Destinataires
Hébergeur Vercel Inc. (USA, clauses contractuelles types).

Dernière MAJ: 01/01/2026.`;

export default function PrivacyContent() {
  return <MDXRemote source={mdxSource} />;
}

This component renders a complete, GDPR-compliant MDX template with cookie tables and actionable links. Customize clauses (e.g., replace publisher). MDX advantage: easy edits without rebuilds; pitfall: validate HTML output for WCAG accessibility.

Implementing the Consent Manager

The technical core: a custom useConsent hook manages cookie categories (essential, analytics, marketing). Encrypted localStorage storage prevents fingerprinting. The banner displays until explicit consent, compliant with ePrivacy.

useConsent Hook with Encryption

src/lib/useConsent.ts
import { useState, useEffect } from 'react';
import CryptoJS from 'crypto-js';

const SECRET_KEY = process.env.NEXT_PUBLIC_CONSENT_KEY || 'your-secret-key-change-it';

export type Consent = {
  essential: boolean;
  analytics: boolean;
  marketing: boolean;
};

export function useConsent(): [Consent, (c: Consent) => void, boolean] {
  const [consent, setConsent] = useState<Consent>({ essential: true, analytics: false, marketing: false });
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    try {
      const stored = localStorage.getItem('consent');
      if (stored) {
        const bytes = CryptoJS.AES.decrypt(stored, SECRET_KEY);
        const original = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
        setConsent(original);
      }
    } catch {
      // Invalid data: reset
    } finally {
      setLoaded(true);
    }
  }, []);

  const saveConsent = (newConsent: Consent) => {
    const encrypted = CryptoJS.AES.encrypt(JSON.stringify(newConsent), SECRET_KEY).toString();
    localStorage.setItem('consent', encrypted);
    setConsent(newConsent);
  };

  return [consent, saveConsent, loaded];
}

This hook decrypts/re-encrypts prefs in AES for enhanced privacy, with fallback to defaults. essential always true (required). Major pitfall: Rotate SECRET_KEY in prod via env vars; test invalid data to avoid crashes.

Cookie Consent Banner Component

src/components/CookieConsentBanner.tsx
import { useConsent } from '@/lib/useConsent';
import { Check, X, Shield } from 'lucide-react';

export default function CookieConsentBanner() {
  const [consent, saveConsent] = useConsent()[0];
  const loaded = useConsent()[2];

  if (loaded && consent.analytics && consent.marketing) return null;

  const handleAcceptAll = () => saveConsent({ essential: true, analytics: true, marketing: true });
  const handleReject = () => saveConsent({ essential: true, analytics: false, marketing: false });

  return (
    <div className="fixed bottom-4 left-4 right-4 bg-gradient-to-r from-blue-600 to-indigo-700 text-white p-6 rounded-xl shadow-2xl z-50 md:max-w-2xl mx-auto">
      <div className="flex flex-col md:flex-row gap-4 items-center justify-between">
        <div className="flex items-center gap-2">
          <Shield className="w-6 h-6" />
          <div>
            <h3 className="font-bold text-lg">Gestion des cookies RGPD</h3>
            <p className="text-sm opacity-90">Nous utilisons des cookies essentiels. Acceptez pour analytics et marketing.</p>
          </div>
        </div>
        <div className="flex gap-2">
          <button
            onClick={handleAcceptAll}
            className="px-4 py-2 bg-white text-blue-600 rounded-lg font-medium hover:bg-gray-100 transition"
          >
            Tout accepter
          </button>
          <button
            onClick={handleReject}
            className="px-4 py-2 border border-white rounded-lg font-medium hover:bg-white/10 transition"
          >
            Refuser non-essentiels
          </button>
        </div>
      </div>
    </div>
  );
}

Responsive Tailwind banner with granular buttons (no misleading 'reject all'). Hides after consent. Analogy: like a CNIL opt-in form. Pitfall: Use fixed z-50 to overlay all content; granularity avoids disputes.

Integration in Layout and Analytics

In src/app/layout.tsx, wrap as a client component. For analytics: condition Plausible script on consent.analytics. Add policy link in footer.

Root Layout with Consent Banner

src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import CookieConsentBanner from '@/components/CookieConsentBanner';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'Mon App RGPD Compliant',
  description: 'Site conforme RGPD 2026',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="fr">
      <body className={inter.className}>{children}</body>
      <CookieConsentBanner />
    </html>
  );
}

Global layout injects the banner everywhere. Simple and non-intrusive. Pitfall: Add 'use client' if needed, but SSR works here. Include

with /privacy link for traceability.

Conditional Analytics Script (Plausible)

src/components/Analytics.tsx
<script dangerouslySetInnerHTML={{
  __html: `
    window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) };
  `,
}} />
{consent.analytics && (
  <script
    defer
    data-domain="example.com"
    src="https://plausible.io/js/script.outbound-links.js"
    data-api="https://plausible.io/api/event"
  />
)}

Conditional snippet: loads only if analytics consent given. Plausible is privacy-first (no cookies). Integrate in layout. Pitfall: defer prevents LCP blocking; track outbound links without extra consent.

Next.js Config for Security Headers

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  headers: async () => [
    {
      source: '/(.*)',
      headers: [
        {
          key: 'Permissions-Policy',
          value: 'camera=(), microphone=(), geolocation=()',
        },
        {
          key: 'Referrer-Policy',
          value: 'strict-origin-when-cross-origin',
        },
        {
          key: 'Content-Security-Policy',
          value: "default-src 'self'; script-src 'self' plausible.io; frame-ancestors 'none'",
        },
      ],
    },
  ],
  output: 'standalone',
};

module.exports = nextConfig;

Headers boost privacy: CSP blocks unwanted trackers, Permissions-Policy stops invasive features. Aligns with GDPR data minimization. Pitfall: Test CSP in report-only mode first to avoid breaks.

Best Practices

  • Granular consent: Minimum 3 categories (essential/analytics/marketing), refresh every 6 months.
  • Encryption + TTL: Add expiration to prefs (e.g., max 1 year).
  • Audit logs: Log anonymized consents in backend for CNIL proof.
  • i18n ready: Translate policy via next-intl.
  • A/B test banner: Measure acceptance rates via post-consent analytics.

Common Mistakes to Avoid

  • Implicit consent: Never default non-essential cookies; CNIL fines are common.
  • Unencrypted localStorage: Exposes to XSS; always use AES or IndexedDB.
  • Forget HTTPS middleware: Add headers: [{ key: 'Strict-Transport-Security', value: 'max-age=31536000' }].
  • Outdated static policy: Automate updates via GitHub Actions for legal dates.

Next Steps