Installing Google Analytics 4 on Netlify CMS / Decap CMS | OpsBlu Docs

Installing Google Analytics 4 on Netlify CMS / Decap CMS

Complete guide to implementing GA4 on static sites built with Hugo, Jekyll, Gatsby, Next.js, and 11ty

This guide provides step-by-step instructions for implementing Google Analytics 4 on static sites built with Netlify CMS (now Decap CMS) across different static site generators.

Prerequisites

Before installing GA4:

  1. Create a GA4 Property in Google Analytics

    • Sign in to analytics.google.com
    • Create a new GA4 property
    • Copy your Measurement ID (format: G-XXXXXXXXXX)
  2. Have a Netlify CMS site using one of these static site generators:

  3. Set up environment variables in Netlify:

    • Production Measurement ID
    • Staging/Preview Measurement ID (optional but recommended)

Method 1: Hugo + GA4

Step 1: Configure Environment Variables

Add to netlify.toml:

[context.production.environment]
  HUGO_GA_ID = "G-PRODUCTION-ID"

[context.deploy-preview.environment]
  HUGO_GA_ID = "G-STAGING-ID"

[context.branch-deploy.environment]
  HUGO_GA_ID = "G-STAGING-ID"

Step 2: Create Analytics Partial

Create layouts/partials/analytics.html:

{{ if not .Site.IsServer }}
  {{ with getenv "HUGO_GA_ID" }}
    <!-- Google Analytics 4 -->
    <script async src="https://www.googletagmanager.com/gtag/js?id={{ . }}"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', '{{ . }}', {
        'anonymize_ip': true,
        'send_page_view': true
      });

      // Inject content metadata
      {{ with $.Params.author }}
      gtag('set', 'user_properties', {
        'content_author': '{{ . }}'
      });
      {{ end }}

      {{ with $.Section }}
      gtag('event', 'page_view', {
        'content_category': '{{ . }}'
      });
      {{ end }}
    </script>
  {{ end }}
{{ end }}

Step 3: Include in Base Template

Add to layouts/_default/baseof.html (or your base template):

<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}">
  <head>
    <meta charset="UTF-8">
    <title>{{ .Title }}</title>
    {{ partial "analytics.html" . }}
  </head>
  <body>
    {{ block "main" . }}{{ end }}
  </body>
</html>

Step 4: Optional - Enhanced Content Tracking

Create layouts/partials/analytics-data-layer.html:

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

Include before the main analytics partial in baseof.html.

Step 5: Deploy and Verify

# Commit changes
git add .
git commit -m "Add GA4 tracking"
git push origin main

# Netlify will rebuild and deploy
# Check Real-Time reports in GA4

Method 2: Jekyll + GA4

Step 1: Configure Tracking ID

Add to _config.yml:

google_analytics: G-XXXXXXXXXX

# Or use environment-specific config
production:
  google_analytics: G-PRODUCTION-ID

development:
  google_analytics: G-STAGING-ID

Step 2: Create Analytics Include

Create _includes/analytics.html:

{% if jekyll.environment == "production" %}
  {% if site.google_analytics %}
    <!-- Google Analytics 4 -->
    <script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', '{{ site.google_analytics }}', {
        'anonymize_ip': true
      });

      {% if page.author %}
      gtag('set', 'user_properties', {
        'content_author': '{{ page.author }}'
      });
      {% endif %}

      {% if page.categories %}
      gtag('event', 'page_view', {
        'content_category': '{{ page.categories | first }}'
      });
      {% endif %}
    </script>
  {% endif %}
{% endif %}

Step 3: Include in Default Layout

Add to _layouts/default.html:

<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
  <head>
    <meta charset="UTF-8">
    <title>{{ page.title | default: site.title }}</title>
    {% include analytics.html %}
  </head>
  <body>
    {{ content }}
  </body>
</html>

Step 4: Configure Netlify Environment

In Netlify UI:

Site Settings → Build & Deploy → Environment

JEKYLL_ENV = production

Or in netlify.toml:

[build]
  command = "JEKYLL_ENV=production bundle exec jekyll build"
  publish = "_site"

[context.production.environment]
  JEKYLL_ENV = "production"

[context.deploy-preview.environment]
  JEKYLL_ENV = "development"

Step 5: Deploy and Test

# Local testing (no tracking)
bundle exec jekyll serve

# Production deploy
git add .
git commit -m "Add GA4 tracking"
git push origin main

Method 3: Gatsby + GA4

Step 1: Install Plugin

npm install gatsby-plugin-google-gtag

Step 2: Configure Plugin

