Google Tag Manager Setup on Drupal | Blue Frog Docs

Google Tag Manager Setup on Drupal

Complete guide to implementing Google Tag Manager on Drupal using modules and manual methods

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.


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):

&gtm_auth=YOUR_AUTH&gtm_preview=env-X&gtm_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

Data Layer:

☑ 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

  1. Create server container in GTM
  2. Deploy to Cloud Run, App Engine, or custom server
  3. 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

<?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';
}
// 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

  1. Open GTM → Preview
  2. Enter your Drupal site URL
  3. 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

  1. Clear Drupal cache:

    drush cr
    
  2. Check module is enabled:

    drush pm:list | grep google_tag
    
  3. Verify container ID in configuration

  4. Check page exclusions - ensure not excluding current page

Data Layer Empty

  1. Check if data layer module is enabled
  2. Verify JavaScript aggregation settings
  3. 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

Next Steps


Resources

// SYS.FOOTER