Google Tag Manager Setup on Episerver | OpsBlu Docs

Google Tag Manager Setup on Episerver

Complete guide to installing and configuring Google Tag Manager on Episerver CMS and Commerce (Optimizely)

This guide covers setting up Google Tag Manager (GTM) on Episerver CMS and Commerce (now Optimizely) for flexible tag management.

Prerequisites

  • Episerver CMS 11+ or CMS 12+
  • Google Tag Manager account created
  • GTM Container ID (format: GTM-XXXXXXX)
  • Access to Episerver solution and templates

Why Use GTM with Episerver?

Benefits

  1. Flexibility: Update tracking without deploying code
  2. Multiple Tags: Manage GA4, Meta Pixel, and other tags from one place
  3. Testing: Built-in preview and debug mode
  4. Version Control: Track changes and roll back if needed
  5. Team Collaboration: Marketing can manage tags without developers

GTM vs. Direct Implementation

Feature GTM Direct
Deployment Speed Fast (no code deploy) Slow (requires deployment)
Non-technical Updates Yes No
Tag Management Centralized Scattered
Testing Tools Built-in Manual
Performance Slight overhead Faster
Complexity Higher initial setup Simpler

Recommendation: Use GTM for most Episerver implementations, especially if marketing teams need tag management flexibility.

Implementation Methods

Add GTM to your master layout for site-wide coverage.

Step 1: Locate Master Layout

Find your main layout file:

  • Views/Shared/_Layout.cshtml (Standard MVC)
  • Views/Shared/_Root.cshtml (Episerver Foundation)
  • Views/Shared/Layouts/_Layout.cshtml (Custom)

Step 2: Add GTM Container Code

Add to the <head> section (as high as possible):

@{
    var gtmId = "GTM-XXXXXXX"; // Replace with your Container ID
    var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
}

@if (!isEditMode)
{
    <!-- 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 -->
}

Add immediately after opening <body> tag:

@if (!isEditMode)
{
    <!-- 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) -->
}

Complete Example:

<!DOCTYPE html>
<html lang="@Model.Language?.Name">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewBag.Title</title>

    @{
        var gtmId = "GTM-XXXXXXX";
        var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
    }

    @if (!isEditMode)
    {
        <!-- 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 -->
    }

    @RenderSection("head", required: false)
</head>
<body>
    @if (!isEditMode)
    {
        <!-- 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) -->
    }

    @RenderBody()

    @RenderSection("scripts", required: false)
</body>
</html>

Method 2: Configuration-Based Approach

Store GTM Container ID in configuration for flexibility across environments.

For CMS 12+ (appsettings.json)

{
  "GoogleTagManager": {
    "ContainerId": "GTM-XXXXXXX",
    "Enabled": true,
    "EnableInEditMode": false,
    "EnableInPreviewMode": false
  }
}

For CMS 11 (web.config)

<configuration>
  <appSettings>
    <add key="GTM:ContainerId" value="GTM-XXXXXXX" />
    <add key="GTM:Enabled" value="true" />
    <add key="GTM:EnableInEditMode" value="false" />
    <add key="GTM:EnableInPreviewMode" value="false" />
  </appSettings>
</configuration>

Create Configuration Class

public class GoogleTagManagerSettings
{
    public string ContainerId { get; set; }
    public bool Enabled { get; set; }
    public bool EnableInEditMode { get; set; }
    public bool EnableInPreviewMode { get; set; }
}

Register in Startup (CMS 12+)

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<GoogleTagManagerSettings>(
            Configuration.GetSection("GoogleTagManager"));

        services.AddMvc();
        // Other services...
    }
}

Update Layout Template

@using Microsoft.Extensions.Options
@inject IOptions<GoogleTagManagerSettings> GTMSettings

@{
    var gtmSettings = GTMSettings.Value;
    var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
    var isPreviewMode = EPiServer.Web.ContextMode.Current == EPiServer.Web.ContextMode.Preview;

    var shouldLoadGTM = gtmSettings.Enabled &&
                        !string.IsNullOrEmpty(gtmSettings.ContainerId) &&
                        (!isEditMode || gtmSettings.EnableInEditMode) &&
                        (!isPreviewMode || gtmSettings.EnableInPreviewMode);
}

@if (shouldLoadGTM)
{
    <!-- 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','@gtmSettings.ContainerId');
    </script>
    <!-- End Google Tag Manager -->
}

Method 3: Multi-Site Configuration

Manage different GTM containers for multiple sites.

