Skills cookie-consent
install
source · Clone the upstream repo
git clone https://github.com/TerminalSkills/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/TerminalSkills/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/cookie-consent" ~/.claude/skills/terminalskills-skills-cookie-consent && rm -rf "$T"
manifest:
skills/cookie-consent/SKILL.mdsource content
Cookie Consent
Overview
The EU ePrivacy Directive (Cookie Law) and GDPR require informed, freely given, granular, and withdrawable consent before placing non-essential cookies. The UK PECR applies post-Brexit. Fines under GDPR can reach 4% of global annual turnover.
Strictly necessary cookies (no consent required): session tokens, shopping cart, security tokens, load balancing.
All others require consent: analytics, advertising, personalization, functional enhancements.
Cookie Categories
| Category | Examples | Consent Required |
|---|---|---|
| Strictly Necessary | Auth session, CSRF token, cart | ❌ No |
| Functional | Language preference, UI theme | ✅ Yes (GDPR) |
| Analytics | Google Analytics, Mixpanel, Hotjar | ✅ Yes |
| Marketing | Facebook Pixel, Google Ads, ad targeting | ✅ Yes |
Consent Requirements (GDPR Article 7)
- Granular: Per-category consent (not all-or-nothing)
- Freely given: "Accept all" ≠ better treatment; reject must be as easy as accept
- Informed: Clear explanation of what each category does
- Withdrawable: Users can change or revoke consent at any time
- Documented: Store a consent record (who consented, when, to what, via which version)
Custom Consent Banner (React/Next.js)
// components/CookieConsent.tsx import { useState, useEffect } from 'react'; interface ConsentPreferences { strictly_necessary: true; // Always true — cannot be disabled functional: boolean; analytics: boolean; marketing: boolean; } interface ConsentRecord { version: string; timestamp: string; preferences: ConsentPreferences; source: 'banner' | 'settings' | 'gpc'; } const CONSENT_VERSION = '2024-01-01'; const CONSENT_COOKIE_NAME = 'consent_preferences'; export function CookieConsent() { const [showBanner, setShowBanner] = useState(false); const [showDetails, setShowDetails] = useState(false); const [preferences, setPreferences] = useState<ConsentPreferences>({ strictly_necessary: true, functional: false, analytics: false, marketing: false, }); useEffect(() => { // Check for GPC signal first if (navigator.globalPrivacyControl) { const gpcConsent: ConsentRecord = { version: CONSENT_VERSION, timestamp: new Date().toISOString(), preferences: { strictly_necessary: true, functional: false, analytics: false, marketing: false }, source: 'gpc', }; saveConsent(gpcConsent); return; } // Check if consent already given const saved = getStoredConsent(); if (!saved || saved.version !== CONSENT_VERSION) { setShowBanner(true); } }, []); const acceptAll = () => { const allConsent: ConsentPreferences = { strictly_necessary: true, functional: true, analytics: true, marketing: true, }; const record: ConsentRecord = { version: CONSENT_VERSION, timestamp: new Date().toISOString(), preferences: allConsent, source: 'banner', }; saveConsent(record); applyConsent(allConsent); setShowBanner(false); }; const rejectAll = () => { const minimalConsent: ConsentPreferences = { strictly_necessary: true, functional: false, analytics: false, marketing: false, }; const record: ConsentRecord = { version: CONSENT_VERSION, timestamp: new Date().toISOString(), preferences: minimalConsent, source: 'banner', }; saveConsent(record); applyConsent(minimalConsent); setShowBanner(false); }; const saveCustom = () => { const record: ConsentRecord = { version: CONSENT_VERSION, timestamp: new Date().toISOString(), preferences: preferences, source: 'banner', }; saveConsent(record); applyConsent(preferences); setShowBanner(false); }; if (!showBanner) return null; return ( <div className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t shadow-lg p-6"> <div className="max-w-4xl mx-auto"> <h2 className="text-lg font-semibold mb-2">Cookie Preferences</h2> <p className="text-sm text-gray-600 mb-4"> We use cookies to improve your experience. You can choose which categories to allow. <a href="/privacy-policy" className="underline ml-1">Learn more</a> </p> {showDetails && ( <div className="mb-4 space-y-3"> {[ { key: 'strictly_necessary', label: 'Strictly Necessary', desc: 'Required for the site to function. Cannot be disabled.', locked: true }, { key: 'functional', label: 'Functional', desc: 'Remember your preferences (language, theme).' }, { key: 'analytics', label: 'Analytics', desc: 'Help us understand how you use our site (Google Analytics, Mixpanel).' }, { key: 'marketing', label: 'Marketing', desc: 'Show relevant ads and measure campaign effectiveness.' }, ].map(({ key, label, desc, locked }) => ( <div key={key} className="flex items-start gap-3"> <input type="checkbox" id={key} checked={preferences[key as keyof ConsentPreferences] as boolean} disabled={locked} onChange={(e) => setPreferences(prev => ({ ...prev, [key]: e.target.checked }))} className="mt-1" /> <label htmlFor={key} className="text-sm"> <strong>{label}</strong> {locked && <span className="text-gray-400">(Always on)</span>} <p className="text-gray-500">{desc}</p> </label> </div> ))} </div> )} <div className="flex flex-wrap gap-2"> <button onClick={rejectAll} className="px-4 py-2 border rounded text-sm">Reject All</button> <button onClick={() => setShowDetails(!showDetails)} className="px-4 py-2 border rounded text-sm"> {showDetails ? 'Hide Details' : 'Customize'} </button> {showDetails && ( <button onClick={saveCustom} className="px-4 py-2 bg-blue-600 text-white rounded text-sm">Save Preferences</button> )} <button onClick={acceptAll} className="px-4 py-2 bg-blue-600 text-white rounded text-sm">Accept All</button> </div> </div> </div> ); }
Consent Storage and Retrieval
// lib/consent.ts const CONSENT_KEY = 'consent_v2'; export function saveConsent(record: ConsentRecord): void { // Store in localStorage for JS access localStorage.setItem(CONSENT_KEY, JSON.stringify(record)); // Also set a cookie for server-side access const expires = new Date(); expires.setFullYear(expires.getFullYear() + 1); document.cookie = `${CONSENT_KEY}=${encodeURIComponent(JSON.stringify(record))}; expires=${expires.toUTCString()}; path=/; SameSite=Strict; Secure`; // Optionally send to your backend for audit trail fetch('/api/consent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(record), keepalive: true, }).catch(() => {}); // Best effort } export function getStoredConsent(): ConsentRecord | null { try { const stored = localStorage.getItem(CONSENT_KEY); return stored ? JSON.parse(stored) : null; } catch { return null; } } export function hasConsent(category: keyof ConsentPreferences): boolean { const consent = getStoredConsent(); return consent?.preferences[category] === true; }
Consent-Gated Analytics
// lib/analytics.ts — Only load analytics after consent export function applyConsent(preferences: ConsentPreferences): void { if (preferences.analytics) { loadGoogleAnalytics(); loadMixpanel(); } else { // Opt out / remove tracking window['ga-disable-G-XXXXXXXX'] = true; // Remove existing analytics cookies deleteCookie('_ga'); deleteCookie('_gid'); deleteCookie('_gat'); } if (preferences.marketing) { loadFacebookPixel(); loadGoogleAds(); } } function loadGoogleAnalytics(): void { if (document.getElementById('ga-script')) return; // Already loaded const script = document.createElement('script'); script.id = 'ga-script'; script.async = true; script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX'; document.head.appendChild(script); window.dataLayer = window.dataLayer || []; function gtag(...args: unknown[]) { window.dataLayer.push(args); } gtag('js', new Date()); gtag('config', 'G-XXXXXXXX', { anonymize_ip: true }); } function deleteCookie(name: string): void { document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${window.location.hostname}`; document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${window.location.hostname}`; }
GPC (Global Privacy Control) Detection
// middleware/gpc.js — Next.js middleware to detect GPC import { NextResponse } from 'next/server'; export function middleware(request) { const response = NextResponse.next(); // GPC header: Sec-GPC: 1 const gpc = request.headers.get('sec-gpc'); if (gpc === '1') { // Signal to client that GPC was detected response.headers.set('X-GPC-Detected', '1'); // Set a cookie to persist for this session response.cookies.set('gpc_optout', '1', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 24 * 365 // 1 year }); } return response; }
Consent Record API (Backend)
// pages/api/consent.ts — Store consent for audit trail import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') return res.status(405).end(); const { version, timestamp, preferences, source } = req.body; await db.consentRecords.create({ user_id: req.session?.userId || null, // null for anonymous session_id: req.session?.id, ip_address: req.headers['x-forwarded-for'] || req.socket.remoteAddress, user_agent: req.headers['user-agent'], consent_version: version, consented_at: timestamp, preferences: JSON.stringify(preferences), source, // 'banner' | 'settings' | 'gpc' }); res.status(204).end(); }
CMP Platforms (Alternatives to Custom Build)
| Platform | Pricing | IAB TCF | GPC | Notes |
|---|---|---|---|---|
| Cookiebot | From $9/mo | ✅ | ✅ | Auto-scan cookies, DSGVO certified |
| OneTrust | Enterprise | ✅ | ✅ | Full compliance suite |
| Osano | From $49/mo | ✅ | ✅ | SOC 2 certified, privacy-first |
| Usercentrics | From €60/mo | ✅ | ✅ | Popular in EU |
| Termly | Free tier | ❌ | ✅ | Simple, good for small sites |
IAB TCF 2.2 (Transparency and Consent Framework) is required if you work with ad networks. Use a certified CMP.
Compliance Checklist
- Cookie audit completed (all cookies inventoried by category)
- No non-essential cookies set before consent
- "Reject all" option as prominent as "Accept all"
- Granular per-category consent options available
- Consent withdrawal option in privacy settings page
- Consent stored with version, timestamp, and preferences
- GPC signal detected and honored automatically
- Privacy policy linked from banner
- Cookie policy lists all cookies with purpose and retention
- Third-party scripts blocked until consent granted
- Analytics opt-out cookies cleared on consent withdrawal
- Banner tested in EU with VPN (actually applies)
- Consent renewed when policy version changes