Google Analytics 4 for Netlify CMS / Decap CMS | OpsBlu Docs

Google Analytics 4 for Netlify CMS / Decap CMS

Overview of implementing GA4 on static sites built with Netlify CMS or Decap CMS

Implementing Google Analytics 4 on static sites built with Netlify CMS (now Decap CMS) requires a different approach than traditional dynamic CMSs. This guide covers GA4 integration strategies for static site generators commonly used with Netlify/Decap CMS.

GA4 for Static Sites: Key Differences

Build-Time vs Runtime Tracking

Traditional CMS (WordPress, Drupal):

  • Server renders pages on each request
  • Can inject tracking code dynamically
  • Server-side user detection
  • Database-driven content includes tracking

Static Site (Netlify CMS/Decap CMS):

  • Pre-rendered HTML at build time
  • Tracking code embedded in templates
  • Client-side only (no server-side user data)
  • Rebuild required to update tracking code

Implementation Patterns

Static Site Generator Implementation Method Configuration Location Rebuild Required?
Hugo Partial templates config.toml / partials/analytics.html Yes
Jekyll Include files _config.yml / _includes/analytics.html Yes
Gatsby React plugins gatsby-config.js / gatsby-browser.js Yes
Next.js Custom App / Script pages/_app.js / .env.local Yes
11ty Layout templates .eleventy.js / _includes/layouts/base.njk Yes

Why Use GA4 with Netlify CMS?

Content Performance Tracking

Track how content created in Netlify CMS performs:

  • Page views per post - Which blog posts get the most traffic
  • Time on page - Content engagement metrics
  • Scroll depth - How far users read
  • Bounce rate - Content quality indicators

Editorial Workflow Insights

Understand the impact of your content workflow:

  • Pre-publish testing - Track on preview deploys before going live
  • A/B test content - Use branch deploys to compare versions
  • Rollback analytics - Git history + GA4 data = impact visibility

Marketing Attribution

Connect content to business outcomes:

  • Traffic sources - Which channels drive readers to your content
  • Conversions - Newsletter signups, downloads from static site
  • Campaign tracking - UTM parameters in Netlify CMS markdown
  • User journeys - Multi-page paths through content

Static Site Generator Comparison

Hugo + GA4

Pros:

  • Fastest build times (important for frequent CMS updates)
  • Go template conditionals prevent development tracking
  • Partial system makes adding GA4 clean

Cons:

  • Go templating syntax has learning curve
  • No built-in plugin system (manual implementation)
  • Requires understanding Hugo's template lookup order

Typical use: Large sites with frequent content updates, performance-critical blogs

Jekyll + GA4

Pros:

  • Simple Liquid templating
  • Easy conditional loading with jekyll.environment
  • Native to GitHub Pages (popular with Netlify CMS)

Cons:

  • Slower builds than Hugo (Ruby-based)
  • Limited client-side routing (mostly static HTML)
  • Fewer modern framework features

Typical use: Documentation sites, simple blogs, GitHub Pages deployments

Gatsby + GA4

Pros:

  • React ecosystem plugins (gatsby-plugin-google-gtag)
  • Automatic route change tracking (SPA behavior)
  • GraphQL integration for content-based tracking
  • Progressive Web App capabilities

Cons:

  • Heavy build process (long build times)
  • Complex configuration
  • Lots of dependencies
  • Over-engineered for simple sites

Typical use: Complex web apps, e-commerce, sites needing advanced interactivity

Next.js + GA4

Pros:

  • Built-in Script component for optimal loading
  • Server-side rendering + static generation hybrid
  • Easy environment variable management
  • Excellent for dynamic + static content mix

Cons:

  • More complex than pure static generators
  • Requires React knowledge
  • Vercel-optimized (works on Netlify but requires more configuration)
  • API routes add complexity

Typical use: Marketing sites, SaaS landing pages, hybrid static/dynamic apps

11ty (Eleventy) + GA4

Pros:

  • Template language agnostic (use Nunjucks, Liquid, Pug, etc.)
  • Zero-config data cascade for tracking variables
  • Fast builds
  • Minimal JavaScript by default

Cons:

  • Smaller community than Gatsby/Next.js
  • Fewer pre-built plugins
  • Manual implementation required
  • Less documentation

Typical use: Simple sites, developers who want flexibility, JAMstack purists

Common GA4 Use Cases for Netlify CMS

Blog Post Performance

Track which content performs best:

