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
Scriptcomponent 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
});
});
});
External Link Tracking
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:
- LCP (Largest Contentful Paint) - Script loading blocks rendering
- FID (First Input Delay) - JavaScript execution delays interactivity
- CLS (Cumulative Layout Shift) - Usually minimal impact
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
- Set Up GA4 on Your Static Site - Implementation guides for each framework
- Configure Event Tracking - Custom events for static sites
- Troubleshoot Tracking Issues - Debug static site tracking
Related Resources
- GA4 Fundamentals - Universal GA4 concepts
- GTM for Static Sites - Alternative implementation
- Performance Optimization - Minimize tracking impact