Google Tag Manager Setup on Drupal
Overview
Google Tag Manager (GTM) provides a powerful way to manage tracking codes and marketing tags on your Drupal site without modifying code. This guide covers both module-based and manual GTM implementation, with considerations for Drupal's caching system and BigPipe.
Method 1: Using the GTM Module (Recommended)
Installation
Via Composer:
# Install GTM module
composer require drupal/google_tag
# Enable the module
drush en google_tag -y
# Clear cache
drush cr
Configuration
Navigate to Configuration → System → Google Tag Manager (/admin/config/system/google_tag)
Container Settings
Container ID:
GTM-XXXXXXX
Enter your GTM container ID (starts with 'GTM-')
Environment Settings:
- Production: Use main container ID
- Staging/Dev: Use separate container or environment parameters
Environment Snippet (Optional):
>m_auth=YOUR_AUTH>m_preview=env-X>m_cookies_win=x
Advanced Settings
Insert Snippet:
- ✅ Head section (Recommended - standard GTM placement)
- ☐ Body section (optional for noscript fallback)
Include/Exclude Pages:
Pages to Include:
# Include all pages (leave blank)
Pages to Exclude:
/admin*
/user/*/edit
/node/add*
/node/*/edit
/node/*/delete
/batch
/taxonomy/term/*/edit
Role-Based Exclusion:
☐ Track Administrator (recommended to exclude)
☑ Track Authenticated User
☑ Track Anonymous User
Advanced Options
☑ Include default data layer
☑ Include user properties
☑ Include entity data
Cache Settings:
☑ Enable tag caching (improves performance)
Cache lifetime: 3600 seconds (1 hour)
BigPipe Compatibility:
☑ Insert via placeholder (compatible with BigPipe)
Configuration via Code
File: config/sync/google_tag.settings.yml
container_id: 'GTM-XXXXXXX'
environment_id: ''
environment_token: ''
path_toggle: 'exclude listed'
path_list: |
/admin*
/user/*/edit
/node/add*
/node/*/edit
/batch
role_toggle: 'exclude listed'
role_list:
administrator: administrator
status_toggle: 'include listed'
status_list:
- 404
- 403
include_classes: true
whitelist_classes: ''
blacklist_classes: ''
include_environment: false
environment_type: 'custom'
data_layer: 'dataLayer'
include_file: true
rebuild_snippets: true
debug_output: false
Import configuration:
drush config:import -y
drush cr
Method 2: Manual Theme Implementation
For complete control over GTM placement and data layer.
Step 1: Add to HTML Head
File: themes/custom/mytheme/templates/html.html.twig
{#
/**
* @file
* Theme override for the basic structure of a single Drupal page.
*/
#}
<!DOCTYPE html>
<html{{ html_attributes }}>
<head>
<head-placeholder token="{{ placeholder_token }}">
<title>{{ head_title|safe_join(' | ') }}</title>
<css-placeholder token="{{ placeholder_token }}">
<js-placeholder token="{{ placeholder_token }}">
{# Google Tag Manager - Head #}
{% if not is_admin %}
<!-- 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-XXXXXXX');</script>
<!-- End Google Tag Manager -->
{% endif %}
</head>
<body{{ attributes }}>
{# Google Tag Manager - Body (noscript) #}
{% if not is_admin %}
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
{% endif %}
<a href="#main-content" class="visually-hidden focusable skip-link">
{{ 'Skip to main content'|t }}
</a>
{{ page_top }}
{{ page }}
{{ page_bottom }}
<js-bottom-placeholder token="{{ placeholder_token }}">
</body>
</html>
Step 2: Add is_admin Variable
File: themes/custom/mytheme/mytheme.theme
<?php
/**
* Implements hook_preprocess_html().
*/
function mytheme_preprocess_html(&$variables) {
// Check if current route is admin
$is_admin = \Drupal::service('router.admin_context')->isAdminRoute();
// Make available in Twig
$variables['is_admin'] = $is_admin;
// Add GTM container ID to drupalSettings
if (!$is_admin) {
$variables['#attached']['drupalSettings']['gtm'] = [
'containerId' => 'GTM-XXXXXXX',
];
}
}
Step 3: Environment-Specific Configuration
File: settings.php
<?php
// GTM Container IDs per environment
$config['google_tag.settings']['container_id'] = 'GTM-XXXXXXX'; // Default
// Override for specific environments
if (getenv('PANTHEON_ENVIRONMENT') === 'live') {
$config['google_tag.settings']['container_id'] = 'GTM-PROD123';
}
elseif (getenv('PANTHEON_ENVIRONMENT') === 'test') {
$config['google_tag.settings']['container_id'] = 'GTM-STAGE456';
}
elseif (getenv('PANTHEON_ENVIRONMENT') === 'dev') {
$config['google_tag.settings']['container_id'] = 'GTM-DEV789';
}
// Or use environment authentication for single container
if (getenv('PANTHEON_ENVIRONMENT') !== 'live') {
$config['google_tag.settings']['environment_id'] = 'env-2';
$config['google_tag.settings']['environment_token'] = 'YOUR_AUTH_TOKEN';
}
BigPipe Compatibility
Drupal's BigPipe module requires special handling for GTM.
Option 1: Using Module's BigPipe Support
The Google Tag module automatically handles BigPipe via placeholders when enabled:
# google_tag.settings.yml
include_file: true
rebuild_snippets: true
Option 2: Manual Placeholder Implementation
<?php
/**
* Implements hook_page_attachments().
*/
function mytheme_page_attachments(array &$attachments) {
if (\Drupal::service('router.admin_context')->isAdminRoute()) {
return;
}
// Add GTM via lazy builder (BigPipe compatible)
$attachments['#attached']['html_head'][] = [
[
'#lazy_builder' => ['mytheme.gtm_builder:buildGtm', []],
'#create_placeholder' => TRUE,
],
'gtm_container'
];
}
Service definition:
File: mytheme.services.yml
services:
mytheme.gtm_builder:
class: Drupal\mytheme\GtmBuilder
arguments: ['@config.factory']
File: src/GtmBuilder.php
<?php
namespace Drupal\mytheme;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
/**
* Builds GTM container snippet.
*/
class GtmBuilder implements TrustedCallbackInterface {
/**
* Config factory.
*/
protected $configFactory;
/**
* Constructor.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['buildGtm'];
}
/**
* Build GTM snippet.
*/
public function buildGtm() {
$container_id = 'GTM-XXXXXXX';
$script = <<<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;
return [
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => $script,
'#cache' => [
'contexts' => ['url.path', 'user.roles'],
],
];
}
}
Multi-Site GTM Setup
For Drupal multi-site installations:
Separate Containers per Site
File: sites/site1/settings.php
<?php
$config['google_tag.settings']['container_id'] = 'GTM-SITE1XX';
File: sites/site2/settings.php
<?php
$config['google_tag.settings']['container_id'] = 'GTM-SITE2XX';
Shared Container with Site Identification
<?php
/**
* Implements hook_page_attachments().
*/
function mytheme_page_attachments(array &$attachments) {
// Get current site directory
$site_path = \Drupal::service('site.path');
$site_name = basename($site_path);
$attachments['#attached']['drupalSettings']['gtm']['siteName'] = $site_name;
$attachments['#attached']['drupalSettings']['gtm']['siteUrl'] = \Drupal::request()->getSchemeAndHttpHost();
}
In GTM, use these variables for filtering:
\{\{siteName\}\}- Custom JavaScript variable:drupalSettings.gtm.siteName\{\{siteUrl\}\}- Use built-in Page Hostname variable
Server-Side GTM (Advanced)
Setup Server-Side Container
- Create server container in GTM
- Deploy to Cloud Run, App Engine, or custom server
- Configure Drupal to send events to server endpoint
File: modules/custom/gtm_server/gtm_server.module
<?php
use Drupal\Core\Form\FormStateInterface;
/**
* Implements hook_page_attachments().
*/
function gtm_server_page_attachments(array &$attachments) {
$config = \Drupal::config('gtm_server.settings');
$server_url = $config->get('server_url');
if ($server_url) {
$attachments['#attached']['drupalSettings']['gtmServer'] = [
'serverUrl' => $server_url,
'containerId' => $config->get('container_id'),
];
$attachments['#attached']['library'][] = 'gtm_server/gtm_client';
}
}
JavaScript client:
(function (Drupal, drupalSettings) {
'use strict';
// Initialize server-side GTM
if (drupalSettings.gtmServer) {
(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=
drupalSettings.gtmServer.serverUrl + '/gtm.js?id='+i;
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer',drupalSettings.gtmServer.containerId);
}
})(Drupal, drupalSettings);
Privacy & GDPR Compliance
Cookie Consent Integration
<?php
/**
* Implements hook_page_attachments().
*/
function mytheme_page_attachments(array &$attachments) {
// Check for consent before loading GTM
$consent_service = \Drupal::service('eu_cookie_compliance.consent');
if ($consent_service && !$consent_service->hasConsent('marketing')) {
// Block GTM loading if no consent
return;
}
// Load GTM
$attachments['#attached']['library'][] = 'google_tag/gtm';
}
Consent Mode V2
// Set default consent state before GTM loads
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'analytics_storage': 'denied',
'functionality_storage': 'granted',
'personalization_storage': 'denied',
'security_storage': 'granted',
'wait_for_update': 500
});
// Update consent when user accepts
document.addEventListener('eu_cookie_compliance.changeStatus', function(event) {
if (event.detail.status === 'allow') {
gtag('consent', 'update', {
'analytics_storage': 'granted',
'ad_storage': event.detail.categories.marketing ? 'granted' : 'denied',
'ad_user_data': event.detail.categories.marketing ? 'granted' : 'denied',
'ad_personalization': event.detail.categories.marketing ? 'granted' : 'denied'
});
}
});
Testing Your Implementation
1. Verify GTM Container Loads
Browser DevTools:
Network tab → Filter: googletagmanager.com
Should see: gtm.js?id=GTM-XXXXXXX
2. GTM Preview Mode
- Open GTM → Preview
- Enter your Drupal site URL
- Verify container fires and data layer populates
3. Tag Assistant
Install Tag Assistant Legacy (by Google) Chrome extension
- Verify GTM container presence
- Check for multiple containers (potential duplicate)
- View tag firing sequence
4. Console Debugging
// View data layer in console
console.log(dataLayer);
// Monitor all pushes to data layer
var originalPush = dataLayer.push;
dataLayer.push = function() {
console.log('dataLayer.push:', arguments);
return originalPush.apply(dataLayer, arguments);
};
Common Issues & Solutions
GTM Not Loading
Clear Drupal cache:
drush crCheck module is enabled:
drush pm:list | grep google_tagVerify container ID in configuration
Check page exclusions - ensure not excluding current page
Data Layer Empty
- Check if data layer module is enabled
- Verify JavaScript aggregation settings
- Ensure GTM snippet loads before other scripts
Duplicate Containers
Check for multiple implementations:
- Module configuration
- Theme template
- Hard-coded in custom module
BigPipe Issues
Enable placeholder support in module settings:
include_file: true
rebuild_snippets: true
Performance Optimization
1. Preconnect to GTM
<?php
function mytheme_page_attachments(array &$attachments) {
$attachments['#attached']['html_head_link'][] = [
[
'rel' => 'preconnect',
'href' => 'https://www.googletagmanager.com',
],
TRUE
];
$attachments['#attached']['html_head_link'][] = [
[
'rel' => 'dns-prefetch',
'href' => 'https://www.googletagmanager.com',
],
TRUE
];
}
2. Async Tag Loading
GTM already loads asynchronously by default. Ensure tags within GTM also use async/defer.
3. Cache GTM Configuration
$attachments['#cache']['tags'][] = 'config:google_tag.settings';
$attachments['#cache']['contexts'][] = 'url.path';
Debugging Tools
Enable Debug Output
Module configuration:
debug_output: true
Or via settings.php:
$config['google_tag.settings']['debug_output'] = TRUE;
Drupal Messages
When debug is enabled, check Drupal messages for:
- Container ID being used
- Snippet insertion location
- Page inclusion/exclusion decisions