Install Google Tag Manager on Grav Sites | OpsBlu Docs

Install Google Tag Manager on Grav Sites

Complete guide to installing GTM on Grav flat-file CMS using Twig templates, plugins, and custom theme integration.

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

  1. Create GTM Container

    • Go to Google Tag Manager
    • Create a new container
    • Select "Web" as target platform
    • Note your Container ID (format: GTM-XXXXXXX)
  2. Admin Access Required

    • Access to Grav Admin Panel or file system
    • Ability to edit theme templates

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

  1. Open GTM Container

  2. Enter Your Site URL

    • Enter your Grav site URL
    • Click Connect
  3. 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:

  1. Verify Container ID: GTM-XXXXXXX format
  2. Test in incognito mode
  3. Check browser console for errors
  4. 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

For general GTM concepts, see Google Tag Manager Guide.