Google Tag Manager for Netlify CMS / Decap CMS | OpsBlu Docs

Google Tag Manager for Netlify CMS / Decap CMS

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

Google Tag Manager (GTM) is particularly valuable for static sites built with Netlify CMS (now Decap CMS) because it allows marketing teams to manage tracking tags without rebuilding the entire site. This guide covers GTM implementation strategies for static site generators.

Why GTM for Static Sites?

Avoid Rebuild for Tag Changes

Traditional approach:

  1. Edit template file to add tracking code
  2. Commit to Git repository
  3. Trigger Netlify build (2-10 minutes)
  4. Wait for deployment
  5. Verify tracking works
  6. If broken, repeat process

GTM approach:

  1. Open GTM interface
  2. Add/modify tag
  3. Preview changes instantly
  4. Publish (takes seconds)
  5. Changes live immediately

Centralized Tag Management

Manage all tracking from one place:

Team Collaboration

Without GTM:

  • Marketing team requests developer to add tracking
  • Developer adds code, creates pull request
  • Code review process
  • Deployment wait time
  • Developer bottleneck for simple changes

With GTM:

  • Marketing team has direct access to GTM
  • No developer involvement for tag changes
  • Built-in version control and rollback
  • Preview mode for testing
  • Approval workflow for changes

GTM vs Direct Implementation

Aspect Direct Implementation Google Tag Manager
Initial Setup Simple (one script block) Complex (container + data layer)
Tag Changes Requires rebuild Instant updates
Performance Minimal overhead Additional container load
Flexibility Limited to hardcoded values Dynamic tag management
Team Access Developer-only Marketing team access
Version Control Git history GTM workspace versions
Testing Preview deploys GTM preview mode
Learning Curve Low Moderate to high

When to Use GTM

Ideal Use Cases

✓ Use GTM when:

  • Marketing team manages multiple tracking tools
  • Frequent tag changes (A/B testing, campaigns)
  • Multiple stakeholders need tag access
  • Complex tag configurations (triggers, variables)
  • Running paid advertising campaigns
  • Need tag debugging tools
  • Compliance requirements (consent mode)

✗ Skip GTM when:

  • Simple blog with just GA4
  • Solo developer managing everything
  • Performance is absolutely critical
  • No tag management complexity
  • Static site is very small (< 10 pages)
  • No marketing team involvement

GTM for Static Site Generators

Hugo + GTM

Advantages:

  • Fast builds mean GTM container doesn't slow down deployments
  • Go templates allow environment-specific container IDs
  • Partial system keeps GTM code isolated

Considerations:

  • Data layer must be injected at build time
  • Content metadata available via Hugo variables
  • No server-side user properties

Jekyll + GTM

Advantages:

  • Simple Liquid syntax for data layer injection
  • GitHub Pages compatible
  • Easy conditional loading

Considerations:

  • Slower builds (but GTM container is static)
  • Limited dynamic data layer values
  • Mostly client-side data layer population

Gatsby + GTM

Advantages:

  • React component ecosystem (gatsby-plugin-gtm)
  • GraphQL for rich data layer population
  • Client-side routing tracked automatically

Considerations:

  • SPA behavior requires special pageview handling
  • Virtual pageviews via History API
  • Plugin configuration can be complex

Next.js + GTM

Advantages:

  • Next.js Script component for optimal loading
  • Server-side rendering for initial data layer
  • Hybrid static/dynamic capabilities

Considerations:

  • Must handle both SSR and CSR contexts
  • Router events for pageview tracking
  • Environment variable management

11ty + GTM

Advantages:

  • Template-agnostic (Nunjucks, Liquid, Pug, etc.)
  • Simple data cascade for GTM ID
  • Minimal JavaScript overhead

Considerations:

  • Manual implementation (no plugin)
  • Data layer built at compile time
  • Limited client-side data layer helpers

GTM Container Structure for Static Sites

Container Setup

Typical container organization:

Variables:
├── Page Path (Built-in)
├── Page URL (Built-in)
├── Referrer (Built-in)
├── Content Category (Data Layer Variable)
├── Author (Data Layer Variable)
├── Publish Date (Data Layer Variable)
└── Environment (Data Layer Variable)