Add to gatsby-config.js:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-google-gtag`,
      options: {
        // Use environment variables
        trackingIds: [
          process.env.GATSBY_GA_MEASUREMENT_ID, // GA4 Measurement ID
        ],
        // This object gets passed directly to the gtag config command
        gtagConfig: {
          anonymize_ip: true,
          cookie_expires: 0,
        },
        // This object is used for configuration specific to this plugin
        pluginConfig: {
          // Puts tracking script in the head instead of the body
          head: true,
          // Setting this parameter is also optional
          respectDNT: true,
          // Avoids sending pageview hits from custom paths
          exclude: ["/preview/**", "/do-not-track/me/too/"],
          // Delays processing pageview events on route update (in milliseconds)
          delayOnRouteUpdate: 0,
        },
      },
    },
  ],
};

Step 3: Set Environment Variables

Create .env.production:

GATSBY_GA_MEASUREMENT_ID=G-PRODUCTION-ID

Create .env.development:

GATSBY_GA_MEASUREMENT_ID=G-STAGING-ID

Add to Netlify environment variables:

Site Settings → Build & Deploy → Environment

GATSBY_GA_MEASUREMENT_ID = G-PRODUCTION-ID

Step 4: Configure Route Change Tracking

Create gatsby-browser.js (or add to existing):

// Track page views on route changes (SPA behavior)
export const location, prevLocation }) => {
  if (process.env.NODE_ENV === 'production' && typeof window.gtag === 'function') {
    // Don't track initial page load (plugin handles this)
    if (prevLocation) {
      window.gtag('event', 'page_view', {
        page_path: location.pathname + location.search,
        page_location: window.location.href,
      });
    }
  }
};

Step 5: Optional - Content Metadata Tracking

Create src/components/Analytics.js:

import { useEffect } from 'react';

const Analytics = ({ pageContext }) => {
  useEffect(() => {
    if (typeof window.gtag !== 'undefined') {
      // Send custom dimensions
      window.gtag('event', 'page_metadata', {
        content_type: pageContext.type || 'page',
        author: pageContext.author || 'unknown',
        publish_date: pageContext.date || '',
        category: pageContext.category || '',
      });
    }
  }, [pageContext]);

  return null;
};

export default Analytics;

Use in page templates:

import Analytics from '../components/Analytics';

const BlogPost = ({ data, pageContext }) => {
  return (
    <>
      <Analytics pageContext={pageContext} />
      {/* Page content */}
    </>
  );
};

Step 6: Build and Deploy

# Local development (uses staging ID)
gatsby develop

# Production build
gatsby build
gatsby serve

# Deploy
git add .
git commit -m "Add GA4 tracking"
git push origin main

Method 4: Next.js + GA4

Step 1: Create Analytics Component

Create lib/analytics.js:

// lib/analytics.js
export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_ID;

// Log page views
export const pageview = (url) => {
  if (typeof window.gtag !== 'undefined') {
    window.gtag('config', GA_MEASUREMENT_ID, {
      page_path: url,
    });
  }
};

// Log specific events
export const event = ({ action, category, label, value }) => {
  if (typeof window.gtag !== 'undefined') {
    window.gtag('event', action, {
      event_category: category,
      event_label: label,
      value: value,
    });
  }
};

Step 2: Add to _app.js

Modify pages/_app.js:

import Script from 'next/script';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import * as gtag from '../lib/analytics';

function MyApp({ Component, pageProps }) {
  const router = useRouter();

  useEffect(() => {
    const handleRouteChange = (url) => {
      gtag.pageview(url);
    };

    router.events.on('routeChangeComplete', handleRouteChange);
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange);
    };
  }, [router.events]);

  return (
    <>
      {/* Google Analytics */}
      {process.env.NODE_ENV === 'production' && (
        <>
          <Script
            strategy="afterInteractive"
            src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_MEASUREMENT_ID}`}
          />
          <Script
            id="google-analytics"
            strategy="afterInteractive"
            dangerouslySetInnerHTML={{
              __html: `
                window.dataLayer = window.dataLayer || [];
                function gtag(){dataLayer.push(arguments);}
                gtag('js', new Date());
                gtag('config', '${gtag.GA_MEASUREMENT_ID}', {
                  page_path: window.location.pathname,
                  anonymize_ip: true
                });
              `,
            }}
          />
        </>
      )}

      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

Step 3: Environment Variables

Create .env.local:

NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX

Add to Netlify:

# netlify.toml
[context.production.environment]
  NEXT_PUBLIC_GA_ID = "G-PRODUCTION-ID"

[context.deploy-preview.environment]
  NEXT_PUBLIC_GA_ID = "G-STAGING-ID"

Step 4: Track Events in Components

import * as gtag from '../lib/analytics';

