Is Your Website GDPR Compliant? A Practical Checklist
Practical GDPR compliance checklist for websites. Cookie audit, consent banner, privacy policy, Google Analytics.
TL;DR
GDPR compliance for websites requires informed consent before setting non-essential cookies, a transparent privacy policy, properly configured analytics tools, and secure handling of personal data through contact forms. This checklist walks you through auditing your cookies, setting up a legally sound consent banner, configuring Google Analytics 4 with Consent Mode v2, and fixing the most common violations that trigger fines. This article provides technical guidance, not legal advice. Consult a qualified attorney for binding legal assessments.
Prerequisites
- Administrative access to your website (CMS backend or hosting file system)
- Access to your Google Analytics / Google Tag Manager account (if applicable)
- A browser with developer tools (Chrome, Firefox, or Edge)
- Basic understanding of HTML and JavaScript
- A text editor for modifying templates or configuration files
Step 1: Understand What GDPR Requires for Websites
The General Data Protection Regulation (GDPR) applies to any website that processes personal data of individuals in the European Economic Area (EEA). For a typical website, personal data processing happens more often than you might think: loading a page already transmits the visitor's IP address.
Core Principles That Affect Websites
- Lawfulness, fairness, and transparency: You must have a legal basis for processing data and clearly inform users about it.
- Purpose limitation: Collect data only for specified, explicit purposes.
- Data minimization: Process only the data you actually need.
- Storage limitation: Do not retain personal data longer than necessary.
- Integrity and confidentiality: Protect data with appropriate technical measures (HTTPS, encryption at rest).
The ePrivacy Directive (Cookie Law)
While GDPR covers personal data broadly, the ePrivacy Directive (2002/58/EC, as amended) specifically governs cookies and similar tracking technologies. Together, they require:
- Strictly necessary cookies may be set without consent.
- All other cookies (analytics, marketing, preferences) require prior informed consent.
- Consent must be freely given, specific, informed, and unambiguous (an affirmative action).
- Pre-checked boxes or implied consent (e.g., "by continuing to browse") do not qualify.
Step 2: Perform a Cookie Audit
Before you can manage consent properly, you need to know exactly which cookies and trackers your website uses. Many site owners are surprised by what they find.
Manual Browser Audit
Open your website in an incognito/private window with all extensions disabled. Then open developer tools:
// Chrome / Edge: F12 → Application → Cookies
// Firefox: F12 → Storage → Cookies
// Steps:
// 1. Clear all cookies for your domain
// 2. Load the homepage without interacting with the consent banner
// 3. Document every cookie that appears
// 4. Navigate through key pages (contact, blog, shop)
// 5. Document new cookies set on each pageFor each cookie, record:
- Cookie name
- Domain (first-party vs. third-party)
- Expiration period
- Purpose (functional, analytics, marketing)
- Whether it is set before or after consent
Automated Scanning
Use a dedicated scanner for a thorough audit. Several free and commercial tools are available:
# Using Cookiebot scanner (free for up to 1 page)
https://www.cookiebot.com/en/cookie-scanner/
# Using the open-source tool "cookie-scanner" via npm
npm install -g @nicedoc/cookie-scanner
cookie-scanner https://yourdomain.com
# Check network requests for third-party calls
# Chrome DevTools → Network tab → filter by "third-party"
# Look for requests to: google-analytics.com, doubleclick.net,
# facebook.com, hotjar.com, fonts.googleapis.com, etc.Common Cookies You Might Find
| Cookie | Source | Category | Consent Required |
|---|---|---|---|
| PHPSESSID | Your server | Strictly necessary | No |
| wordpress_logged_in_* | WordPress | Strictly necessary | No |
| _ga, _gid | Google Analytics | Analytics | Yes |
| _fbp | Facebook Pixel | Marketing | Yes |
| fr | Marketing | Yes | |
| _hjid | Hotjar | Analytics | Yes |
Step 3: Implement a Compliant Consent Banner
A GDPR-compliant consent banner is not just a notification. It is a functional interface that collects, records, and enforces user consent choices.
Requirements for a Valid Consent Banner
- No pre-selected checkboxes. All non-essential categories must be off by default.
- Equal prominence for Accept and Reject. The reject option must be as easy to reach as the accept option. A small text link hidden under layers does not qualify.
- Granular choices. Users must be able to consent to categories independently (e.g., analytics yes, marketing no).
- No cookie walls. You must not block access to the website if the user declines non-essential cookies (exceptions exist for some paywall scenarios).
- Consent storage. Record the consent decision with a timestamp and make it revocable at any time.
- Prior blocking. Non-essential scripts must not execute until consent is granted.
Implementation Example with a Custom Script
If you prefer not to use a third-party Consent Management Platform (CMP), here is a minimal consent logic pattern:
<!-- Consent Banner HTML -->
<div id="consent-banner" role="dialog" aria-label="Cookie Consent" style="display:none;">
<p>We use cookies to analyze website traffic and optimize your experience.
You can choose which categories to allow.</p>
<div class="consent-options">
<label>
<input type="checkbox" checked disabled> Strictly Necessary (always active)
</label>
<label>
<input type="checkbox" id="consent-analytics"> Analytics
</label>
<label>
<input type="checkbox" id="consent-marketing"> Marketing
</label>
</div>
<button id="consent-accept-all">Accept All</button>
<button id="consent-save">Save Preferences</button>
<button id="consent-reject-all">Reject All</button>
</div>
<script>
(function() {
var stored = localStorage.getItem('cookie_consent');
if (!stored) {
document.getElementById('consent-banner').style.display = 'block';
} else {
applyConsent(JSON.parse(stored));
}
document.getElementById('consent-accept-all').addEventListener('click', function() {
saveConsent({ analytics: true, marketing: true });
});
document.getElementById('consent-reject-all').addEventListener('click', function() {
saveConsent({ analytics: false, marketing: false });
});
document.getElementById('consent-save').addEventListener('click', function() {
saveConsent({
analytics: document.getElementById('consent-analytics').checked,
marketing: document.getElementById('consent-marketing').checked
});
});
function saveConsent(choices) {
choices.timestamp = new Date().toISOString();
localStorage.setItem('cookie_consent', JSON.stringify(choices));
document.getElementById('consent-banner').style.display = 'none';
applyConsent(choices);
}
function applyConsent(choices) {
if (choices.analytics) { loadAnalytics(); }
if (choices.marketing) { loadMarketing(); }
}
function loadAnalytics() {
// Load GA4 only after consent
var s = document.createElement('script');
s.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX';
document.head.appendChild(s);
}
function loadMarketing() {
// Load marketing scripts only after consent
}
})();
</script>Recommended Consent Management Platforms
For production sites, consider a dedicated CMP that handles edge cases, consent logging, and integration with IAB TCF:
- Cookiebot (by Usercentrics) — scans cookies automatically, integrates with GTM
- Usercentrics — highly configurable, strong GTM integration
- Klaro (open source) — self-hosted, lightweight, privacy-friendly
- Osano — simple setup, free tier available
Step 4: Create a Proper Privacy Policy
Every website processing personal data must provide a comprehensive privacy policy accessible from every page (typically linked in the footer).
Mandatory Sections
- Identity and contact details of the data controller (company name, address, email).
- Data Protection Officer contact details (if applicable — required for public authorities and large-scale data processors).
- Types of data collected (IP addresses, email addresses, cookies, form data).
- Purposes and legal basis for each processing activity (Art. 6(1) GDPR — consent, legitimate interest, contractual necessity).
- Recipients of personal data (hosting provider, analytics services, email service).
- Third-country transfers (if data is sent outside the EEA, such as to US-based services — specify the safeguard mechanism, e.g., EU-US Data Privacy Framework, Standard Contractual Clauses).
- Retention periods for each category of data.
- Data subject rights: access, rectification, erasure, restriction, portability, objection.
- Right to lodge a complaint with a supervisory authority.
- Cookie information (can reference the consent banner or a separate cookie policy).
Technical Implementation
<!-- Every page must link to the privacy policy -->
<footer>
<a href="/privacy-policy">Privacy Policy</a>
<a href="/imprint">Imprint</a> <!-- Required in DE/AT -->
</footer>
<!-- Privacy policy page must not be blocked by cookie banners -->
<!-- Privacy policy page should be indexable (not noindex) -->
<!-- Privacy policy should load without requiring JavaScript -->Step 5: Configure Google Analytics for GDPR Compliance
Google Analytics 4 (GA4) requires specific configuration to be used lawfully in the EEA.
Enable Google Consent Mode v2
Consent Mode v2 communicates the user's consent state to Google tags, adjusting their behavior accordingly.
<!-- Set default consent state BEFORE loading gtag.js -->
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Default: deny all until user consents
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'functionality_storage': 'denied',
'personalization_storage': 'denied',
'security_storage': 'granted', // security cookies are strictly necessary
'wait_for_update': 500 // wait 500ms for CMP to load
});
</script>
<!-- Load gtag.js -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
gtag('js', new Date());
gtag('config', 'G-XXXXXXX', {
'anonymize_ip': true, // Redundant in GA4 but explicit
'send_page_view': true
});
</script>
<!-- When user grants consent, update the state -->
<script>
function updateConsentState(analyticsGranted, marketingGranted) {
gtag('consent', 'update', {
'analytics_storage': analyticsGranted ? 'granted' : 'denied',
'ad_storage': marketingGranted ? 'granted' : 'denied',
'ad_user_data': marketingGranted ? 'granted' : 'denied',
'ad_personalization': marketingGranted ? 'granted' : 'denied'
});
}
</script>GA4 Settings in the Admin Panel
- Navigate to Admin → Data Streams → your stream → Configure tag settings.
- Disable Google signals data collection if you do not have explicit consent for cross-device tracking.
- Set data retention to the minimum period you need (2 months or 14 months).
- Enable IP anonymization — GA4 does this by default for EEA traffic, but verify the setting.
- Review the Data Processing Amendment in Admin → Account Settings and confirm it is accepted.
Server-Side Tagging (Advanced)
For maximum control over data sent to Google, consider server-side Google Tag Manager. This routes analytics data through your own server, allowing you to strip or redact personal data before it reaches Google:
# Server-side GTM deployment overview:
# 1. Deploy a server-side GTM container (App Engine, Cloud Run, or self-hosted)
# 2. Route the GA4 measurement endpoint through your domain
# 3. Configure the server container to strip client IP, user-agent details
# 4. Forward sanitized data to GA4
# Your domain serves as the analytics endpoint:
# https://yourdomain.com/g/collect → your server container → GA4Step 6: Secure Contact Forms and Data Processing
Contact forms collect personal data directly. They require careful implementation.
Technical Requirements
<!-- Contact form with required GDPR elements -->
<form action="/submit-contact" method="POST">
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<!-- GDPR consent checkbox (must not be pre-checked) -->
<label>
<input type="checkbox" id="privacy-consent" name="privacy_consent" required>
I have read the <a href="/privacy-policy" target="_blank">privacy policy</a>
and agree to the processing of my data for the purpose of responding
to my inquiry. *
</label>
<button type="submit">Send Message</button>
</form>Backend Requirements
- HTTPS only: Form submissions must be encrypted in transit. Redirect all HTTP to HTTPS.
- Minimal data collection: Only request fields you actually need. A phone number field on a general contact form is rarely necessary.
- Consent logging: Store the consent timestamp, the version of the privacy policy accepted, and the IP address (for proof of consent) in your database alongside the form data.
- Auto-deletion: Implement a retention policy. Delete inquiry data after you have responded and the purpose is fulfilled (e.g., 6 months).
- Email security: If form data is sent via email, ensure the email server uses TLS. Consider storing data in a database instead of emailing it in plaintext.
# Example: Server-side consent logging (pseudocode)
def handle_contact_form(request):
if not request.POST.get('privacy_consent'):
return error('Consent required')
submission = {
'name': request.POST['name'],
'email': request.POST['email'],
'message': request.POST['message'],
'consent_given': True,
'consent_timestamp': datetime.utcnow().isoformat(),
'consent_ip': request.META['REMOTE_ADDR'],
'privacy_policy_version': '2024-01-15',
'retention_delete_after': datetime.utcnow() + timedelta(days=180)
}
db.contact_submissions.insert(submission)
return success('Message sent')Step 7: Fix Common Violations
Violation 1: Cookies Set Before Consent
The most frequent violation. Analytics or marketing scripts fire immediately on page load.
Fix: Wrap all non-essential scripts in your consent logic. In Google Tag Manager, use the Consent Overview to ensure tags only fire when the corresponding consent category is granted.
Violation 2: No Reject Option (or Hidden Reject)
Many banners show a prominent "Accept All" button but hide the reject option behind "Manage Preferences" → "Save" with everything unchecked.
Fix: Provide a "Reject All" button at the same level and with equal visual weight as "Accept All."
Violation 3: Third-Party Fonts Loaded Without Consent
Google Fonts loaded from fonts.googleapis.com transmit the visitor's IP to Google servers. A German court ruled this a GDPR violation in January 2022 (LG Munich, Case 3 O 17493/20).
# Fix: Self-host your fonts
# 1. Download fonts from https://gwfh.mranftl.com/fonts or google-webfonts-helper
# 2. Place font files in your /fonts/ directory
# 3. Reference them locally in your CSS:
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/open-sans-v34-latin-regular.woff2') format('woff2');
}
# 4. Remove any <link> tags pointing to fonts.googleapis.comViolation 4: Embedded Content Without Consent
YouTube videos, Google Maps, and social media widgets load third-party cookies and transmit visitor data on page load.
Fix: Use a two-click solution. Display a placeholder with a consent notice. Load the actual embed only after the user clicks to activate it.
Violation 5: Missing or Incomplete Privacy Policy
Fix: Audit your privacy policy against the mandatory sections listed in Step 4. Use a generator as a starting point, then customize it to reflect your actual data processing activities.
Violation 6: No Data Processing Agreement (DPA)
You must have a signed Data Processing Agreement (Art. 28 GDPR) with every third-party processor: your hosting provider, email service, analytics provider, CRM, etc.
Fix: Contact each service provider and request their DPA. Most major providers (AWS, Google, Mailchimp, Cloudflare) offer an online DPA you can accept through their admin panel.
Troubleshooting
Consent Banner Does Not Block Scripts
If cookies still appear before consent, check for:
- Scripts loaded directly in HTML (not through GTM) that bypass the consent check.
- WordPress plugins that inject their own scripts independent of your consent mechanism.
- Cached pages serving old HTML with inline scripts. Clear your CDN and server cache after implementing consent logic.
# Verify no cookies are set before consent:
# 1. Open incognito window
# 2. Open DevTools → Application → Cookies → clear all
# 3. Load your site but do NOT interact with the consent banner
# 4. Check cookies — only strictly necessary cookies should appear
# Check for network requests that should be blocked:
# DevTools → Network → reload page without consent
# Filter by: google-analytics | doubleclick | facebook | hotjar
# If you see requests to these domains, your blocking is incompleteGoogle Analytics Shows No Data After Implementation
- Verify Consent Mode is sending the
consent updatecall by checking the Network tab for requests togoogle-analytics.com/g/collectwith the parametergcs=(consent state). - Use Google Tag Assistant (tagassistant.google.com) to debug tag firing.
- Note: With Consent Mode, Google models data for users who decline consent. You will see modeled data in reports, but it takes time to appear.
WordPress Plugin Conflicts
Cookie consent plugins may conflict with caching plugins or other scripts. Typical resolution steps:
- Exclude the consent script from minification and deferral.
- Ensure the consent script loads before all other non-essential scripts in the HTML.
- Disable JavaScript optimization for consent-related files in your caching plugin settings.
Prevention
Ongoing Compliance Checklist
- Quarterly cookie audits: New plugins, updates, or embedded content can introduce new cookies. Schedule a scan every 3 months.
- Privacy policy reviews: Update your privacy policy whenever you add or remove a service that processes personal data.
- DPA inventory: Maintain a list of all processors and their DPA status. Review annually.
- Staff training: Anyone who manages website content should understand that adding a YouTube embed or a new tracking pixel has GDPR implications.
- Data subject request process: Have a documented procedure for handling access, deletion, and portability requests within the 30-day deadline.
- Records of processing activities (ROPA): Maintain the register required by Art. 30 GDPR. Include your website as a processing activity.
Automated Monitoring
# Set up automated cookie monitoring with a cron job
# This script checks for unexpected cookies weekly
#!/bin/bash
# cookie-monitor.sh
DOMAIN="yourdomain.com"
EXPECTED_COOKIES="PHPSESSID,cookie_consent,csrf_token"
# Use a headless browser to detect cookies set without interaction
npx puppeteer-cli screenshot "https://$DOMAIN" \
--cookie-dump /tmp/cookies.json \
--no-interaction
# Compare found cookies against expected list
python3 - <<PYEOF
import json
expected = set("$EXPECTED_COOKIES".split(','))
with open('/tmp/cookies.json') as f:
found = set(c['name'] for c in json.load(f))
unexpected = found - expected
if unexpected:
print(f"WARNING: Unexpected cookies found: {unexpected}")
# Send alert email or Slack notification
PYEOFBefore Deploying Any Change
- Check if the change introduces new third-party requests or cookies.
- If yes, add the new resource to your consent configuration.
- Update the cookie declaration in your privacy policy or cookie policy.
- Test in an incognito browser: no non-essential cookies before consent.
- Document the change in your records of processing activities.
Disclaimer: This article provides technical guidance for implementing GDPR-related website features. It does not constitute legal advice. Data protection requirements vary by jurisdiction and specific use case. Consult a qualified data protection attorney or your Data Protection Officer for legally binding assessments.
Need Expert Help?
Want a professional GDPR/cookie audit with written fixes? €49.
Book Now — €49100% money-back guarantee