Site Extension Method

public static class SiteDefinitionExtensions
{
    public static string GetGTMContainerId(this SiteDefinition site)
    {
        return site.SiteUrl.Host switch
        {
            "www.site1.com" => "GTM-AAAAAAA",
            "www.site2.com" => "GTM-BBBBBBB",
            "www.site3.com" => "GTM-CCCCCCC",
            _ => "GTM-DEFAULT"
        };
    }
}

Or use site-level settings:

[UIHint("Textarea")]
public virtual string GTMContainerId { get; set; }

Use in Template

@{
    var currentSite = SiteDefinition.Current;
    var gtmId = currentSite.GetGTMContainerId();
    var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
}

@if (!string.IsNullOrEmpty(gtmId) && !isEditMode)
{
    <!-- 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 -->
}

Method 4: View Component Approach

Create a reusable view component for cleaner templates.

Create View Component

using Microsoft.AspNetCore.Mvc;
using EPiServer;
using EPiServer.Web;
using Microsoft.Extensions.Options;

public class GoogleTagManagerViewComponent : ViewComponent
{
    private readonly IOptions<GoogleTagManagerSettings> _settings;

    public GoogleTagManagerViewComponent(IOptions<GoogleTagManagerSettings> settings)
    {
        _settings = settings;
    }

    public IViewComponentResult Invoke(string location)
    {
        var settings = _settings.Value;

        // Don't load in edit mode unless explicitly enabled
        if (EPiServer.Editor.PageEditing.PageIsInEditMode &&
            !settings.EnableInEditMode)
        {
            return Content(string.Empty);
        }

        // Don't load in preview mode unless explicitly enabled
        if (EPiServer.Web.ContextMode.Current == EPiServer.Web.ContextMode.Preview &&
            !settings.EnableInPreviewMode)
        {
            return Content(string.Empty);
        }

        if (!settings.Enabled || string.IsNullOrEmpty(settings.ContainerId))
        {
            return Content(string.Empty);
        }

        var viewName = location == "head"
            ? "~/Views/Shared/Components/GoogleTagManager/Head.cshtml"
            : "~/Views/Shared/Components/GoogleTagManager/Body.cshtml";

        return View(viewName, settings.ContainerId);
    }
}

Create Views

Views/Shared/Components/GoogleTagManager/Head.cshtml:

@model string

<!-- 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','@Model');
</script>
<!-- End Google Tag Manager -->

Views/Shared/Components/GoogleTagManager/Body.cshtml:

@model string

<!-- Google Tag Manager (noscript) -->
<noscript>
    <iframe src="https://www.googletagmanager.com/ns.html?id=@Model"
            height="0" width="0" style="display:none;visibility:hidden">
    </iframe>
</noscript>
<!-- End Google Tag Manager (noscript) -->

Use in Layout

<head>
    <!-- Other head content -->
    @await Component.InvokeAsync("GoogleTagManager", new { location = "head" })
</head>
<body>
    @await Component.InvokeAsync("GoogleTagManager", new { location = "body" })

    <!-- Rest of body -->
</body>

Environment-Specific Containers

Use different GTM containers for development, staging, and production.

appsettings.json Approach

appsettings.Development.json:

{
  "GoogleTagManager": {
    "ContainerId": "GTM-DEVXXXX",
    "Enabled": true
  }
}

appsettings.Staging.json:

{
  "GoogleTagManager": {
    "ContainerId": "GTM-STAGEXXX",
    "Enabled": true
  }
}

appsettings.Production.json:

{
  "GoogleTagManager": {
    "ContainerId": "GTM-PRODXXX",
    "Enabled": true
  }
}

Content Delivery API (Headless)

For headless Episerver implementations with separate frontend.

Server-Side Setup

Return GTM Container ID in API responses:

public class ContentApiModelConverter : IContentApiModelConverter
{
    private readonly IOptions<GoogleTagManagerSettings> _gtmSettings;

    public ContentApiModelConverter(IOptions<GoogleTagManagerSettings> gtmSettings)
    {
        _gtmSettings = gtmSettings;
    }

    public ConvertedContentApiModel Convert(IContent content, ConverterContext converterContext)
    {
        var model = new ConvertedContentApiModel();

        // Add GTM configuration to metadata
        model.Properties.Add("gtmConfig", new
        {
            containerId = _gtmSettings.Value.ContainerId,
            enabled = _gtmSettings.Value.Enabled
        });

        return model;
    }
}

Client-Side Implementation (React Example)

