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
- Navigate to SEOmatic → Tracking Scripts
- Click Google Tag Manager
- Enter your GTM Container ID in the Google Tag Manager ID field
- Configure environment settings:
- Environment: Set to
productiononly - Data Layer: Enable for dynamic data
- Environment: Set to
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' %}>m_preview={{ getenv('GTM_PREVIEW_KEY') }}>m_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",
]),
],
],
];
Cookie Consent Integration
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
- Browser Console: Check for
dataLayerarray - Network Tab: Verify requests to
googletagmanager.com - GTM Preview Mode: Use GTM's built-in preview and debug tool
- 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:
- AdBlockers: Disable ad blockers during testing
- CSP Headers: Verify Content Security Policy allows GTM
- Container Not Published: Publish your GTM container
- 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
- GTM Data Layer - Implement comprehensive data layer with Craft data
- GA4 Setup - Configure GA4 tags in GTM
- Event Tracking - Track custom events via GTM