const NewsletterForm = () => {
  const handleSubmit = (e) => {
    e.preventDefault();

    // Track conversion
    gtag.event({
      action: 'generate_lead',
      category: 'Newsletter',
      label: 'Footer Signup',
    });

    // Submit form
    // ...
  };

  return (
    <form
      {/* Form fields */}
    </form>
  );
};

Step 5: Deploy

# Development
npm run dev

# Build and test
npm run build
npm run start

# Deploy
git add .
git commit -m "Add GA4 tracking"
git push origin main

Method 5: 11ty (Eleventy) + GA4

Step 1: Configure Data File

Create _data/site.js:

module.exports = {
  title: "My Site",
  url: "https://example.com",
  analytics: {
    ga4: process.env.GA_MEASUREMENT_ID || "G-XXXXXXXXXX"
  }
};

Step 2: Create Analytics Partial

Create _includes/analytics.njk (Nunjucks):

{% if site.analytics.ga4 and not env.local %}
  <!-- Google Analytics 4 -->
  <script async src="https://www.googletagmanager.com/gtag/js?id={{ site.analytics.ga4 }}"></script>
  <script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', '{{ site.analytics.ga4 }}', {
      'anonymize_ip': true
    });

    {% if author %}
    gtag('set', 'user_properties', {
      'content_author': '{{ author }}'
    });
    {% endif %}

    {% if category %}
    gtag('event', 'page_view', {
      'content_category': '{{ category }}'
    });
    {% endif %}
  </script>
{% endif %}

Step 3: Include in Base Layout

Modify _includes/layouts/base.njk:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>{{ title }}</title>
    {% include "analytics.njk" %}
  </head>
  <body>
    {{ content | safe }}
  </body>
</html>

Step 4: Configure Environment

Add to .eleventy.js:

module.exports = function(eleventyConfig) {
  // Pass environment to templates
  eleventyConfig.addGlobalData("env", {
    local: process.env.ELEVENTY_ENV === 'development'
  });

  return {
    dir: {
      input: "src",
      output: "_site",
      includes: "_includes",
      data: "_data"
    }
  };
};

Create netlify.toml:

[build]
  command = "npm run build"
  publish = "_site"

[build.environment]
  GA_MEASUREMENT_ID = "G-PRODUCTION-ID"

[context.deploy-preview.environment]
  GA_MEASUREMENT_ID = "G-STAGING-ID"

Step 5: Build and Deploy

# Development (no tracking)
ELEVENTY_ENV=development npx @11ty/eleventy --serve

# Production build
npx @11ty/eleventy

# Deploy
git add .
git commit -m "Add GA4 tracking"
git push origin main

Verification Steps

1. Check Real-Time Reports

After deploying:

  1. Visit your live site
  2. Open Google Analytics → Reports → Realtime
  3. Confirm pageview appears within 30 seconds

2. Browser Developer Tools

// Open browser console
console.log(window.gtag); // Should be a function
console.log(window.dataLayer); // Should be an array

// Manually fire test event
gtag('event', 'test_event', { 'test_parameter': 'hello' });

3. Network Tab

  1. Open DevTools → Network tab
  2. Filter by "collect" or "gtag"
  3. Reload page
  4. Verify requests to www.google-analytics.com

4. Tag Assistant

  1. Install Tag Assistant Chrome Extension
  2. Visit your site
  3. Click Tag Assistant icon
  4. Verify GA4 tag is present and firing

Common Setup Issues

Tracking Not Working on Localhost

Issue: GA4 doesn't fire during local development.

Solution: This is intentional. Use conditional loading:

// Hugo
{{ if not .Site.IsServer }}

// Jekyll
{% if jekyll.environment == "production" %}

// Gatsby/Next.js
if (process.env.NODE_ENV === 'production')

Environment Variables Not Loading

Issue: process.env.GA_ID is undefined.

Solution:

  • Gatsby: Prefix with GATSBY_
  • Next.js: Prefix with NEXT_PUBLIC_
  • Hugo/Jekyll: Use Netlify build environment variables
  • Verify in Netlify UI: Site Settings → Build & Deploy → Environment

Preview Deploys Polluting Production Data

Issue: Preview URLs send data to production GA4.

Solution: Use environment-specific tracking IDs:

# netlify.toml
[context.production.environment]
  GA_ID = "G-PRODUCTION-ID"

[context.deploy-preview.environment]
  GA_ID = "G-STAGING-ID"

Duplicate Pageviews

Issue: SPA frameworks (Gatsby, Next.js) send multiple pageviews per route change.

Solution: Remove send_page_view: true from config, handle manually:

// Gatsby: Use onRouteUpdate
// Next.js: Use router.events listener

Next Steps