McGarrah Technical Blog

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

13 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 = '{{ site.google_analytics }}';
    const ADSENSE_ID = '{{ site.google_adsense }}';
    
    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();
        }
    }
})();

Notice the Front Matter entries at the top. Those are important to support the lookup of the site.<variables> so we do not hard code them.

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

Chrome Developer 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 beyond “accept all”
  3. Transparency - Clear explanation of what data is collected and why
  4. Easy Withdrawal - Users must be able to change their minds

Initial Success: AdSense Approval

The basic implementation successfully passed Google’s AdSense review on the first submission:

Performance Impact

Why Custom Implementation Over NPM Libraries?

With the basic implementation working, I reflected on the decision to build custom rather than use existing libraries:

Available NPM Options

Several mature libraries exist for cookie consent:

Why Custom Was the Right Choice

Jekyll Integration Challenges

Most libraries expect dynamic backends for configuration. They can’t access Jekyll variables like G-F90DVB199P directly, requiring additional build steps or manual configuration.

Tailored Logic Requirements

My implementation needed specific features:

Performance Benefits

// Custom solution: ~5KB, no additional HTTP requests
// vs
// Library solutions: 13-50KB + CDN request + configuration overhead

Maintenance Advantages

When Libraries Make Sense

Libraries would be better if you need:

The Verdict

For Jekyll static sites with straightforward GDPR needs, a custom implementation offers:

The custom approach was more work upfront but resulted in a more maintainable, performant solution tailored exactly to the use case.

Evolving Requirements: Region-Based Enhancement

With AdSense approval secured, I had time to analyze user behavior and realized a significant UX issue: showing GDPR banners to all users worldwide wasn’t optimal. US users don’t need GDPR consent, and the banner creates unnecessary friction for the vast majority of my traffic.

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 (🇺🇸):

Final Results and Impact

After implementing the complete region-aware GDPR solution:

Enhanced User Experience

EU Visitors (🇪🇺):

US Visitors (🇺🇸):

Performance Improvements

The region-aware implementation provided measurable benefits:

Technical Reliability

The two-tier fallback system proved robust:

Conclusion

Implementing GDPR compliance on Jekyll sites requires careful consideration of static site limitations and user experience. While NPM libraries exist, a custom solution often provides better integration, performance, and maintainability for straightforward use cases.

The key is understanding that GDPR compliance isn’t just about showing a banner—it’s about respecting user privacy through thoughtful technical implementation and transparent communication.

The success of this implementation sparked interest in creating a reusable Jekyll plugin for the community.

Final recommendation: Start with a custom implementation for Jekyll sites unless you have complex enterprise requirements that justify the overhead of external libraries.


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.