Triggers:
├── All Pages (Pageview)
├── DOM Ready
├── Window Loaded
├── Scroll Depth (25%, 50%, 75%, 90%)
├── Outbound Link Click
├── Download Click (PDF, ZIP, etc.)
└── Form Submission

Tags:
├── GA4 Configuration
├── GA4 - All Pages (Pageview)
├── GA4 - Scroll Tracking
├── GA4 - Outbound Links
├── GA4 - Downloads
├── Meta Pixel - Base Code
├── Meta Pixel - PageView
└── LinkedIn Insight Tag

Preview Deploy Considerations

Problem: Preview URLs pollute analytics with test data.

Solution: Use GTM environments or conditional tags:

// Data layer variable: Environment
function() {
  var hostname = {{Page Hostname}};

  if (hostname.indexOf('deploy-preview') > -1) {
    return 'preview';
  } else if (hostname.indexOf('netlify.app') > -1) {
    return 'staging';
  } else {
    return 'production';
  }
}

// Use in tag triggers
// Fire tag only when Environment equals "production"

Branch Deploy Tracking

Track different Git branches separately:

// Custom JavaScript Variable: Git Branch
function() {
  var hostname = {{Page Hostname}};

  // Extract branch from URL: branch-name--site.netlify.app
  var branchMatch = hostname.match(/^(.+?)--/);

  if (branchMatch) {
    return branchMatch[1]; // Returns branch name
  } else {
    return 'main';
  }
}

// Send as custom dimension in GA4

Data Layer Architecture

Build-Time Data Layer

Inject content metadata when static site builds:

Hugo example:

<!-- layouts/partials/gtm-data-layer.html -->
<script>
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    'pageType': '{{ .Type }}',
    'contentCategory': '{{ .Section }}',
    'author': '{{ .Params.author }}',
    'publishDate': '{{ .Date.Format "2006-01-02" }}',
    'wordCount': {{ .WordCount }},
    'tags': {{ .Params.tags | jsonify }},
    'environment': '{{ getenv "CONTEXT" }}'
  });
</script>

Jekyll example:

<!-- _includes/gtm-data-layer.html -->
<script>
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    'pageType': '{{ page.layout }}',
    'contentCategory': '{{ page.categories | first }}',
    'author': '{{ page.author }}',
    'publishDate': '{{ page.date | date: "%Y-%m-%d" }}',
    'wordCount': {{ page.content | number_of_words }},
    'tags': {{ page.tags | jsonify }}
  });
</script>

Runtime Data Layer

Populate user-specific data client-side:

// Add to page after initial data layer
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'userType': localStorage.getItem('returning_visitor') ? 'returning' : 'new',
  'sessionCount': parseInt(localStorage.getItem('session_count') || '0') + 1,
  'timeOnSite': 0, // Updated by timer
  'scrollDepth': 0 // Updated by scroll listener
});

// Increment session count
var sessions = parseInt(localStorage.getItem('session_count') || '0') + 1;
localStorage.setItem('session_count', sessions);
localStorage.setItem('returning_visitor', 'true');

Performance Optimization

Script Loading Strategy

Synchronous loading (blocking):

<!-- Blocks page rendering until GTM loads -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX');</script>

Asynchronous loading (non-blocking):

<!-- Doesn't block rendering -->
<script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXX"></script>

Deferred loading (optimal for static sites):

// Load GTM after initial page render
function loadGTM() {
  (function(w,d,s,l,i){
    w[l]=w[l]||[];
    w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});
    var f=d.getElementsByTagName(s)[0],
        j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
    j.async=true;
    j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;
    f.parentNode.insertBefore(j,f);
  })(window,document,'script','dataLayer','GTM-XXXXXX');
}

// Load on first user interaction
['mousedown', 'touchstart', 'keydown'].forEach(function(event) {
  window.addEventListener(event, loadGTM, {once: true, passive: true});
});

// Fallback: load after 3 seconds
setTimeout(loadGTM, 3000);

Resource Hints

Preconnect to GTM domains:

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

Container Optimization

Minimize container weight:

  • Remove unused tags
  • Consolidate similar triggers
  • Use custom HTML tags sparingly
  • Leverage built-in tag templates
  • Enable tag sequencing only when needed

SPA Pageview Tracking

Static site generators with client-side routing (Gatsby, Next.js) require special handling:

Gatsby Virtual Pageviews