// Data layer with post metadata (injected at build time)
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'content_type': 'blog_post',
  'author': '{{ author }}',
  'category': '{{ category }}',
  'publish_date': '{{ date }}',
  'word_count': {{ wordCount }}
});

Newsletter Signups

Track conversion events:

// Form submission tracking
document.querySelector('#newsletter-form').addEventListener('submit', function(e) {
  e.preventDefault();

  gtag('event', 'generate_lead', {
    'form_location': 'blog_sidebar',
    'post_title': '{{ page.title }}'
  });

  // Then submit form
  this.submit();
});

Content Downloads

Track PDF, ebook, or resource downloads:

// Download tracking
document.querySelectorAll('a[href$=".pdf"]').forEach(function(link) {
  link.addEventListener('click', function() {
    gtag('event', 'file_download', {
      'file_name': this.href.split('/').pop(),
      'file_extension': 'pdf',
      'link_text': this.textContent
    });
  });
});

Track outbound link clicks:

// Outbound link tracking
document.querySelectorAll('a[href^="http"]').forEach(function(link) {
  if (!link.href.includes(window.location.hostname)) {
    link.addEventListener('click', function() {
      gtag('event', 'click', {
        'event_category': 'outbound',
        'event_label': this.href,
        'transport_type': 'beacon'
      });
    });
  }
});

Environment-Specific Tracking

Production vs Preview Tracking

Problem: Preview deploys (deploy-preview-123--site.netlify.app) shouldn't pollute production analytics.

Solution: Use different GA4 properties based on environment:

// Environment detection
const isProduction = window.location.hostname === 'www.yoursite.com';
const isPreview = window.location.hostname.includes('deploy-preview');

let gaId;
if (isProduction) {
  gaId = 'G-PRODUCTION-ID';
} else if (isPreview) {
  gaId = 'G-STAGING-ID';
} else {
  gaId = 'G-DEVELOPMENT-ID';
}

gtag('config', gaId);

Branch Deploy Tracking

Track different branches separately:

// Extract branch name from Netlify URL
// Format: branch-name--site.netlify.app
const hostname = window.location.hostname;
const branchMatch = hostname.match(/^(.+?)--/);
const branchName = branchMatch ? branchMatch[1] : 'main';

gtag('set', 'custom_map', {
  'dimension1': 'git_branch'
});

gtag('event', 'page_view', {
  'git_branch': branchName
});

Performance Considerations

Impact on Core Web Vitals

GA4 tracking affects static site performance:

Optimization Strategies

1. Async Script Loading

<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>

2. Defer Non-Critical Tracking

// Load GA4 after user interaction
let gaLoaded = false;

function loadGA4() {
  if (gaLoaded) return;
  gaLoaded = true;

  const script = document.createElement('script');
  script.src = 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX';
  script.async = true;
  document.head.appendChild(script);

  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
}

// Trigger on first user interaction
['mousedown', 'touchstart', 'keydown'].forEach(event => {
  window.addEventListener(event, loadGA4, {once: true, passive: true});
});

// Fallback after 3 seconds
setTimeout(loadGA4, 3000);

3. Resource Hints

<link rel="preconnect" href="https://www.google-analytics.com">
<link rel="preconnect" href="https://www.googletagmanager.com">

Build-Time Optimization

Minimize build impact:

  • Store tracking IDs in environment variables (no hardcoding)
  • Use conditional compilation (exclude dev environment tracking)
  • Cache templates that include tracking code
  • Single source of truth for tracking code (one partial/include)

Data Layer Architecture

Build-Time Data Layer

Inject content metadata when site builds:

<!-- Hugo example -->
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'page_type': '{{ .Type }}',
  'content_category': '{{ .Section }}',
  'publish_date': '{{ .Date.Format "2006-01-02" }}',
  'last_modified': '{{ .Lastmod.Format "2006-01-02" }}',
  'word_count': {{ .WordCount }},
  'reading_time': {{ .ReadingTime }},
  {{ with .Params.author }}'author': '{{ . }}',{{ end }}
  {{ with .Params.tags }}'tags': {{ . | jsonify }},{{ end }}
});
</script>

Runtime Data Layer

Add user-specific data in browser:

// User engagement data (runtime only)
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'user_type': localStorage.getItem('returning_visitor') ? 'returning' : 'new',
  'session_count': parseInt(localStorage.getItem('session_count') || '0') + 1,
  'scroll_depth': 0, // Updated by scroll listener
  'time_on_page': 0 // Updated by timer
});

Next Steps