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[]'
/* 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 *;`
}
}
}
});
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>
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']
};
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 });
});
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']);
});
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)