Custom event tracking on static sites requires client-side JavaScript since there's no server-side processing. This guide covers implementing GA4 events across different static site generators used with Netlify CMS.
Event Tracking Fundamentals for Static Sites
Build-Time vs Runtime Events
Build-Time (Template Injection):
- Content metadata (author, category, publish date)
- Page types (blog post, landing page, documentation)
- Static properties (word count, reading time)
Runtime (JavaScript):
- User interactions (clicks, scrolls, form submissions)
- Time-based events (time on page, session duration)
- Dynamic behavior (video plays, tab switches)
Event Naming Conventions
Follow GA4 recommended event structure:
gtag('event', 'event_name', {
'parameter_1': 'value',
'parameter_2': 'value'
});
Recommended events for content sites:
page_view- Automatic (SPA route changes)scroll- User scrolls 90%click- Outbound links, buttons, CTAsfile_download- PDFs, ebooks, resourcesform_submit- Newsletter, contact formsvideo_play/video_complete- Embedded videossearch- Site search usageshare- Social sharing buttons
Common Event Implementations
1. Outbound Link Tracking
Track clicks on external links:
// Universal implementation (works on all static site generators)
document.addEventListener('DOMContentLoaded', function() {
// Select all external links
document.querySelectorAll('a[href^="http"]').forEach(function(link) {
// Check if link is external
if (!link.href.includes(window.location.hostname)) {
link.addEventListener('click', function(e) {
gtag('event', 'click', {
'event_category': 'outbound',
'event_label': this.href,
'transport_type': 'beacon',
'event_callback': function() {
// Fallback if beacon fails
}
});
});
}
});
});
Hugo partial (layouts/partials/analytics-events.html):
<script>
// Outbound link tracking
document.addEventListener('DOMContentLoaded', function() {
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,
'link_text': this.textContent,
'page_location': window.location.pathname
});
});
}
});
});
</script>
2. File Download Tracking
Track PDF, ebook, and resource downloads:
// Track all downloadable file types
const downloadExtensions = ['.pdf', '.zip', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a').forEach(function(link) {
const href = link.href.toLowerCase();
const hasDownloadableExtension = downloadExtensions.some(ext => href.endsWith(ext));
if (hasDownloadableExtension) {
link.addEventListener('click', function() {
const fileName = this.href.split('/').pop();
const fileExtension = fileName.split('.').pop();
gtag('event', 'file_download', {
'file_name': fileName,
'file_extension': fileExtension,
'link_text': this.textContent,
'link_url': this.href
});
});
}
});
});
Jekyll include (_includes/analytics-downloads.html):
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a[href$=".pdf"], a[href$=".zip"], a[href$=".doc"]').forEach(function(link) {
link.addEventListener('click', function() {
gtag('event', 'file_download', {
'file_name': this.href.split('/').pop(),
'file_extension': this.href.split('.').pop(),
'page_title': '{{ page.title }}'
});
});
});
});
</script>
3. Scroll Depth Tracking
Track when users scroll to specific page depths:
// Track 25%, 50%, 75%, and 90% scroll depths
let scrollMarks = {
25: false,
50: false,
75: false,
90: false
};
function calculateScrollDepth() {
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollPercent = Math.floor((scrollTop / (documentHeight - windowHeight)) * 100);
for (let mark in scrollMarks) {
if (!scrollMarks[mark] && scrollPercent >= mark) {
scrollMarks[mark] = true;
gtag('event', 'scroll', {
'event_category': 'engagement',
'event_label': mark + '%',
'value': parseInt(mark),
'page_title': document.title
});
}
}
}
// Debounce scroll events (performance optimization)
let scrollTimeout;
window.addEventListener('scroll', function() {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(calculateScrollDepth, 100);
}, { passive: true });
Gatsby component (src/components/ScrollTracking.js):
import { useEffect, useState } from 'react';
const ScrollTracking = () => {
const [scrollMarks] = useState({ 25: false, 50: false, 75: false, 90: false });
useEffect(() => {
const handleScroll = () => {
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const scrollTop = window.pageYOffset;
const scrollPercent = Math.floor((scrollTop / (documentHeight - windowHeight)) * 100);
Object.keys(scrollMarks).forEach(mark => {
if (!scrollMarks[mark] && scrollPercent >= parseInt(mark)) {
scrollMarks[mark] = true;
if (typeof window.gtag !== 'undefined') {
window.gtag('event', 'scroll', {
'event_category': 'engagement',
'event_label': `${mark}%`,
'value': parseInt(mark)
});
}
}
});
};
let timeout;
const debouncedScroll = () => {
clearTimeout(timeout);
timeout = setTimeout(handleScroll, 100);
};
window.addEventListener('scroll', debouncedScroll, { passive: true });
return () => {
window.removeEventListener('scroll', debouncedScroll);
};
}, [scrollMarks]);
return null;
};
export default ScrollTracking;
4. Form Submission Tracking
Track newsletter signups and contact forms:
// Newsletter form tracking
document.addEventListener('DOMContentLoaded', function() {
const newsletterForm = document.querySelector('#newsletter-form');
if (newsletterForm) {
newsletterForm.addEventListener('submit', function(e) {
e.preventDefault();
gtag('event', 'generate_lead', {
'event_category': 'forms',
'event_label': 'newsletter_signup',
'form_location': 'sidebar', // or footer, popup, etc.
'value': 1
});
// Then submit form
this.submit();
});
}
});
Next.js component example:
import { useState } from 'react';
import * as gtag from '../lib/analytics';
const NewsletterForm = ({ location = 'footer' }) => {
const [email, setEmail] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
// Track conversion
gtag.event({
action: 'generate_lead',
category: 'Newsletter',
label: location,
value: 1
});
// Submit to API
try {
await fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify({ email })
});
alert('Subscribed!');
} catch (error) {
console.error('Subscription failed', error);
}
};
return (
<form
<input
type="email"
value={email} => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit">Subscribe</button>
</form>
);
};
export default NewsletterForm;
5. CTA Button Tracking
Track specific call-to-action buttons:
// Track CTA clicks with detailed parameters
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.cta-button').forEach(function(button) {
button.addEventListener('click', function() {
gtag('event', 'cta_click', {
'event_category': 'engagement',
'button_text': this.textContent.trim(),
'button_location': this.getAttribute('data-location') || 'unknown',
'button_url': this.href || '',
'page_title': document.title
});
});
});
});
Hugo shortcode (layouts/shortcodes/cta-button.html):
<a
href="{{ .Get "url" }}"
class="cta-button"
data-location="{{ .Get "location" }}" 'cta_click', {
'button_text': '{{ .Get "text" }}',
'button_location': '{{ .Get "location" }}',
'button_url': '{{ .Get "url" }}'
});"
>
{{ .Get "text" }}
</a>
Usage in content:
{{< cta-button url="/pricing" text="Get Started" location="hero" >}}
6. Video Tracking
Track embedded video engagement:
// YouTube video tracking
document.addEventListener('DOMContentLoaded', function() {
// Load YouTube IFrame API
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
});
function onYouTubeIframeAPIReady() {
const videos = document.querySelectorAll('iframe[src*="youtube.com"]');
videos.forEach(function(iframe, index) {
const player = new YT.Player(iframe, {
events: {
'onStateChange': function(event) {
const videoUrl = event.target.getVideoUrl();
const videoTitle = event.target.getVideoData().title;
// Track play
if (event.data === YT.PlayerState.PLAYING) {
gtag('event', 'video_start', {
'event_category': 'video',
'event_label': videoTitle,
'video_url': videoUrl
});
}
// Track complete
if (event.data === YT.PlayerState.ENDED) {
gtag('event', 'video_complete', {
'event_category': 'video',
'event_label': videoTitle,
'video_url': videoUrl
});
}
}
}
});
});
}
7. Search Tracking
Track site search usage:
// Track search form submissions
document.addEventListener('DOMContentLoaded', function() {
const searchForm = document.querySelector('#search-form');
if (searchForm) {
searchForm.addEventListener('submit', function(e) {
const searchInput = this.querySelector('input[name="q"], input[name="search"]');
const searchTerm = searchInput ? searchInput.value : '';
gtag('event', 'search', {
'search_term': searchTerm
});
});
}
});
11ty implementation:
<!-- _includes/search-form.njk -->
<form id="search-form" action="/search" method="get">
<input type="text" name="q" placeholder="Search...">
<button type="submit">Search</button>
</form>
<script>
document.getElementById('search-form').addEventListener('submit', function() {
const searchTerm = this.querySelector('input[name="q"]').value;
gtag('event', 'search', { 'search_term': searchTerm });
});
</script>
8. Social Share Tracking
Track social media sharing:
// Track social share clicks
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.social-share-button').forEach(function(button) {
button.addEventListener('click', function() {
const platform = this.getAttribute('data-platform');
gtag('event', 'share', {
'method': platform, // 'twitter', 'facebook', 'linkedin'
'content_type': 'article',
'item_id': window.location.pathname
});
});
});
});
Jekyll include (_includes/social-share.html):
<div class="social-share">
<a href="https://twitter.com/intent/tweet?url={{ page.url | absolute_url }}&text={{ page.title }}"
class="social-share-button"
data-platform="twitter"
target="_blank" 'share', {'method': 'twitter', 'content_type': 'article'});">
Share on Twitter
</a>
<a href="https://www.facebook.com/sharer/sharer.php?u={{ page.url | absolute_url }}"
class="social-share-button"
data-platform="facebook"
target="_blank" 'share', {'method': 'facebook', 'content_type': 'article'});">
Share on Facebook
</a>
<a href="https://www.linkedin.com/shareArticle?mini=true&url={{ page.url | absolute_url }}&title={{ page.title }}"
class="social-share-button"
data-platform="linkedin"
target="_blank" 'share', {'method': 'linkedin', 'content_type': 'article'});">
Share on LinkedIn
</a>
</div>
Framework-Specific Implementations
Gatsby Event Tracking
Using React hooks:
// src/hooks/useAnalytics.js
import { useEffect } from 'react';
export const useAnalytics = () => {
const trackEvent = (eventName, eventParams) => {
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
window.gtag('event', eventName, eventParams);
}
};
return { trackEvent };
};
// Usage in component
import { useAnalytics } from '../hooks/useAnalytics';
const DownloadButton = ({ fileName }) => {
const { trackEvent } = useAnalytics();
const handleClick = () => {
trackEvent('file_download', {
'file_name': fileName,
'file_extension': fileName.split('.').pop()
});
};
return <button {fileName}</button>;
};
Next.js Event Tracking
Using custom hook:
// hooks/useTracking.js
import { useCallback } from 'react';
import * as gtag from '../lib/analytics';
export const useTracking = () => {
const trackClick = useCallback((label, category = 'engagement') => {
gtag.event({
action: 'click',
category,
label
});
}, []);
const trackDownload = useCallback((fileName) => {
gtag.event({
action: 'file_download',
category: 'downloads',
label: fileName
});
}, []);
const trackFormSubmit = useCallback((formName) => {
gtag.event({
action: 'generate_lead',
category: 'forms',
label: formName
});
}, []);
return { trackClick, trackDownload, trackFormSubmit };
};
// Usage
import { useTracking } from '../hooks/useTracking';
const ContactForm = () => {
const { trackFormSubmit } = useTracking();
const handleSubmit = (e) => {
e.preventDefault();
trackFormSubmit('contact_form');
// Submit logic...
};
return <form
};
Content Metadata Events
Build-Time Content Properties
Inject content metadata when pages are built:
Hugo example:
<!-- layouts/_default/single.html -->
<script>
// Track content metadata on page load
gtag('event', 'page_view', {
'content_type': '{{ .Type }}',
'content_category': '{{ .Section }}',
'author': '{{ .Params.author }}',
'publish_date': '{{ .Date.Format "2006-01-02" }}',
'word_count': {{ .WordCount }},
'reading_time': {{ .ReadingTime }},
{{ if .Params.tags }}'tags': {{ .Params.tags | jsonify }}{{ end }}
});
</script>
Jekyll example:
<!-- _layouts/post.html -->
<script>
gtag('event', 'page_view', {
'content_type': 'blog_post',
'content_category': '{{ page.categories | first }}',
'author': '{{ page.author }}',
'publish_date': '{{ page.date | date: "%Y-%m-%d" }}',
'word_count': {{ page.content | number_of_words }}
});
</script>
Event Tracking Best Practices
1. Debounce High-Frequency Events
Prevent excessive event firing (especially scroll events):
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Usage
const handleScroll = debounce(() => {
// Track scroll
}, 100);
window.addEventListener('scroll', handleScroll, { passive: true });
2. Use Event Callback for Navigation
Ensure events fire before page navigation:
link.addEventListener('click', function(e) {
e.preventDefault();
const destination = this.href;
gtag('event', 'click', {
'event_category': 'outbound',
'event_label': destination,
'event_callback': function() {
window.location = destination;
}
});
// Fallback timeout
setTimeout(function() {
window.location = destination;
}, 1000);
});
3. Respect User Privacy
Don't track sensitive information:
// BAD - Don't track PII
gtag('event', 'form_submit', {
'user_email': email, // Don't do this!
'user_name': name // Don't do this!
});
// GOOD - Track form usage without PII
gtag('event', 'form_submit', {
'form_name': 'contact',
'form_location': 'footer'
});
4. Test Events Locally
Use console logging during development:
function trackEvent(eventName, eventParams) {
if (process.env.NODE_ENV === 'development') {
console.log('GA4 Event:', eventName, eventParams);
} else {
gtag('event', eventName, eventParams);
}
}
Testing Event Tracking
1. DebugView in GA4
Enable debug mode:
gtag('config', 'G-XXXXXXXXXX', {
'debug_mode': true
});
View events in real-time:
- GA4 → Configure → DebugView
- Events appear within seconds
- See event parameters and values
2. Browser Console
Check if events are firing:
// Override gtag to log events
const originalGtag = window.gtag;
window.gtag = function() {
console.log('GA4 Event:', arguments);
originalGtag.apply(window, arguments);
};
3. Network Tab
Monitor GA4 requests:
- Open DevTools → Network
- Filter by "collect" or "google-analytics"
- Click tracked element
- Verify request sent to GA4
Next Steps
- Set Up Data Layer - Advanced event configuration
- Debug Tracking Issues - Fix common problems
- GTM Implementation - Manage events via Tag Manager
Related Resources
- GA4 Event Reference - Standard GA4 events
- Performance Impact - Optimize tracking overhead
- Privacy Compliance - GDPR and cookie consent