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
- Flexibility: Update tracking without deploying code
- Multiple Tags: Manage GA4, Meta Pixel, and other tags from one place
- Testing: Built-in preview and debug mode
- Version Control: Track changes and roll back if needed
- 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
Method 1: Master Layout Template (Recommended)
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
- Create Workspace: Use separate workspaces for different features
- Set up Built-in Variables: Enable necessary built-in variables
- Create Tags: Add GA4, Meta Pixel, etc.
- Create Triggers: Configure when tags fire
- Test: Use Preview mode
- Publish: Create version and publish
Recommended Built-in Variables
Enable these in GTM > Variables > Built-in Variables:
Pages
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:
- GTM script in
<head>section - Noscript iframe after
<body>tag - Correct Container ID
- Script NOT present in edit mode
2. GTM Preview Mode
- Open GTM container
- Click Preview
- Enter your Episerver site URL
- Verify GTM debugger connects
- Check that tags fire correctly
3. Browser Developer Tools
- Open DevTools Network tab
- Filter for
googletagmanager.com - Verify
gtm.jsloads successfully - Check for
dataLayerin 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:
- Verify Container ID is correct
- Check edit mode detection is working
- Ensure no Content Security Policy blocking
- 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:
- Search codebase for
googletagmanager.com - Remove duplicate implementations
- 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
- Data Layer Implementation - Set up data layer
- GA4 Setup - Configure GA4 via GTM
- Meta Pixel Setup - Add Meta Pixel via GTM
- Troubleshooting - Fix common issues