Google Tag Manager Setup on Craft CMS | OpsBlu Docs

Google Tag Manager Setup on Craft CMS

Complete guide to installing and configuring Google Tag Manager (GTM) on Craft CMS with environment-aware loading and best practices.

Learn how to implement Google Tag Manager on your Craft CMS website using Twig templates, environment configuration, and the SEOmatic plugin.

Prerequisites

  • Active Google Tag Manager account
  • GTM Container ID (format: GTM-XXXXXXX)
  • Craft CMS 4.x or 5.x installation
  • Basic understanding of Twig templating

Method 1: Direct Twig Template Integration

Step 1: Configure Environment Variables

Add your GTM Container ID to .env:

# .env
GTM_CONTAINER_ID="GTM-XXXXXXX"
ENVIRONMENT="production"

Step 2: Create GTM Partial Template

Create a reusable partial at templates/_analytics/google-tag-manager.twig:

{# templates/_analytics/google-tag-manager.twig #}

{% set gtmId = getenv('GTM_CONTAINER_ID') %}
{% set environment = craft.app.config.general.environment %}

{# Only load GTM in production and not during Live Preview #}
{% if gtmId and environment == 'production' and not craft.app.request.isLivePreview %}

<!-- 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','{{ gtmId }}');</script>
<!-- End Google Tag Manager -->

{% elseif craft.app.config.general.devMode %}
<!-- Google Tag Manager disabled in dev mode -->
{% endif %}

Create the noscript fallback at templates/_analytics/google-tag-manager-noscript.twig:

{# templates/_analytics/google-tag-manager-noscript.twig #}

{% set gtmId = getenv('GTM_CONTAINER_ID') %}
{% set environment = craft.app.config.general.environment %}

{% if gtmId and environment == 'production' and not craft.app.request.isLivePreview %}
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={{ gtmId }}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
{% endif %}

Step 3: Include in Base Layout

Add GTM to your main layout template:

{# templates/_layouts/base.twig #}
<!DOCTYPE html>
<html lang="{{ currentSite.language }}">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title ?? siteName }}</title>

    {# GTM - Load as early as possible in <head> #}
    {{ include('_analytics/google-tag-manager') }}

    {# Other head content #}
    {{ head() }}
</head>
<body>
    {# GTM noscript fallback - Immediately after opening <body> tag #}
    {{ include('_analytics/google-tag-manager-noscript') }}

    {{ beginBody() }}

    {# Main content #}
    {% block content %}{% endblock %}

    {{ endBody() }}
</body>
</html>

Method 2: Using SEOmatic Plugin

Step 1: Install SEOmatic

cd /path/to/craft-project
composer require nystudio107/craft-seomatic

Step 2: Configure in Control Panel

  1. Navigate to SEOmatic → Tracking Scripts
  2. Click Google Tag Manager
  3. Enter your GTM Container ID in the Google Tag Manager ID field
  4. Configure environment settings:
    • Environment: Set to production only
    • Data Layer: Enable for dynamic data

Step 3: Environment Configuration

Create or update config/seomatic.php:

<?php

use craft\helpers\App;

return [
    '*' => [
        'pluginName' => 'SEOmatic',
        'renderEnabled' => true,
        'environment' => App::env('ENVIRONMENT') ?: 'production',
    ],
    'production' => [
        'renderEnabled' => true,
    ],
    'staging' => [
        'renderEnabled' => false, // Disable GTM in staging
    ],
    'dev' => [
        'renderEnabled' => false, // Disable GTM in development
    ],
];

Step 4: Configure GTM Settings

// config/seomatic.php - Extended configuration
return [
    '*' => [
        'googleTagManager' => [
            'containerId' => App::env('GTM_CONTAINER_ID'),
            'dataLayer' => 'dataLayer',
            'dataLayerVariableName' => 'dataLayer',
        ],
    ],
];

Method 3: Custom Module Implementation

For advanced control and server-side integration:

Step 1: Create GTM Module

mkdir -p modules/gtm

Create modules/gtm/Module.php:

<?php

namespace modules\gtm;

use Craft;
use craft\events\TemplateEvent;
use craft\web\View;
use yii\base\Event;
use yii\base\Module as BaseModule;

class Module extends BaseModule
{
    public static $instance;

    public function init()
    {
        parent::init();
        self::$instance = $this;

        // Inject GTM head script
        Event::on(
            View::class,
            View::EVENT_BEGIN_HEAD,
            [$this, 'injectGtmHead']
        );

        // Inject GTM body script
        Event::on(
            View::class,
            View::EVENT_BEGIN_BODY,
            [$this, 'injectGtmBody']
        );
    }

    public function injectGtmHead(TemplateEvent $event)
    {
        if (!$this->shouldLoadGtm()) {
            return;
        }

        $containerId = getenv('GTM_CONTAINER_ID');
        $event->output = $this->getGtmHeadScript($containerId) . $event->output;
    }

    public function injectGtmBody(TemplateEvent $event)
    {
        if (!$this->shouldLoadGtm()) {
            return;
        }

        $containerId = getenv('GTM_CONTAINER_ID');
        $event->output = $this->getGtmBodyScript($containerId) . $event->output;
    }

    private function shouldLoadGtm(): bool
    {
        // Don't load in dev or during Live Preview
        if (Craft::$app->config->general->environment !== 'production') {
            return false;
        }

        if (Craft::$app->request->isLivePreview) {
            return false;
        }

        if (!getenv('GTM_CONTAINER_ID')) {
            return false;
        }

        return true;
    }

    private function getGtmHeadScript(string $containerId): string
    {
        return <<<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','{$containerId}');</script>
<!-- End Google Tag Manager -->
HTML;
    }

    private function getGtmBodyScript(string $containerId): string
    {
        return <<<HTML
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id={$containerId}"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
HTML;
    }
}

Step 2: Bootstrap the Module

Edit config/app.php:

<?php

use craft\helpers\App;

return [
    'modules' => [
        'gtm' => [
            'class' => \modules\gtm\Module::class,
        ],
    ],
    'bootstrap' => ['gtm'],
];

Multi-Site GTM Configuration

For Craft multi-site installations with different containers:

{# templates/_analytics/google-tag-manager.twig #}

{% set gtmContainers = {
    'siteHandleOne': getenv('GTM_SITE_ONE'),
    'siteHandleTwo': getenv('GTM_SITE_TWO'),
    'siteHandleThree': getenv('GTM_SITE_THREE'),
} %}

{% set gtmId = gtmContainers[currentSite.handle] ?? null %}

{% if gtmId and craft.app.config.general.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','{{ gtmId }}');</script>
<!-- End Google Tag Manager -->
{% endif %}

Environment variables:

# .env
GTM_SITE_ONE="GTM-AAAAAAA"
GTM_SITE_TWO="GTM-BBBBBBB"
GTM_SITE_THREE="GTM-CCCCCCC"

Environment-Specific Containers

Use different GTM containers for staging and production:

{# templates/_analytics/google-tag-manager.twig #}

{% set environment = craft.app.config.general.environment %}

{% set gtmId = null %}
{% if environment == 'production' %}
    {% set gtmId = getenv('GTM_PRODUCTION') %}
{% elseif environment == 'staging' %}
    {% set gtmId = getenv('GTM_STAGING') %}
{% endif %}

{% if gtmId %}
<!-- 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{% if environment == 'staging' %}&gtm_preview={{ getenv('GTM_PREVIEW_KEY') }}&gtm_auth={{ getenv('GTM_AUTH_KEY') }}{% endif %};f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','{{ gtmId }}');</script>
<!-- End Google Tag Manager -->
{% endif %}

GTM Preview Mode in Development

Enable GTM Preview Mode for testing in staging:

# .env
GTM_STAGING="GTM-XXXXXXX"
GTM_PREVIEW_KEY="preview_key_from_gtm"
GTM_AUTH_KEY="auth_key_from_gtm"

Content Security Policy (CSP)

Configure CSP headers to allow GTM scripts:

// config/general.php
return [
    '*' => [
        'securityHeaders' => [
            'Content-Security-Policy' => implode('; ', [
                "default-src 'self'",
                "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com",
                "img-src 'self' data: https://www.googletagmanager.com https://www.google-analytics.com",
                "connect-src 'self' https://www.google-analytics.com https://www.googletagmanager.com",
                "frame-src https://www.googletagmanager.com",
            ]),
        ],
    ],
];

Implement GDPR-compliant loading based on consent:

{# templates/_analytics/google-tag-manager.twig #}

{% set cookieConsent = craft.cookies.get('cookie_consent') %}

{% if cookieConsent == 'all' or cookieConsent == 'analytics' %}
    {# Load GTM only with consent #}
    {% set gtmId = getenv('GTM_CONTAINER_ID') %}

    {% if gtmId and craft.app.config.general.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','{{ gtmId }}');</script>
    <!-- End Google Tag Manager -->
    {% endif %}
{% else %}
    {# Show consent banner #}
    {{ include('_components/cookie-consent-banner') }}
{% endif %}

Performance Optimization

DNS Prefetch and Preconnect

{# In <head> section #}
<link rel="dns-prefetch" href="//www.googletagmanager.com">
<link rel="preconnect" href="https://www.googletagmanager.com" crossorigin>

Lazy Loading GTM

For non-critical pages, delay GTM loading:

<script>
  // Load GTM after page load
  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','{{ gtmId }}');
  });
</script>

Testing and Validation

Verify GTM Installation

  1. Browser Console: Check for dataLayer array
  2. Network Tab: Verify requests to googletagmanager.com
  3. GTM Preview Mode: Use GTM's built-in preview and debug tool
  4. Tag Assistant: Install Google Tag Assistant Chrome extension

Debug in Development

Add debug output in development mode:

{% if craft.app.config.general.devMode %}
<!-- GTM Debug Info -->
{% set gtmId = getenv('GTM_CONTAINER_ID') %}
<!-- GTM Container ID: {{ gtmId ? gtmId : 'NOT SET' }} -->
<!-- Environment: {{ craft.app.config.general.environment }} -->
<!-- Live Preview: {{ craft.app.request.isLivePreview ? 'Yes' : 'No' }} -->
{% endif %}

Console Logging

Monitor dataLayer pushes in development:

{% if craft.app.config.general.devMode %}
<script>
  // Log all dataLayer pushes
  window.dataLayer = window.dataLayer || [];
  var originalPush = window.dataLayer.push;
  window.dataLayer.push = function() {
    console.log('GTM dataLayer Push:', arguments);
    return originalPush.apply(window.dataLayer, arguments);
  };
</script>
{% endif %}

Common Issues and Solutions

GTM Not Loading

Check these common issues:

{# Debug template #}
{% set gtmId = getenv('GTM_CONTAINER_ID') %}
{% set environment = craft.app.config.general.environment %}

<!-- Debug Output (remove in production) -->
<!-- GTM ID: {{ gtmId ? 'Set' : 'NOT SET' }} -->
<!-- Environment: {{ environment }} -->
<!-- Should Load GTM: {{ gtmId and environment == 'production' ? 'Yes' : 'No' }} -->

Tags Not Firing

Common causes:

  1. AdBlockers: Disable ad blockers during testing
  2. CSP Headers: Verify Content Security Policy allows GTM
  3. Container Not Published: Publish your GTM container
  4. Wrong Environment: Ensure production environment is set

User Role Exclusion

Exclude admin users from tracking:

{% set currentUser = currentUser ?? null %}
{% set shouldLoadGtm = not currentUser or not currentUser.isInGroup('admins') %}

{% if shouldLoadGtm %}
    {# Load GTM #}
{% endif %}

Next Steps

Resources