Track user interactions on your Grav site using GA4 events. This guide covers tracking forms, downloads, clicks, and custom events using Twig templates and JavaScript.
Grav-Specific Event Tracking
Form Submission Tracking
Grav forms are defined in page frontmatter. Track submissions using JavaScript.
Basic Form Tracking
{# templates/forms/default.html.twig #}
<form name="{{ form.name }}"
action="{{ form.action ?: page.route }}"
method="{{ form.method|upper ?: 'POST' }}"
{{ form.fields|join|raw }}
<button type="submit">{{ form.buttons.submit.value ?: 'Submit' }}</button>
</form>
<script>
function trackFormSubmit(form) {
if (typeof gtag !== 'undefined') {
gtag('event', 'form_submit', {
'event_category': 'Form',
'event_label': form.name,
'form_name': form.name,
'page_location': window.location.href
});
}
}
</script>
Track Contact Form
# pages/contact/form.md
---
title: Contact Form
form:
name: contact-form
fields:
- name: name
label: Name
type: text
validate:
required: true
- name: email
label: Email
type: email
validate:
required: true
- name: message
label: Message
type: textarea
validate:
required: true
buttons:
submit:
type: submit
value: Send Message
process:
- email:
from: "{{ config.plugins.email.from }}"
to: "{{ config.plugins.email.to }}"
subject: "Contact Form Submission"
body: "{% include 'forms/data.html.twig' %}"
- save:
fileprefix: contact-
dateformat: Ymd-His-u
extension: txt
body: "{% include 'forms/data.txt.twig' %}"
- display: thankyou
---
{# templates/forms/fields/submit/submit.html.twig - Custom submit button #}
<button type="submit"
class="{{ form_button_classes ?: 'button' }}" form.name }}')">
{{ field.value }}
</button>
<script>
function trackFormStart(formName) {
if (typeof gtag !== 'undefined') {
gtag('event', 'form_start', {
'event_category': 'Form',
'event_label': formName,
'form_name': formName
});
}
}
// Track successful submission
window.addEventListener('DOMContentLoaded', function() {
if (window.location.search.includes('thank-you') ||
document.querySelector('.form-success')) {
gtag('event', 'form_success', {
'event_category': 'Form',
'event_label': '{{ form.name }}',
'form_name': '{{ form.name }}'
});
}
});
</script>
Download Tracking
Track file downloads from Grav media library.
Track PDF Downloads
{# Track all PDF links automatically #}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Track all download links
var downloadLinks = document.querySelectorAll('a[href$=".pdf"], a[href$=".zip"], a[href$=".doc"], a[href$=".docx"]');
downloadLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
var url = this.href;
var fileName = url.substring(url.lastIndexOf('/') + 1);
var fileType = fileName.substring(fileName.lastIndexOf('.') + 1);
if (typeof gtag !== 'undefined') {
gtag('event', 'file_download', {
'event_category': 'Download',
'event_label': fileName,
'file_name': fileName,
'file_type': fileType,
'link_url': url,
'link_text': this.textContent.trim()
});
}
});
});
});
</script>
Track Specific Downloads
{# In your template or page content #}
<a href="{{ page.media['whitepaper.pdf'].url }}" 'whitepaper', 'pdf')">
Download Whitepaper (PDF)
</a>
<script>
function trackDownload(element, fileName, fileType) {
if (typeof gtag !== 'undefined') {
gtag('event', 'file_download', {
'event_category': 'Download',
'event_label': fileName,
'file_name': fileName + '.' + fileType,
'file_type': fileType,
'link_text': element.textContent.trim(),
'page_title': '{{ page.title }}'
});
}
}
</script>
Blog Post Engagement
Track reading time and scroll depth on blog posts.
{# templates/blog.html.twig or item.html.twig #}
{% extends 'partials/base.html.twig' %}
{% block content %}
<article id="blog-post" data-post-id="{{ page.slug }}">
<h1>{{ page.title }}</h1>
{% if page.header.author %}
<p class="author">By {{ page.header.author }}</p>
{% endif %}
{{ page.content|raw }}
</article>
<script>
// Track blog post view
if (typeof gtag !== 'undefined') {
gtag('event', 'blog_post_view', {
'event_category': 'Blog',
'event_label': '{{ page.title }}',
'post_title': '{{ page.title }}',
'post_category': '{{ page.taxonomy.category|first ?: "Uncategorized" }}',
'post_author': '{{ page.header.author ?: "Unknown" }}',
'post_date': '{{ page.date|date("Y-m-d") }}',
'post_tags': '{{ page.taxonomy.tag|join(",") }}'
});
}
// Track scroll depth
var scrollDepths = [25, 50, 75, 90];
var scrolledTo = [];
window.addEventListener('scroll', function() {
var scrollPercentage = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
scrollDepths.forEach(function(depth) {
if (scrollPercentage >= depth && scrolledTo.indexOf(depth) === -1) {
scrolledTo.push(depth);
if (typeof gtag !== 'undefined') {
gtag('event', 'scroll', {
'event_category': 'Blog',
'event_label': '{{ page.title }}',
'percent_scrolled': depth,
'post_title': '{{ page.title }}'
});
}
}
});
});
// Track time on page
var startTime = Date.now();
window.addEventListener('beforeunload', function() {
var timeOnPage = Math.round((Date.now() - startTime) / 1000);
if (typeof gtag !== 'undefined' && timeOnPage > 5) {
gtag('event', 'content_engagement', {
'event_category': 'Blog',
'event_label': '{{ page.title }}',
'time_on_page': timeOnPage,
'engagement_level': timeOnPage > 60 ? 'high' : (timeOnPage > 30 ? 'medium' : 'low')
});
}
});
</script>
{% endblock %}
Search Tracking
Track SimpleSearch plugin usage.
{# templates/simplesearch_results.html.twig #}
{% if query %}
<script>
if (typeof gtag !== 'undefined') {
gtag('event', 'search', {
'search_term': '{{ query }}',
'results_count': {{ results|length }},
'event_category': 'Search'
});
}
</script>
{% endif %}
Navigation Tracking
Track menu clicks and internal navigation.
{# templates/partials/navigation.html.twig #}
<nav id="main-nav">
{% for item in pages.children.visible %}
<a href="{{ item.url }}" '{{ item.menu }}')">
{{ item.menu }}
</a>
{% endfor %}
</nav>
<script>
function trackNavClick(element, menuLabel) {
if (typeof gtag !== 'undefined') {
gtag('event', 'nav_click', {
'event_category': 'Navigation',
'event_label': menuLabel,
'link_text': element.textContent.trim(),
'link_url': element.href,
'nav_position': 'main_menu'
});
}
}
</script>
Outbound Link Tracking
Track clicks to external sites.
{# Add to base.html.twig #}
<script>
document.addEventListener('DOMContentLoaded', function() {
var internalDomain = '{{ grav.uri.host() }}';
// Track all external links
var links = document.querySelectorAll('a[href^="http"]');
links.forEach(function(link) {
var url = new URL(link.href);
if (url.hostname !== internalDomain) {
link.addEventListener('click', function(e) {
if (typeof gtag !== 'undefined') {
gtag('event', 'click', {
'event_category': 'Outbound Link',
'event_label': url.hostname,
'link_url': link.href,
'link_text': link.textContent.trim(),
'link_domain': url.hostname
});
}
});
}
});
});
</script>
Video Tracking
Track embedded videos (YouTube, Vimeo).
{# For YouTube videos in markdown/content #}
<script>
// Track YouTube video interactions
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
var players = [];
function onYouTubeIframeAPIReady() {
var videos = document.querySelectorAll('iframe[src*="youtube.com"]');
videos.forEach(function(iframe, index) {
var videoId = iframe.src.match(/embed\/([^?]+)/)[1];
iframe.id = 'youtube-player-' + index;
players[index] = new YT.Player(iframe.id, {
events: {
'onStateChange': function(event) {
trackYouTubeEvent(event, videoId);
}
}
});
});
}
function trackYouTubeEvent(event, videoId) {
var eventName = '';
switch(event.data) {
case YT.PlayerState.PLAYING:
eventName = 'video_start';
break;
case YT.PlayerState.PAUSED:
eventName = 'video_pause';
break;
case YT.PlayerState.ENDED:
eventName = 'video_complete';
break;
}
if (eventName && typeof gtag !== 'undefined') {
gtag('event', eventName, {
'event_category': 'Video',
'event_label': videoId,
'video_provider': 'YouTube',
'video_id': videoId,
'video_url': event.target.getVideoUrl()
});
}
}
</script>
Plugin-Based Event Tracking
Track events from popular Grav plugins.
SimpleSearch Events
{# After search form #}
<script>
// Track search form usage
document.querySelector('.simplesearch-form').addEventListener('submit', function(e) {
var searchTerm = this.querySelector('input[name="searchfield"]').value;
if (typeof gtag !== 'undefined' && searchTerm) {
gtag('event', 'search', {
'event_category': 'Search',
'search_term': searchTerm,
'search_type': 'site_search'
});
}
});
</script>
Comments Plugin Events
{# Track comment submissions #}
<script>
// Track comment form submission
var commentForm = document.querySelector('.comment-form form');
if (commentForm) {
commentForm.addEventListener('submit', function(e) {
if (typeof gtag !== 'undefined') {
gtag('event', 'comment_submit', {
'event_category': 'Engagement',
'event_label': '{{ page.title }}',
'page_title': '{{ page.title }}',
'page_url': '{{ page.url(true) }}'
});
}
});
}
</script>
Modular Page Tracking
Track interactions on modular pages.
{# templates/modular.html.twig #}
{% extends 'partials/base.html.twig' %}
{% block content %}
{% for module in page.collection() %}
<div class="modular-section" data-module="{{ module.template() }}">
{{ module.content|raw }}
</div>
{% endfor %}
{% endblock %}
{% block bottom %}
<script>
// Track modular page view
if (typeof gtag !== 'undefined') {
var modules = [];
document.querySelectorAll('.modular-section').forEach(function(section) {
modules.push(section.dataset.module);
});
gtag('event', 'modular_page_view', {
'event_category': 'Page',
'event_label': '{{ page.title }}',
'modules': modules.join(','),
'module_count': modules.length
});
}
// Track scroll to each module
var observedModules = new Set();
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting && !observedModules.has(entry.target)) {
observedModules.add(entry.target);
var moduleName = entry.target.dataset.module;
if (typeof gtag !== 'undefined') {
gtag('event', 'module_view', {
'event_category': 'Modular',
'event_label': moduleName,
'module_name': moduleName,
'page_title': '{{ page.title }}'
});
}
}
});
}, { threshold: 0.5 });
document.querySelectorAll('.modular-section').forEach(function(section) {
observer.observe(section);
});
</script>
{% endblock %}
Custom Event Helper
Create a reusable Twig macro for tracking events.
{# templates/macros/analytics.html.twig #}
{% macro trackEvent(eventName, category, label, params) %}
<script>
(function() {
if (typeof gtag !== 'undefined') {
var eventParams = {
'event_category': '{{ category }}',
'event_label': '{{ label }}'
};
{% if params %}
var customParams = {{ params|json_encode|raw }};
Object.assign(eventParams, customParams);
{% endif %}
gtag('event', '{{ eventName }}', eventParams);
}
})();
</script>
{% endmacro %}
Usage:
{% import 'macros/analytics.html.twig' as analytics %}
{# Track custom event #}
{{ analytics.trackEvent('button_click', 'CTA', 'Sign Up', {
'button_position': 'header',
'page_type': page.template()
}) }}
E-commerce Tracking (ShoppingCart Plugin)
Track e-commerce events if using Grav e-commerce plugins.
{# Track add to cart #}
<button product.id }}', '{{ product.title }}', {{ product.price }})">
Add to Cart
</button>
<script>
function addToCart(productId, productName, productPrice) {
// Your add to cart logic here
// Track event
if (typeof gtag !== 'undefined') {
gtag('event', 'add_to_cart', {
'currency': 'USD',
'value': productPrice,
'items': [{
'item_id': productId,
'item_name': productName,
'price': productPrice,
'quantity': 1
}]
});
}
}
</script>
Testing Events
Debug Mode
<script>
// Enable GA4 debug mode
gtag('config', 'G-XXXXXXXXXX', {
'debug_mode': true
});
// Log events to console in development
{% if grav.config.system.debugger.enabled %}
var originalGtag = gtag;
gtag = function() {
console.log('GA4 Event:', Array.from(arguments));
originalGtag.apply(this, arguments);
};
{% endif %}
</script>
Test Events in Console
// Test form event
gtag('event', 'form_submit', {
'event_category': 'Form',
'event_label': 'test-form'
});
// Test download event
gtag('event', 'file_download', {
'event_category': 'Download',
'file_name': 'test.pdf'
});
Best Practices
1. Use Consistent Event Naming
// Good - consistent naming
gtag('event', 'form_submit', { ... });
gtag('event', 'button_click', { ... });
gtag('event', 'file_download', { ... });
// Bad - inconsistent
gtag('event', 'FormSubmit', { ... });
gtag('event', 'btnClick', { ... });
gtag('event', 'downloadFile', { ... });
2. Include Page Context
gtag('event', 'custom_event', {
'event_category': 'Category',
'event_label': 'Label',
// Context
'page_title': '{{ page.title }}',
'page_type': '{{ page.template() }}',
'page_url': '{{ page.url(true) }}'
});
3. Avoid PII
// Bad - includes email
gtag('event', 'form_submit', {
'user_email': 'user@example.com' // Don't do this
});
// Good - anonymous
gtag('event', 'form_submit', {
'form_name': 'contact',
'user_type': 'visitor'
});
Next Steps
- Install GTM - For easier event management
- Set up Data Layer - Structure your tracking data
- Troubleshoot Events - Debug tracking issues
For general GA4 event concepts, see GA4 Event Tracking Guide.