DEV Community

Nevik Schmidt
Nevik Schmidt

Posted on

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

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 5 Things That Will Get You Abmahn'd

1. External Google Fonts (Most Common)

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[]'
Enter fullscreen mode Exit fullscreen mode
/* 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;
}
Enter fullscreen mode Exit fullscreen mode

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 *;`
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

2. No Cookie Consent Banner (or Non-Compliant One)

The problem: The ePrivacy Directive requires consent before setting non-essential cookies.

The fix: Implement a proper consent banner with granular controls.

<!-- DON'T: Load analytics before consent -->
<script src="https://www.google-analytics.com/analytics.js"></script>

<!-- DO: Conditional loading -->
<script>
// 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();
}
</script>
Enter fullscreen mode Exit fullscreen mode

3. Missing or Incomplete Privacy Policy

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']
};
Enter fullscreen mode Exit fullscreen mode

4. Insecure Contact Forms

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 < NOW();

  res.json({ success: true });
});
Enter fullscreen mode Exit fullscreen mode

5. Third-Party Scripts Loading Without Consent

The problem: Chat widgets, analytics, social media embeds, etc. loading before user consent.

The fix: Consent-aware script loader.

// consent-loader.js
class ConsentManager {
  constructor() {
    this.consent = this.loadConsent();
    this.queue = [];
  }

  loadConsent() {
    return JSON.parse(localStorage.getItem('gdpr-consent') || '{}');
  }

  onConsent(category, callback) {
    if (this.consent[category]) {
      callback();
    } else {
      this.queue.push({ category, callback });
    }
  }

  grantConsent(category) {
    this.consent[category] = true;
    this.consent[`${category}_timestamp`] = new Date().toISOString();
    localStorage.setItem('gdpr-consent', JSON.stringify(this.consent));

    this.queue
      .filter(item => 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']);
});
Enter fullscreen mode Exit fullscreen mode

Quick Compliance Checklist

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)

Free Tool to Check Your Site

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.

Top comments (0)