Google Tag Manager (GTM) simplifies managing tracking codes on your Grav site. This guide covers installation via Twig templates and custom plugins.
Before You Begin
Create GTM Container
- Go to Google Tag Manager
- Create a new container
- Select "Web" as target platform
- Note your Container ID (format:
GTM-XXXXXXX)
Admin Access Required
- Access to Grav Admin Panel or file system
- Ability to edit theme templates
Method 1: Twig Template Integration (Recommended)
Add GTM directly to your theme templates.
Add to Base Template
{# user/themes/your-theme/templates/partials/base.html.twig #}
<!DOCTYPE html>
<html lang="{{ grav.language.getActive ?: 'en' }}">
<head>
{% block head %}
<meta charset="utf-8" />
<title>{% if page.title %}{{ page.title }} | {% endif %}{{ site.title }}</title>
{# Google Tag Manager #}
{% set gtm_id = grav.config.site.gtm_id %}
{% if gtm_id %}
<!-- Google Tag Manager -->
<script>(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_id }}');</script>
<!-- End Google Tag Manager -->
{% endif %}
{{ assets.css()|raw }}
{% endblock %}
</head>
<body>
{# Google Tag Manager (noscript) #}
{% if gtm_id %}
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{ gtm_id }}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
{% endif %}
{% block content %}{% endblock %}
{{ assets.js()|raw }}
</body>
</html>
Configure in site.yaml
# user/config/site.yaml
title: My Grav Site
default_lang: en
gtm_id: 'GTM-XXXXXXX'
Separate Header and Body Partials
If your theme uses separate partials:
{# user/themes/your-theme/templates/partials/head.html.twig #}
<meta charset="utf-8" />
<title>{% if page.title %}{{ page.title|e('html') }} | {% endif %}{{ site.title|e('html') }}</title>
{% set gtm_id = grav.config.site.gtm_id %}
{% if gtm_id and grav.config.system.environment == 'production' %}
<!-- Google Tag Manager -->
<script>(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_id }}');</script>
<!-- End Google Tag Manager -->
{% endif %}
{# user/themes/your-theme/templates/partials/body-top.html.twig #}
{% set gtm_id = grav.config.site.gtm_id %}
{% if gtm_id and grav.config.system.environment == 'production' %}
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{ gtm_id }}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
{% endif %}
Method 2: Custom Plugin
Create a minimal GTM plugin for easier management.
Create Plugin Structure
# Create plugin directory
mkdir -p user/plugins/gtm
# Create files
touch user/plugins/gtm/gtm.php
touch user/plugins/gtm/gtm.yaml
Plugin Code
<?php
// user/plugins/gtm/gtm.php
namespace Grav\Plugin;
use Grav\Common\Plugin;
class GtmPlugin extends Plugin
{
public static function getSubscribedEvents()
{
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0]
];
}
public function onPluginsInitialized()
{
if ($this->isAdmin()) {
return;
}
$this->enable([
'onPageContentRaw' => ['onPageContentRaw', 0],
'onOutputGenerated' => ['onOutputGenerated', 0]
]);
}
public function onPageContentRaw()
{
$config = $this->config->get('plugins.gtm');
if (!$config['enabled'] || empty($config['container_id'])) {
return;
}
$container_id = $config['container_id'];
// Add GTM head script
$head_script = <<<HTML
<!-- Google Tag Manager -->
<script>(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','{$container_id}');</script>
<!-- End Google Tag Manager -->
HTML;
$this->grav['assets']->addInlineJs($head_script, ['position' => 'head']);
}
public function onOutputGenerated()
{
$config = $this->config->get('plugins.gtm');
if (!$config['enabled'] || empty($config['container_id'])) {
return;
}
$container_id = $config['container_id'];
// Add GTM noscript after <body> tag
$noscript = <<<HTML
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={$container_id}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
HTML;
$output = $this->grav->output;
$output = preg_replace('/(<body[^>]*>)/i', '$1' . "\n" . $noscript, $output, 1);
$this->grav->output = $output;
}
}
Plugin Configuration
# user/plugins/gtm/gtm.yaml
enabled: true
container_id: 'GTM-XXXXXXX'
Method 3: Environment-Specific Containers
Use different GTM containers for development and production.
{# Use different containers per environment #}
{% set gtm_containers = {
'production': 'GTM-PROD123',
'staging': 'GTM-STAGE456',
'development': 'GTM-DEV789'
} %}
{% set environment = grav.config.system.environment ?: 'production' %}
{% set gtm_id = gtm_containers[environment] %}
{% if gtm_id %}
<!-- Google Tag Manager -->
<script>(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_id }}');</script>
<!-- End Google Tag Manager -->
{% endif %}
Data Layer Initialization
Initialize data layer with Grav page data.
Basic Data Layer
{# Initialize data layer before GTM #}
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'pageType': '{{ page.template() }}',
'pageTitle': '{{ page.title|e("js") }}',
'pageCategory': '{{ page.taxonomy.category|first ?: "uncategorized" }}',
'pageTags': '{{ page.taxonomy.tag|join(",") }}',
'pageAuthor': '{{ page.header.author ?: "unknown" }}',
'pageLanguage': '{{ grav.language.getActive ?: grav.config.site.default_lang }}',
'siteEnvironment': '{{ grav.config.system.environment ?: "production" }}'
});
</script>
<!-- GTM code follows -->
Advanced Data Layer with User Info
<script>
window.dataLayer = window.dataLayer || [];
// Page data
var pageData = {
'event': 'page_load',
'page': {
'type': '{{ page.template() }}',
'title': '{{ page.title|e("js") }}',
'path': '{{ page.route() }}',
'language': '{{ grav.language.getActive ?: "en" }}',
'category': '{{ page.taxonomy.category|first ?: "uncategorized" }}',
'tags': '{{ page.taxonomy.tag|join(",") }}'
}
};
// Add user data if logged in
{% if grav.user.authenticated %}
pageData.user = {
'id': '{{ grav.user.username|md5 }}', // Hashed for privacy
'status': 'logged_in',
'groups': '{{ grav.user.groups|join(",") }}'
};
{% else %}
pageData.user = {
'status': 'guest'
};
{% endif %}
window.dataLayer.push(pageData);
</script>
Page-Specific Container IDs
Use different containers for specific pages or sections.
{# In page frontmatter (pages/special-page.md) #}
---
title: Special Landing Page
gtm_container: 'GTM-SPECIAL'
---
{# In template #}
{% set gtm_id = page.header.gtm_container ?: grav.config.site.gtm_id %}
{% if gtm_id %}
<!-- Google Tag Manager -->
<script>(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_id }}');</script>
<!-- End Google Tag Manager -->
{% endif %}
Server-Side GTM (Advanced)
For server-side tagging (requires GTM Server container).
{% set gtm_server_url = grav.config.site.gtm_server_url %}
<script>(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=
'{{ gtm_server_url ?: "https://www.googletagmanager.com" }}/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','{{ gtm_id }}');</script>
Multi-Language Sites
Track language and locale in data layer.
{% set active_lang = grav.language.getActive ?: grav.config.site.default_lang %}
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'pageLanguage': '{{ active_lang }}',
'siteLanguages': '{{ grav.language.getLanguages|join(",") }}',
'defaultLanguage': '{{ grav.config.site.default_lang }}'
});
</script>
GDPR Compliance
Conditional GTM loading based on consent.
<script>
// Check consent
function hasGTMConsent() {
return localStorage.getItem('gtm_consent') === 'true';
}
// Load GTM only if consent given
if (hasGTMConsent()) {
(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_id }}');
} else {
// Initialize data layer but don't load GTM
window.dataLayer = window.dataLayer || [];
}
</script>
{# Consent banner #}
<div id="consent-banner" style="display:none;">
<p>We use cookies for analytics. Accept to continue.</p>
<button
</div>
<script>
function grantConsent() {
localStorage.setItem('gtm_consent', 'true');
location.reload();
}
if (!localStorage.getItem('gtm_consent')) {
document.getElementById('consent-banner').style.display = 'block';
}
</script>
Testing & Verification
Use GTM Preview Mode
Open GTM Container
- Go to Google Tag Manager
- Select your container
- Click Preview button
Enter Your Site URL
- Enter your Grav site URL
- Click Connect
Verify Installation
- Tag Assistant should connect
- Verify container ID matches
- Check tags firing correctly
Browser Console Testing
// Check if data layer exists
console.log(window.dataLayer);
// Check if GTM loaded
console.log(google_tag_manager);
// Push test event
dataLayer.push({
'event': 'test_event',
'test_param': 'test_value'
});
View Page Source
# Check if GTM code present
curl https://yoursite.com | grep GTM-
Common Issues
GTM Not Loading
Possible Causes:
- Incorrect Container ID
- GTM script blocked by ad blocker
- JavaScript errors preventing load
- Cache not cleared
Solutions:
- Verify Container ID:
GTM-XXXXXXXformat - Test in incognito mode
- Check browser console for errors
- Clear Grav cache:
bin/grav clear-cache
Data Layer Not Populating
Cause: Data layer initialized after GTM loads.
Solution: Always initialize data layer BEFORE GTM:
<!-- 1. Data Layer -->
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ /* data */ });
</script>
<!-- 2. Then GTM -->
<script>(function(w,d,s,l,i){ /* GTM code */ })</script>
Noscript Tag Not Showing
Cause: Missing noscript tag in body.
Solution: Ensure noscript immediately after opening <body> tag.
Cache Issues
Problem: Changes not appearing.
Solution:
# Clear all Grav caches
bin/grav clear-cache
# Or via Admin Panel
Admin → Configuration → Clear Cache
Performance Optimization
Async Loading
GTM loads async by default, but you can further optimize:
<script>
// Defer GTM loading until page interactive
window.addEventListener('load', function() {
(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_id }}');
});
</script>
Exclude from Admin
{% if not grav.uri.path starts with '/admin' %}
{# GTM code #}
{% endif %}
Next Steps
- Set up Data Layer - Configure custom data layer
- Configure GA4 in GTM - Add Google Analytics via GTM
- Install Meta Pixel via GTM - Add Facebook tracking
For general GTM concepts, see Google Tag Manager Guide.