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
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-jsThis 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)
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)
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
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
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
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)
<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
/** @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
- CNIL Documentation: Policy Templates
- Tools: Open-Source Cookiebot Alternative
- Learni Training: Master GDPR DevOps
- Deploy on Vercel and test with privacy-checker