Netlify CMS (now Decap CMS) is a Git-based headless CMS that works with static site generators. Integrating analytics and tracking requires a different approach than traditional CMSs, as content is compiled at build time. This section covers implementation strategies for static site workflows.
Available Integrations
Analytics Platforms
Google Analytics 4 (GA4)
For static sites built with Netlify CMS/Decap CMS, GA4 integration depends on your static site generator and build process:
- Hugo - Partial templates, config.toml parameters
- Jekyll - Include files, _config.yml variables
- Gatsby - React components, gatsby-plugin-google-gtag
- Next.js - Custom App component, next-seo, or GTM
- 11ty/Eleventy - Base layout templates, JavaScript data files
Learn more about Google Analytics setup →
Tag Management
Google Tag Manager (GTM)
GTM provides centralized control over all marketing and analytics tags for static sites. Essential for teams managing multiple tracking tools without rebuilding the site for each change.
- Build-time injection - Add GTM container to base templates
- Script tag placement - Head and body snippets in layout files
- Data layer configuration - Inject content metadata at build time
- Preview deploy testing - Test tags on Netlify preview URLs
Marketing Pixels
Meta Pixel (Facebook Pixel)
Track visitor behavior and optimize Facebook/Instagram advertising campaigns on static sites:
- Template-level implementation - Base layout or header partial
- Static site plugin integration - Framework-specific plugins
- GTM deployment - Manage via Tag Manager (recommended)
- Conversion API (CAPI) - Server-side tracking via Netlify Functions
Learn more about Meta Pixel setup →
Netlify CMS / Decap CMS-Specific Considerations
Git-Based Workflow
Every change triggers a new build and deployment:
- Template changes require rebuild - Adding/updating tracking codes means redeploying
- Content changes trigger builds - Editorial workflow affects deployment frequency
- Branch deploys need separate tracking - Development/staging should use test properties
- Preview deploys complicate tracking - Unique URLs for each preview can inflate analytics
Static Site Generator Integration
Hugo
Hugo uses Go templating with partials and configuration:
# config.toml
[params]
googleAnalytics = "G-XXXXXXXXXX"
googleTagManager = "GTM-XXXXXXX"
facebookPixel = "XXXXXXXXXXXXXXXX"
Implementation pattern:
- Store tracking IDs in
config.toml - Reference in partials:
\{\{ .Site.Params.googleAnalytics \}\} - Conditional loading based on environment:
\{\{ if not .Site.IsServer \}\}
Jekyll
Jekyll uses Liquid templating with includes and configuration:
# _config.yml
google_analytics: G-XXXXXXXXXX
google_tag_manager: GTM-XXXXXXX
facebook_pixel: XXXXXXXXXXXXXXXX
Implementation pattern:
- Store tracking IDs in
_config.yml - Reference in includes:
\{\{ site.google_analytics \}\} - Conditional loading:
{% if jekyll.environment == "production" %}
Gatsby
Gatsby uses React components with plugins:
// gatsby-config.js
{
resolve: `gatsby-plugin-google-gtag`,
options: {
trackingIds: ["G-XXXXXXXXXX"],
gtagConfig: {
anonymize_ip: true,
},
pluginConfig: {
head: true,
},
},
}
Implementation pattern:
- Install npm packages:
gatsby-plugin-google-gtag,gatsby-plugin-facebook-pixel - Configure in
gatsby-config.js - Use React context for data layer values
Next.js
Next.js uses React with custom App component:
// pages/_app.js
import Script from 'next/script'
function MyApp({ Component, pageProps }) {
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
`}
</Script>
<Component {...pageProps} />
</>
)
}
Implementation pattern:
- Use Next.js
Scriptcomponent for optimal loading - Environment variables for tracking IDs:
process.env.NEXT_PUBLIC_GA_ID - Separate tracking for preview/production deployments
Netlify Platform Considerations
Deploy Contexts
Netlify provides different deploy contexts - configure tracking accordingly:
# netlify.toml
[context.production.environment]
GATSBY_GA_TRACKING_ID = "G-PRODUCTION-ID"
GATSBY_GTM_ID = "GTM-PROD-XXX"
[context.deploy-preview.environment]
GATSBY_GA_TRACKING_ID = "G-STAGING-ID"
GATSBY_GTM_ID = "GTM-DEV-XXX"
[context.branch-deploy.environment]
GATSBY_GA_TRACKING_ID = "G-STAGING-ID"
GATSBY_GTM_ID = "GTM-DEV-XXX"
Deploy contexts:
- Production - Main branch, live site tracking
- Deploy Preview - Pull request previews, test tracking property
- Branch Deploy - Feature branches, development tracking
Preview Deploy Tracking
Preview URLs (e.g., deploy-preview-123--site.netlify.app) should use separate analytics properties:
Why separate tracking:
- Prevents preview traffic from inflating production analytics
- Allows testing tracking implementation before merge
- Isolates editorial workflow testing from real user data
Implementation:
// Conditional tracking based on URL
const isPreview = window.location.hostname.includes('deploy-preview');
const gaId = isPreview ? 'G-STAGING-ID' : 'G-PRODUCTION-ID';
Editorial Workflow
Netlify CMS editorial workflow creates branches for content review:
Editorial workflow stages:
- Draft - Content created, saved in CMS
- In Review - Pull request created, preview deploy generated
- Ready - Approved, merged to main branch
- Published - Main branch deployed to production
Tracking implications:
- Draft stage - No deployment, no tracking
- In review - Preview deploy with test tracking
- Published - Production deploy with live tracking
Best practices:
- Test tracking on preview deploys before merging
- Use branch deploys to validate event tracking
- Review Analytics Real-Time reports after merge
Branch Deploys and Staging
# netlify.toml - Deploy all branches to separate URLs
[build]
publish = "public"
command = "hugo"
# Deploy every branch (enables testing tracking changes)
[context.branch-deploy]
command = "hugo --buildDrafts --buildFuture"
Branch deploy strategy:
- Main branch - Production tracking IDs
- Develop branch - Staging tracking IDs
- Feature branches - Test tracking or no tracking
Implementation Approaches
1. Template-Based (Most Common)
Best for: All static site generators, full control, no build overhead
Pros:
- Complete control over implementation
- Minimal build time impact
- No additional dependencies
- Easy to customize per environment
Cons:
- Requires template editing
- Must rebuild to update tracking codes
- Manual implementation for each site generator
- No GUI for non-technical users
2. Plugin-Based (Framework-Specific)
Best for: Gatsby, Next.js, 11ty - frameworks with plugin ecosystems
Pros:
- Easy configuration via config files
- Automatic optimization (script loading, defer/async)
- Community-maintained updates
- Often includes helper functions
Cons:
- Limited to frameworks with plugin systems
- Adds build dependencies
- May not support all tracking features
- Plugin updates can break builds
3. Google Tag Manager (Most Flexible)
Best for: Marketing teams, multiple tracking tools, frequent changes
Pros:
- No rebuild needed for tag changes
- Centralized tag management
- Version control and rollback within GTM
- Built-in debugging tools
Cons:
- Initial GTM container setup requires rebuild
- Requires GTM knowledge
- Additional container load time
- Data layer must be configured at build time
4. Build Hooks + Netlify Functions
Best for: Advanced use cases, server-side tracking, dynamic tracking
Pros:
- Server-side event tracking
- Dynamic tracking without rebuilding
- Conversion API support
- Enhanced privacy compliance
Cons:
- Complex setup
- Requires serverless function knowledge
- Netlify Functions usage limits
- Additional infrastructure
Static Site-Specific Tracking Challenges
Pre-Rendered Content
Static sites generate HTML at build time:
- Dynamic data must be injected client-side
- User-specific tracking requires JavaScript
- Content metadata embedded in data layer at build time
- No server-side variables - can't track logged-in users server-side
Client-Side Routing (SPA)
Frameworks like Gatsby and Next.js use client-side routing:
- Virtual pageviews must be tracked on route changes
- History API navigation doesn't trigger page loads
- Framework-specific listeners needed for route changes
Gatsby example:
// gatsby-browser.js
export const location }) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'page_view', {
page_path: location.pathname + location.search,
});
}
};
Next.js example:
// pages/_app.js
import { useRouter } from 'next/router';
import { useEffect } from 'react';
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const handleRouteChange = (url) => {
window.gtag('config', 'G-XXXXXXXXXX', {
page_path: url,
});
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return <Component {...pageProps} />;
}
Build-Time vs Runtime
Build-time (during deployment):
- Template rendering
- Content compilation
- Environment variable injection
- Asset optimization
Runtime (in user's browser):
- Event tracking
- User interactions
- Dynamic data layer values
- Cookie consent management
What goes where:
- Tracking IDs → Build-time (environment variables)
- Container code → Build-time (templates)
- Event tracking → Runtime (JavaScript)
- User properties → Runtime (cannot be pre-rendered)
Data Privacy and Compliance
Environment-Based Cookie Consent
// Load consent management based on build context
const consentScriptUrl = process.env.GATSBY_DEPLOY_CONTEXT === 'production'
? '/scripts/cookie-consent.js'
: '/scripts/cookie-consent-dev.js';
GDPR Compliance for Static Sites
- Consent before tracking - Load analytics only after user consent
- Cookie banners - Implement via static site framework or third-party service
- Anonymize IPs - Configure in tracking initialization
- Data layer privacy - Don't include PII in build-time data layer
Cookie consent integration:
// Wait for consent before loading GA4
document.addEventListener('cookieConsentAccepted', function() {
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', {
'anonymize_ip': true
});
});
Testing and Validation
Preview Deploy Testing
Test tracking implementation on Netlify preview deploys:
- Create test branch - Make tracking changes in feature branch
- Commit and push - Triggers preview deploy
- Test on preview URL - Verify tracking on
deploy-preview-XXX--site.netlify.app - Check Analytics - Confirm events appear in test GA4 property
- Merge to main - Deploy to production with confidence
Browser Extensions
- Google Analytics Debugger - Console logging for GA hits
- Meta Pixel Helper - Validate Facebook Pixel implementation
- Tag Assistant - Debug Google tags (GA, GTM, Ads)
- ObservePoint - Automated tag auditing
Framework-Specific Testing
- Gatsby - Run
gatsby developlocally, check browser console - Hugo - Run
hugo serverwith--disableFastRenderflag - Jekyll - Run
bundle exec jekyll servewith--livereload - Next.js - Run
npm run dev, check Network tab for tracking hits
Common Issues
See Troubleshooting → Tracking Issues for static site-specific debugging.
Performance Optimization
Resource Hints for Static Sites
<!-- Add to base template head -->
<link rel="preconnect" href="https://www.google-analytics.com">
<link rel="preconnect" href="https://www.googletagmanager.com">
<link rel="dns-prefetch" href="//connect.facebook.net">
Script Loading Strategies
Hugo partial:
{{ if not .Site.IsServer }}
<script async src="https://www.googletagmanager.com/gtag/js?id={{ .Site.Params.googleAnalytics }}"></script>
{{ end }}
Jekyll include:
{% unless jekyll.environment == "development" %}
<script async src="https://www.googletagmanager.com/gtag/js?id={{ site.google_analytics }}"></script>
{% endunless %}
Gatsby plugin (automatic):
// gatsby-config.js - plugin handles async loading
{
resolve: `gatsby-plugin-google-gtag`,
options: {
trackingIds: ["G-XXXXXXXXXX"],
pluginConfig: {
head: true, // Load in <head> but asynchronously
},
},
}
Conditional Loading
Only load tracking on production builds:
// Next.js
const isProd = process.env.NODE_ENV === 'production';
{isProd && (
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_ID}`}
strategy="afterInteractive"
/>
)}
Netlify-Specific Features
Build Plugins
Use Netlify Build Plugins to inject tracking automatically:
# netlify.toml
[[plugins]]
package = "@netlify/plugin-gatsby"
[[plugins]]
package = "netlify-plugin-inline-critical-css"
# Custom plugin to inject environment-specific tracking
[[plugins]]
package = "./plugins/inject-analytics"
[plugins.inputs]
gaId = "G-XXXXXXXXXX"
gtmId = "GTM-XXXXXXX"
Environment Variables
Manage tracking IDs via Netlify UI without code changes:
Site Settings → Build & Deploy → Environment → Environment variables
GATSBY_GA_ID = G-XXXXXXXXXX
GATSBY_GTM_ID = GTM-XXXXXXX
NEXT_PUBLIC_FB_PIXEL = XXXXXXXXXXXXXXXX
Netlify Functions for Server-Side Tracking
Implement Conversion API or Measurement Protocol:
// netlify/functions/track-conversion.js
exports.handler = async (event) => {
const { eventName, eventData } = JSON.parse(event.body);
// Send to GA4 Measurement Protocol
const response = await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${process.env.GA_MEASUREMENT_ID}&api_secret=${process.env.GA_API_SECRET}`,
{
method: 'POST',
body: JSON.stringify({
client_id: eventData.clientId,
events: [{
name: eventName,
params: eventData.params
}]
})
}
);
return {
statusCode: 200,
body: JSON.stringify({ success: true })
};
};
Next Steps
Choose your integration path:
- Google Analytics 4 - Setup Guide
- Google Tag Manager - Installation Guide
- Meta Pixel - Implementation Guide
Related Resources
- Global Analytics Fundamentals - Universal concepts applicable to all platforms
- Netlify CMS Troubleshooting - Common integration issues
- Performance Optimization - Speed considerations for static sites