// useGTM.js
import { useEffect } from 'react';

export function useGTM(containerId) {
  useEffect(() => {
    if (!containerId) return;

    // Add GTM script to head
    const script = document.createElement('script');
    script.innerHTML = `
      (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}');
    `;
    document.head.appendChild(script);

    // Add noscript iframe to body
    const noscript = document.createElement('noscript');
    noscript.innerHTML = `
      <iframe src="https://www.googletagmanager.com/ns.html?id=${containerId}"
              height="0" width="0" style="display:none;visibility:hidden">
      </iframe>
    `;
    document.body.insertBefore(noscript, document.body.firstChild);

    return () => {
      // Cleanup if needed
    };
  }, [containerId]);
}

// App.js
import { useGTM } from './hooks/useGTM';

function App({ content }) {
  useGTM(content.gtmConfig?.containerId);

  return (
    <div>
      {/* Your app */}
    </div>
  );
}

GTM Container Setup

Basic Container Configuration

  1. Create Workspace: Use separate workspaces for different features
  2. Set up Built-in Variables: Enable necessary built-in variables
  3. Create Tags: Add GA4, Meta Pixel, etc.
  4. Create Triggers: Configure when tags fire
  5. Test: Use Preview mode
  6. Publish: Create version and publish

Enable these in GTM > Variables > Built-in Variables:

Pages

  • Page URL
  • Page Hostname
  • Page Path
  • Referrer

Utilities

  • Event
  • Environment Name
  • Container ID
  • Container Version

Errors

  • Error Message
  • Error URL
  • Error Line

Verification

1. Check Script Loading

View page source and verify:

  1. GTM script in <head> section
  2. Noscript iframe after <body> tag
  3. Correct Container ID
  4. Script NOT present in edit mode

2. GTM Preview Mode

  1. Open GTM container
  2. Click Preview
  3. Enter your Episerver site URL
  4. Verify GTM debugger connects
  5. Check that tags fire correctly

3. Browser Developer Tools

  1. Open DevTools Network tab
  2. Filter for googletagmanager.com
  3. Verify gtm.js loads successfully
  4. Check for dataLayer in Console:
    console.log(window.dataLayer);
    

4. Tag Assistant

Use Google Tag Assistant to verify installation.

Troubleshooting

GTM Not Loading

Symptoms: No GTM requests in Network tab

Solutions:

  1. Verify Container ID is correct
  2. Check edit mode detection is working
  3. Ensure no Content Security Policy blocking
  4. Check browser console for JavaScript errors

Edit Mode Check Not Working

Problem: GTM loads in Episerver edit mode

Solution: Verify edit mode check:

var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;

For CMS 11, you may need:

var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode(HttpContext);

Preview Mode Issues

Problem: Want to track in preview mode

Solution: Remove preview mode check or add configuration option:

var isPreviewMode = EPiServer.Web.ContextMode.Current == EPiServer.Web.ContextMode.Preview;
if (!isEditMode && !isPreviewMode)
{
    // Load GTM
}

Content Security Policy Errors

Error: "Refused to load the script..."

Solution: Update CSP headers in web.config:

<system.webServer>
  <httpProtocol>
    <customHeaders>
      <add name="Content-Security-Policy"
           value="script-src 'self' 'unsafe-inline' https://*.googletagmanager.com; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com; img-src 'self' https://*.google-analytics.com https://*.googletagmanager.com;" />
    </customHeaders>
  </httpProtocol>
</system.webServer>

Multiple GTM Containers

Problem: Multiple containers loading on same page

Solution:

  1. Search codebase for googletagmanager.com
  2. Remove duplicate implementations
  3. Use single, centralized location

Security Considerations

1. Container ID Validation

Validate container ID format:

public static bool IsValidGTMContainerId(string containerId)
{
    return !string.IsNullOrEmpty(containerId) &&
           Regex.IsMatch(containerId, @"^GTM-[A-Z0-9]{7}$");
}

2. Prevent Injection

Always use Razor's HTML encoding:

@gtmSettings.ContainerId // Automatically encoded

3. Server-Side GTM (Optional)

For enhanced security and server-side tracking, consider Server-Side GTM.

Performance Optimization

1. DNS Prefetch

Add to <head>:

<link rel="dns-prefetch" href="//www.googletagmanager.com">

2. Preconnect

<link rel="preconnect" href="https://www.googletagmanager.com">

3. Async Loading

GTM loads asynchronously by default, but ensure no blocking scripts before it.

Next Steps

Additional Resources