McGarrah Technical Blog

Implementing GDPR Compliance for Jekyll Sites: A Real-World AdSense Integration Story

12 min read

The GDPR Challenge: When AdSense Review Meets Compliance Reality

When Google AdSense requires GDPR compliance “by tomorrow,” you quickly learn that privacy regulations aren’t just legal checkboxes—they’re complex technical implementations that can make or break your site’s functionality.

This is the story of implementing GDPR compliance on a Jekyll static site in one day, complete with the debugging challenges, false starts, and eventual success that led to AdSense approval.

The Urgent Requirements

The email was clear: Google AdSense review pending, GDPR compliance required immediately. The checklist seemed straightforward:

But as any developer knows, “straightforward” requirements often hide complex implementation details.

The Technical Challenge

Jekyll static sites present unique challenges for GDPR compliance:

  1. No server-side processing - Everything must work client-side
  2. Build-time vs runtime - Jekyll processes templates at build time, but consent happens at runtime
  3. Third-party scripts - Google Analytics and AdSense must load conditionally
  4. No external dependencies - Keep it lightweight and maintainable

Implementation Architecture

The Three-Layer Approach

I designed a three-layer system:

  1. Passive Includes - Jekyll templates that initialize but don’t load scripts
  2. Consent Manager - JavaScript that handles user choices and script loading
  3. Dynamic Loading - Scripts load only after explicit consent

File Structure

├── _includes/
│   ├── cookie-consent.html      # Banner component
│   ├── analytics.html           # Passive Analytics setup
│   └── adsense.html             # Passive AdSense setup
├── assets/
│   ├── css/cookie-consent.css   # Banner styling
│   └── js/cookie-consent.js     # Consent logic
├── _layouts/default.html        # Integration point
└── privacypolicy.md             # GDPR-compliant policy

The Implementation Journey

The banner needed to be more than just a notification—it required three distinct consent levels:

<div id="cookie-consent-banner" class="cookie-consent-banner">
  <div class="cookie-consent-content">
    <p>This site uses cookies to improve your experience and for analytics. 
       <a href="/privacy/">Learn more</a></p>
    <div class="cookie-consent-buttons">
      <button id="cookie-accept">Accept All</button>
      <button id="cookie-necessary">Necessary Only</button>
      <button id="cookie-decline">Decline</button>
    </div>
  </div>
</div>

Key Design Decisions:

The JavaScript needed to handle multiple complex requirements:

---
---
// GDPR Cookie Consent Management
(function() {
    'use strict';
    
    const CONSENT_KEY = 'cookie-consent';
    const GA_ID = 'G-F90DVB199P';
    const ADSENSE_ID = 'ca-pub-2421538118074948';
    
    function loadConsentBasedScripts(consentLevel) {
        // Load Google Analytics conditionally
        if (GA_ID && !document.querySelector('script[src*="googletagmanager.com/gtag"]')) {
            const gaScript = document.createElement('script');
            gaScript.async = true;
            gaScript.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
            gaScript.onload = function() {
                gtag('js', new Date());
                gtag('config', GA_ID);
                gtag('consent', 'update', {
                    'analytics_storage': consentLevel === 'all' ? 'granted' : 'denied',
                    'ad_storage': consentLevel === 'all' ? 'granted' : 'denied'
                });
            };
            document.head.appendChild(gaScript);
        }
        
        // Load AdSense if full consent given
        if (consentLevel === 'all') {
            loadAdSense();
        }
    }
})();

Critical Implementation Details:

Step 3: The Debugging Nightmare

The first implementation seemed to work, but testing revealed multiple issues:

Issue 1: Scripts Loading Unconditionally

Problem: Google Analytics was loading with HTTP 200 status before consent Root Cause: The analytics include was calling gtag() immediately Solution: Made includes truly passive, moved all logic to consent manager

Issue 2: AdSense Showing as “BLOCKED”

Problem: AdSense appeared blocked in Network tab Initial Panic: Thought the implementation was broken Reality Check: BLOCKED status was actually correct—it meant consent was working! Learning: “BLOCKED” before consent = success, HTTP 200 after consent = success

Issue 3: Hardcoded Configuration Values

Problem: JavaScript had hardcoded Google Analytics ID Impact: Not maintainable or reusable Solution: Convert JS to Jekyll-processed file with front matter

Step 4: Privacy Policy Overhaul

The existing privacy policy needed comprehensive GDPR updates:

## Quick Summary

This is a personal blog that tries to be privacy-friendly. We don't collect 
your personal info directly, but we do use Google Analytics (to see what 
people read) and Google AdSense (to show ads). If you leave comments, those 
go through GitHub and follow their privacy rules.

## Your Rights (GDPR)

If you are in the EU, you have the right to:
- **Access**: Request information about data we process
- **Rectification**: Correct inaccurate personal data
- **Erasure**: Request deletion of your personal data
- **Portability**: Receive your data in a structured format
- **Object**: Object to processing of your personal data
- **Withdraw Consent**: Withdraw consent for cookie usage at any time

Key Additions:

Testing and Debugging Process

The Testing Protocol

Testing GDPR compliance requires systematic verification:

# 1. Start Jekyll development server
bundle exec jekyll serve --livereload

# 2. Open Chrome incognito window
# Navigate to http://localhost:4000

# 3. Open DevTools (F12) → Network tab
# Reload page (banner should appear)

# 4. Verify BEFORE consent:
# - adsbygoogle.js should be BLOCKED or absent
# - gtag/js should be BLOCKED or absent

# 5. Click "Accept All"

# 6. Verify AFTER consent:
# - Both scripts should load with HTTP 200
# - Banner should disappear

Console Commands for Testing

// Check current consent status
localStorage.getItem('cookie-consent')

// Clear consent (banner should reappear)
localStorage.removeItem('cookie-consent')

// Verify script loading
typeof gtag  // 'undefined' before, 'function' after consent
document.querySelector('script[src*="adsbygoogle"]')  // null before, element after

Common Testing Pitfalls

  1. Using regular browser instead of incognito - Cached consent masks issues
  2. Not clearing localStorage between tests - Previous consent affects results
  3. Misinterpreting “BLOCKED” status - It’s actually the desired behavior
  4. Testing only happy path - Need to test consent withdrawal too

Lessons Learned

Technical Insights

  1. Jekyll Processing is Powerful - Front matter in JS files enables dynamic configuration
  2. Passive Includes Work Better - Let consent manager handle all script loading
  3. Testing is Critical - GDPR compliance isn’t “set and forget”
  4. Documentation Matters - Complex implementations need thorough documentation

GDPR Implementation Principles

  1. Consent Before Collection - No tracking scripts until explicit consent
  2. Granular Choices - Users need meaningful options, not just “accept all”
  3. Easy Withdrawal - Consent removal must be as easy as consent giving
  4. Transparency - Clear language about what data is collected and why

Jekyll-Specific Considerations

  1. Build vs Runtime - Understand what happens when
  2. Static Site Limitations - Everything must work client-side
  3. Configuration Management - Use Jekyll variables for maintainability
  4. Performance Impact - Conditional loading can improve performance

Advanced Enhancement: Region-Based GDPR Detection

After the initial implementation, I realized that showing GDPR banners to all users worldwide wasn’t optimal. US users don’t need GDPR consent, and the banner creates unnecessary friction.

The Region Detection Solution

I implemented intelligent region detection that:

// EU countries requiring GDPR consent
const EU_COUNTRIES = ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB', 'IS', 'LI', 'NO'];

async function checkUserRegion() {
    try {
        // Primary: Geolocation API
        const response = await fetch('https://ipapi.co/json/', { timeout: 3000 });
        const data = await response.json();
        return EU_COUNTRIES.includes(data.country_code);
    } catch (error) {
        // Fallback: Timezone detection
        const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
        const euTimezones = ['Europe/', 'Atlantic/Reykjavik', 'Atlantic/Canary'];
        return euTimezones.some(tz => timezone.startsWith(tz));
    }
}

async function initConsent() {
    const consent = getConsent();
    const isEU = await checkUserRegion();
    
    if (!consent) {
        if (isEU) {
            showBanner(); // EU users see consent banner
        } else {
            setConsent('all'); // US users auto-consent
            return;
        }
    } else {
        loadConsentBasedScripts(consent);
    }
}

Detection Strategy

Primary Method: Geolocation API

Fallback Method: Timezone Detection

User Experience Impact

EU Visitors (🇪🇺):

US Visitors (🇺🇸):

The Final Architecture

The completed system provides:

Results and Impact

AdSense Review Success

The implementation passed Google’s AdSense review on the first submission. Key factors:

Performance Benefits

The region-aware implementation provides multiple performance improvements:

User Experience

The region-aware system optimizes UX for different audiences:

EU Users:

US Users:

Code Repository

All implementation files are available in the site repository:

Testing the Region Detection

Manual Testing Methods

// Force EU detection for testing
localStorage.setItem('test-region', 'EU');
location.reload(); // Banner should appear

// Force US detection for testing  
localStorage.setItem('test-region', 'US');
location.reload(); // No banner, auto-consent

// Clear test overrides
localStorage.removeItem('test-region');

VPN Testing

Conclusion

Implementing region-aware GDPR compliance on Jekyll sites demonstrates that privacy regulations can be both legally compliant and user-friendly. The evolution from universal consent to targeted compliance shows the importance of iterative improvement.

The key insights:

GDPR compliance isn’t just about avoiding fines—it’s about respecting user privacy while maintaining optimal site functionality. The region-aware approach proves that you can have both legal compliance and excellent user experience.

The implementation went from basic compliance to sophisticated region detection, showing how privacy features can evolve to serve users better while maintaining regulatory compliance.

Implementation Timeline

Day 1: Basic GDPR Compliance

Day 2: Region-Aware Enhancement


This implementation was completed in September 2025 for Google AdSense review compliance. The site successfully passed review and maintains full GDPR compliance while providing an optimized user experience based on visitor location.