GDPR compliance for web devs: A practical technical guide (2026 edition with code examples)

Published: (June 11, 2026 at 06:40 AM EDT)
4 min read
Source: Dev.to

Source: Dev.to

Originally written for r/webdev on Reddit — sharing here for the dev.to community. I’m a developer based in Germany. After getting hit with a €900 Abmahnung (warning letter) because a client’s website loaded Google Fonts externally, I went deep down the GDPR/DSGVO compliance rabbit hole. Here’s everything I learned, distilled into actionable technical steps. This is NOT legal advice. This IS what actually works in practice based on EU court rulings as of 2026. The problem: Loading fonts.googleapis.com sends user IP to Google servers. The Munich court ruled this constitutes data processing without consent (LG München, 2022). The fix: Self-host your fonts.

Download Google Fonts locally

npx google-font-download “Inter:wght@400;600;700” —output ./fonts

Or use the google-webfonts-helper API

curl “https://gwfh.mranftl.com/api/fonts/inter?subsets=latin” | jq -r ‘.variants[] | .fontFiles[]’

/* Before (ILLEGAL in EU) */ @import url(‘https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap’);

/* After (DSGVO compliant) */ @font-face { font-family: ‘Inter’; font-style: normal; font-weight: 400; font-display: swap; src: local(‘Inter Regular’), url(‘/fonts/inter-v12-latin-regular.woff2’) format(‘woff2’); unicode-range: U+0000-00FF, U+0131, U+0152-0153; }

If you use Vite or webpack: // vite.config.js — self-host fonts instead of Google CDN export default defineConfig({ css: { preprocessorOptions: { scss: { additionalData: @use "src/styles/fonts" as *; } } } });

The problem: The ePrivacy Directive requires consent before setting non-essential cookies. The fix: Implement a proper consent banner with granular controls.

// Check consent state from localStorage const consent = JSON.parse(localStorage.getItem(‘cookie-consent’) || ’{}’);

if (consent.analytics) { // Load analytics only after consent const script = document.createElement(‘script’); script.src = ‘/js/analytics.js’; // Self-hosted! script.async = true; document.head.appendChild(script); }

// Cookie consent banner logic function setConsent(type) { const consent = JSON.parse(localStorage.getItem(‘cookie-consent’) || ’{}’); consent[type] = true; consent.timestamp = new Date().toISOString(); localStorage.setItem(‘cookie-consent’, JSON.stringify(consent)); document.getElementById(‘cookie-banner’).style.display = ‘none’;

if (consent.analytics) loadAnalytics(); if (consent.marketing) loadMarketing(); }

The problem: Article 13/14 GDPR requires specific information about data processing. The fix: Dynamic privacy policy that reflects actual data processing. // privacy-policy-data.js — Keep your privacy policy in sync with reality const dataProcessing = { cookies: { essential: [ { name: ‘session’, purpose: ‘Login session’, duration: ‘24h’, provider: ‘First-party’ } ], analytics: [ { name: ‘_pa’, purpose: ‘Page analytics’, duration: ‘13 months’, provider: ‘Self-hosted Matomo’ } ] }, thirdPartyServices: [ { name: ‘Hetzner’, purpose: ‘Server hosting’, location: ‘Germany’, data: ‘Server logs’ }, { name: ‘Stripe’, purpose: ‘Payment processing’, location: ‘EU/US (SCCs)’, data: ‘Payment data’ } ], dataSubjectRights: [‘access’, ‘rectification’, ‘erasure’, ‘portability’, ‘restriction’, ‘objection’] };

The problem: Forms that send data via email without encryption, or store data without consent. The fix: DSGVO-compliant form handling. // DSGVO-compliant form handler app.post(‘/contact’, rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }), async (req, res) => { const { name, email, message, privacyConsent } = req.body;

// 1. Verify consent if (!privacyConsent) { return res.status(400).json({ error: ‘Privacy consent required’ }); }

// 2. Log consent with timestamp await db.query( ‘INSERT INTO consent_log (email, purpose, timestamp, ip_hash) VALUES ($1, $2, NOW(), $3)’, [email, ‘contact_form’, hashIP(req.ip)] ];

// 3. Store inquiry with auto-deletion (Art. 5(1)(e) — storage limitation) await db.query( ‘INSERT INTO inquiries (name, email, message, created_at, delete_after) VALUES ($1, $2, $3, NOW(), NOW() + INTERVAL ‘30 days’)’, [name, email, message] );

// 4. Auto-delete old data (run as cron job) // DELETE FROM inquiries WHERE delete_after item.category === category) .forEach(item => item.callback()); } }

// Usage: const consent = new ConsentManager();

// Only load Intercom if user consents to support cookies consent.onConsent(‘support’, () => { window.Intercom(‘boot’, { app_id: ‘YOUR_ID’ }); });

// Only load analytics if user consents consent.onConsent(‘analytics’, () => { // Use Matomo self-hosted instead of GA const _paq = window._paq || []; _paq.push([‘trackPageView’]); _paq.push([‘enableLinkTracking’]); });

Use this as a pre-launch checklist: [ ] All fonts self-hosted (no Google Fonts CDN) [ ] Cookie consent banner with granular controls [ ] Analytics loads only after consent (use Matomo self-hosted) [ ] Privacy policy lists all data processing activities [ ] Contact forms log consent with timestamp [ ] Data auto-deletion after 30 days [ ] SSL/TLS on all pages [ ] No third-party scripts loading before consent [ ] Impressum with real contact info (for .de domains) [ ] Server located in EU (or proper SCCs in place) I got tired of manually checking all these things, so I built a scanner. It’s free, no signup: nevik.de/guard/ — Enter any URL and it checks: External resource loading (fonts, scripts, CDNs) Cookie consent status SSL/TLS configuration Missing legal pages (Impressum, Datenschutz) Third-party tracker detection It found violations on 73% of German websites I tested, including some big ones. If anyone wants to go deeper, I wrote a complete DSGVO audit guide for developers with all the code above plus templates for privacy policies, consent banners, and data processing documentation. DM me for the link.

0 views
Back to Blog

Related posts

Read more »

The spec is in the wrong place

My day job is at a large tech company. Hundreds of engineering teams, and every one of them is somewhere different on AI adoption. Some are still treating codin...

The Heuristics Say Don't

A culture that only records its disasters ends up with a biased archive. Wars documented, plagues chronicled, collapses catalogued. The quiet decades go unwritt...