// gatsby-browser.js
export const location, prevLocation }) => {
  if (prevLocation && typeof window !== 'undefined' && window.dataLayer) {
    window.dataLayer.push({
      'event': 'virtualPageview',
      'pageUrl': location.pathname + location.search,
      'pagePath': location.pathname,
      'pageTitle': document.title
    });
  }
};

GTM trigger:

  • Trigger Type: Custom Event
  • Event Name: virtualPageview
  • Use in GA4 pageview tag

Next.js Router Events

// pages/_app.js
import { useEffect } from 'react';
import { useRouter } from 'next/router';

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

  useEffect(() => {
    const handleRouteChange = (url) => {
      if (typeof window !== 'undefined' && window.dataLayer) {
        window.dataLayer.push({
          'event': 'virtualPageview',
          'pageUrl': url,
          'pageTitle': document.title
        });
      }
    };

    router.events.on('routeChangeComplete', handleRouteChange);

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

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

GTM Consent Mode for GDPR/CCPA compliance:

// Initialize consent before GTM loads
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}

gtag('consent', 'default', {
  'analytics_storage': 'denied',
  'ad_storage': 'denied',
  'wait_for_update': 500
});

// After user accepts cookies
document.addEventListener('cookieConsentAccepted', function() {
  gtag('consent', 'update', {
    'analytics_storage': 'granted',
    'ad_storage': 'granted'
  });
});

Editorial Workflow Considerations

Draft vs Published Tracking

Netlify CMS editorial workflow creates preview deploys for drafts:

Strategy 1: Separate containers

// Hugo partial
{{ if eq (getenv "CONTEXT") "production" }}
  {{ $gtmId := "GTM-PROD-XXX" }}
{{ else }}
  {{ $gtmId := "GTM-DEV-XXX" }}
{{ end }}

Strategy 2: Data layer flag

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'environment': '{{ getenv "CONTEXT" }}', // 'production', 'deploy-preview', 'branch-deploy'
  'contentStatus': '{{ .Draft }}' // true/false in Hugo
});

// In GTM, create trigger:
// Fire tag only when environment equals 'production'

Preview Deploy Testing

Test GTM changes on preview deploys before merging:

  1. Make GTM changes in workspace
  2. Create preview deploy via pull request
  3. Enable GTM preview mode (click Preview in GTM)
  4. Visit preview URL (deploy-preview-123--site.netlify.app)
  5. Verify tags fire correctly using GTM debug pane
  6. Merge pull request to deploy to production
  7. Publish GTM workspace to make tags live

Common GTM Patterns for Static Sites

Built-in variable: Click URL

Trigger:

  • Trigger Type: Click - All Elements
  • This trigger fires on: Some Clicks
  • Click URL does not contain \{\{Page Hostname\}\}

Tag: GA4 Event

  • Event Name: click
  • Event Parameters:
    • event_category: outbound
    • event_label: \{\{Click URL\}\}

File Download Tracking

Trigger:

  • Trigger Type: Click - All Elements
  • This trigger fires on: Some Clicks
  • Click URL matches RegEx: \.(pdf|zip|doc|docx|xls|xlsx)$

Tag: GA4 Event

  • Event Name: file_download
  • Event Parameters:
    • file_name: \{\{Click Text\}\}
    • file_extension: Custom JavaScript variable extracting extension

Form Submission Tracking

Trigger:

  • Trigger Type: Form Submission
  • This trigger fires on: All Forms (or specific form ID)

Tag: GA4 Event

  • Event Name: generate_lead
  • Event Parameters:
    • form_id: \{\{Form ID\}\}
    • form_name: Custom variable

Testing GTM Implementation

Preview Mode

  1. Open GTM container
  2. Click Preview button
  3. Enter your site URL (production or preview deploy)
  4. GTM debugger opens
  5. Interact with site, verify tags fire
  6. Check data layer values

Browser Console

// Check data layer contents
console.log(window.dataLayer);

// Check GTM loaded
console.log(google_tag_manager);

// Push test event
window.dataLayer.push({'event': 'test_event', 'test_param': 'hello'});

Tag Assistant

  1. Install Tag Assistant Chrome extension
  2. Connect to your site
  3. View all tags firing
  4. Verify GTM container loaded
  5. Check GA4, Meta Pixel, other tags

Next Steps