GTM Data Layer Implementation for Episerver | OpsBlu Docs

GTM Data Layer Implementation for Episerver

Implement comprehensive data layer structure for Google Tag Manager on Episerver CMS and Commerce

Learn how to implement a comprehensive data layer for Google Tag Manager on Episerver (Optimizely) CMS and Commerce.

Prerequisites

Data Layer Overview

The data layer is a JavaScript object that stores information about the page, user, and events for GTM to access.

Basic Structure

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'event': 'eventName',
  'property1': 'value1',
  'property2': 'value2'
});

Page Load Data Layer

Initialize the data layer on every page load with common information.

Create Helper Class

using EPiServer;
using EPiServer.Core;
using EPiServer.Web;
using EPiServer.ServiceLocation;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

public class GTMDataLayerHelper
{
    private readonly IContentLoader _contentLoader;
    private readonly ISiteDefinitionRepository _siteRepository;

    public GTMDataLayerHelper(
        IContentLoader contentLoader,
        ISiteDefinitionRepository siteRepository)
    {
        _contentLoader = contentLoader;
        _siteRepository = siteRepository;
    }

    public object GetPageDataLayer(IContent content)
    {
        var page = content as PageData;
        var currentSite = _siteRepository.Get(SiteDefinition.Current.Id);

        var dataLayer = new
        {
            // Page Information
            pageType = content.GetOriginalType().Name,
            pageId = content.ContentLink.ID.ToString(),
            pageName = content.Name,
            pageUrl = GetPageUrl(content),
            pageTemplate = page?.PageTypeName,

            // Site Information
            siteName = currentSite.Name,
            siteId = currentSite.Id.ToString(),
            language = (content as ILocalizable)?.Language?.Name ?? "en",

            // Content Information
            contentGuid = content.ContentGuid.ToString(),
            contentVersion = (content as IVersionable)?.Status.ToString(),
            publishedDate = (content as IChangeTrackable)?.Changed.ToString("yyyy-MM-dd"),

            // Category/Structure
            category = GetContentCategory(content),
            breadcrumb = GetBreadcrumb(content),

            // User Context
            isEditMode = PageEditing.PageIsInEditMode,
            isPreviewMode = ContextMode.Current == ContextMode.Preview,

            // Episerver Version
            cmsVersion = EPiServer.Framework.FrameworkVersion.Current.ToString()
        };

        return dataLayer;
    }

    public string GetPageDataLayerJson(IContent content)
    {
        var dataLayer = GetPageDataLayer(content);

        return JsonConvert.SerializeObject(dataLayer, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        });
    }

    private string GetPageUrl(IContent content)
    {
        var urlResolver = ServiceLocator.Current.GetInstance<IUrlResolver>();
        return urlResolver.GetUrl(content.ContentLink, null, new UrlResolverArguments
        {
            ContextMode = ContextMode.Default
        });
    }

    private string GetContentCategory(IContent content)
    {
        // Implement based on your content structure
        // Example: Return content area or categorization property
        if (content is ICategorizable categorizable)
        {
            return string.Join(", ", categorizable.Category?.Select(c => c.Description) ?? new string[0]);
        }

        return null;
    }

    private string[] GetBreadcrumb(IContent content)
    {
        var breadcrumb = new List<string>();
        var current = content;

        while (current != null && !(current is PageData && ((PageData)current).IsStartPage()))
        {
            breadcrumb.Insert(0, current.Name);
            var parent = _contentLoader.Get<IContent>(current.ParentLink);
            current = parent;
        }

        return breadcrumb.ToArray();
    }
}

Add to Master Layout

@using Newtonsoft.Json
@inject GTMDataLayerHelper DataLayerHelper

@{
    var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
}

@if (!isEditMode)
{
    <script>
      // Initialize data layer before GTM loads
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push(@Html.Raw(DataLayerHelper.GetPageDataLayerJson(Model)));
    </script>
}

<!-- GTM script here -->

Complete Example

<!DOCTYPE html>
<html>
<head>
    @{
        var gtmId = "GTM-XXXXXXX";
        var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
    }

    @if (!isEditMode)
    {
        <!-- Initialize Data Layer -->
        <script>
          window.dataLayer = window.dataLayer || [];
          window.dataLayer.push(@Html.Raw(DataLayerHelper.GetPageDataLayerJson(Model)));
        </script>

        <!-- 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>
    }
</head>
<body>
    <!-- Content -->
</body>
</html>

Commerce Data Layer

Product Data Layer Helper

using EPiServer.Commerce.Catalog.ContentTypes;
using EPiServer.Commerce.Order;
using Mediachase.Commerce;
using Mediachase.Commerce.Catalog;

public class GTMCommerceDataLayerHelper
{
    private readonly IContentLoader _contentLoader;
    private readonly ReferenceConverter _referenceConverter;
    private readonly ICurrentMarket _currentMarket;

    public GTMCommerceDataLayerHelper(
        IContentLoader contentLoader,
        ReferenceConverter referenceConverter,
        ICurrentMarket currentMarket)
    {
        _contentLoader = contentLoader;
        _referenceConverter = referenceConverter;
        _currentMarket = currentMarket;
    }

    public object GetProductDataLayer(EntryContentBase entry)
    {
        var variant = entry as VariationContent;
        var product = variant != null
            ? _contentLoader.Get<ProductContent>(variant.GetParentProducts().FirstOrDefault())
            : entry as ProductContent;

        var price = entry.GetDefaultPrice();
        var stock = entry.GetStockPlacement()?.Quantity ?? 0;

        return new
        {
            // Ecommerce object for GTM
            ecommerce = new
            {
                currencyCode = _currentMarket.GetCurrentMarket().DefaultCurrency.CurrencyCode,
                detail = new
                {
                    products = new[]
                    {
                        new
                        {
                            id = entry.Code,
                            name = entry.DisplayName,
                            price = price?.UnitPrice.Amount,
                            brand = product?.Brand,
                            category = GetCategoryPath(entry),
                            variant = variant?.VariantName,
                            stockLevel = stock,
                            currency = price?.UnitPrice.Currency.CurrencyCode
                        }
                    }
                }
            },

            // Additional product metadata
            productType = entry.GetOriginalType().Name,
            productId = entry.ContentLink.ID,
            inStock = stock > 0
        };
    }

    public object GetCartDataLayer(ICart cart)
    {
        var lineItems = cart.GetAllLineItems().Select((item, index) => new
        {
            id = item.Code,
            name = item.DisplayName,
            price = item.PlacedPrice,
            quantity = item.Quantity,
            variant = item.Properties["VariantName"]?.ToString(),
            position = index + 1
        }).ToArray();

        return new
        {
            ecommerce = new
            {
                currencyCode = cart.Currency.CurrencyCode,
                value = cart.GetTotal().Amount,
                items = lineItems
            },
            cartId = cart.OrderLink.OrderGroupId,
            cartItemCount = cart.GetAllLineItems().Sum(li => li.Quantity)
        };
    }

    public object GetCheckoutDataLayer(ICart cart, int step)
    {
        var lineItems = cart.GetAllLineItems().Select(item => new
        {
            id = item.Code,
            name = item.DisplayName,
            price = item.PlacedPrice,
            quantity = item.Quantity
        }).ToArray();

        var stepName = step switch
        {
            1 => "shipping",
            2 => "payment",
            3 => "review",
            _ => "unknown"
        };

        return new
        {
            ecommerce = new
            {
                currencyCode = cart.Currency.CurrencyCode,
                checkout = new
                {
                    actionField = new
                    {
                        step = step,
                        option = stepName
                    },
                    products = lineItems
                }
            },
            checkoutStep = step,
            checkoutStepName = stepName
        };
    }

    public object GetPurchaseDataLayer(IPurchaseOrder order)
    {
        var lineItems = order.GetAllLineItems().Select(item => new
        {
            id = item.Code,
            name = item.DisplayName,
            price = item.PlacedPrice,
            quantity = item.Quantity,
            variant = item.Properties["VariantName"]?.ToString()
        }).ToArray();

        var shipping = order.GetFirstShipment()?.ShippingSubTotal.Amount ?? 0;
        var tax = order.TaxTotal.Amount;

        return new
        {
            ecommerce = new
            {
                currencyCode = order.Currency.CurrencyCode,
                purchase = new
                {
                    actionField = new
                    {
                        id = order.OrderNumber,
                        affiliation = order.MarketName,
                        revenue = order.GetTotal().Amount,
                        tax = tax,
                        shipping = shipping,
                        coupon = order.GetFirstForm()?.CouponCodes.FirstOrDefault()
                    },
                    products = lineItems
                }
            },
            transactionId = order.OrderNumber,
            orderStatus = order.Status.ToString()
        };
    }

    private string GetCategoryPath(EntryContentBase entry)
    {
        var category = entry.GetCategories()
            .Select(c => _contentLoader.Get<NodeContent>(c))
            .FirstOrDefault();

        if (category == null) return string.Empty;

        var path = new List<string>();
        var current = category;

        while (current != null && !(current is CatalogContent))
        {
            path.Insert(0, current.DisplayName);
            var parent = _contentLoader.Get<IContent>(current.ParentLink);
            current = parent as NodeContent;
        }

        return string.Join("/", path);
    }
}

Product Page Data Layer

@model ProductPageViewModel
@inject GTMCommerceDataLayerHelper CommerceDataLayer

@{
    var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
}

@if (!isEditMode)
{
    <script>
      // Push product view to data layer
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push(@Html.Raw(JsonConvert.SerializeObject(
          CommerceDataLayer.GetProductDataLayer(Model.Product),
          new JsonSerializerSettings
          {
              ContractResolver = new CamelCasePropertyNamesContractResolver()
          }
      )));

      // Trigger GTM event
      window.dataLayer.push({
        'event': 'productView'
      });
    </script>
}

Cart Page Data Layer

@model CartPageViewModel
@inject GTMCommerceDataLayerHelper CommerceDataLayer

@{
    var cart = Model.Cart;
    var isEditMode = EPiServer.Editor.PageEditing.PageIsInEditMode;
}

@if (!isEditMode && cart != null)
{
    <script>
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push(@Html.Raw(JsonConvert.SerializeObject(
          CommerceDataLayer.GetCartDataLayer(cart),
          new JsonSerializerSettings
          {
              ContractResolver = new CamelCasePropertyNamesContractResolver()
          }
      )));

      window.dataLayer.push({
        'event': 'cartView'
      });
    </script>
}

Event Tracking via Data Layer

Custom Event Helper

// /Scripts/gtm-events.js
window.GTMEvents = window.GTMEvents || {};

GTMEvents.push = function(eventName, eventData) {
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    'event': eventName,
    ...eventData
  });
};

// Convenience methods
GTMEvents.trackClick = function(elementType, elementName, elementId) {
  this.push('elementClick', {
    'elementType': elementType,
    'elementName': elementName,
    'elementId': elementId,
    'pageUrl': window.location.pathname
  });
};

GTMEvents.trackFormSubmit = function(formName, formId) {
  this.push('formSubmit', {
    'formName': formName,
    'formId': formId,
    'formUrl': window.location.pathname
  });
};

GTMEvents.trackDownload = function(fileName, fileUrl) {
  this.push('fileDownload', {
    'fileName': fileName,
    'fileUrl': fileUrl,
    'fileExtension': fileName.split('.').pop()
  });
};

GTMEvents.trackSearch = function(searchTerm, resultCount) {
  this.push('siteSearch', {
    'searchTerm': searchTerm,
    'searchResults': resultCount,
    'searchLocation': window.location.pathname
  });
};

GTMEvents.trackVideo = function(action, videoTitle, videoUrl) {
  this.push('videoInteraction', {
    'videoAction': action,
    'videoTitle': videoTitle,
    'videoUrl': videoUrl
  });
};

GTMEvents.trackOutbound = function(url, linkText) {
  this.push('outboundClick', {
    'outboundUrl': url,
    'linkText': linkText,
    'linkDomain': new URL(url).hostname
  });
};

Usage in Templates

<button 'Subscribe', 'subscribe-btn')">
    Subscribe
</button>

<a href="/documents/file.pdf" this.href)">
    Download PDF
</a>

<form id="contact-form" Form', 'contact-form')">
    <!-- Form fields -->
</form>

Block-Specific Data Layer

Track data layer information for content blocks.

Block Data Layer Attribute

public class BlockDataLayerAttribute : Attribute
{
    public string EventName { get; set; }
    public bool TrackImpression { get; set; } = false;
    public bool TrackInteraction { get; set; } = true;

    public BlockDataLayerAttribute(string eventName)
    {
        EventName = eventName;
    }
}

Block Template with Data Layer

@model HeroBlockType

@{
    var blockId = Model.ContentLink.ID;
    var blockType = Model.GetOriginalType().Name;
}

<div class="hero-block"
     data-block-id="@blockId"
     data-block-type="@blockType"
     data-gtm-event="blockInteraction">

    <h2>@Model.Heading</h2>

    <button {
        'blockType': '@blockType',
        'blockId': '@blockId',
        'blockName': '@Model.Name',
        'buttonText': '@Model.ButtonText'
    });">
        @Model.ButtonText
    </button>
</div>

<script>
  // Track block impression
  (function() {
    var blockElement = document.querySelector('[data-block-id="@blockId"]');
    var observer = new IntersectionObserver(function(entries) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          GTMEvents.push('blockImpression', {
            'blockType': '@blockType',
            'blockId': '@blockId',
            'blockName': '@Model.Name'
          });
          observer.unobserve(entry.target);
        }
      });
    }, { threshold: 0.5 });

    observer.observe(blockElement);
  })();
</script>

User Information Data Layer

Add authenticated user information (respecting privacy).

public object GetUserDataLayer()
{
    var user = HttpContext.Current.User;
    var isAuthenticated = user?.Identity?.IsAuthenticated ?? false;

    if (!isAuthenticated)
    {
        return new
        {
            userStatus = "guest",
            userId = null,
            userType = "anonymous"
        };
    }

    var contact = CustomerContext.Current.CurrentContact;

    return new
    {
        userStatus = "logged_in",
        userId = contact?.PrimaryKeyId.ToString(), // Hashed or anonymized
        userType = contact?.ContactType,
        // DO NOT include PII like name, email, etc.
    };
}

Add to page data layer:

<script>
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    @Html.Raw(DataLayerHelper.GetPageDataLayerJson(Model)),
    ...@Html.Raw(JsonConvert.SerializeObject(DataLayerHelper.GetUserDataLayer()))
  });
</script>

Personalization Data Layer

Track personalized content and visitor groups.

public object GetPersonalizationDataLayer()
{
    var visitor = ServiceLocator.Current.GetInstance<IVisitorGroupRepository>();
    var visitorGroups = visitor.List()
        .Where(vg => vg.IsMatch(HttpContext.Current.User))
        .Select(vg => vg.Name)
        .ToArray();

    return new
    {
        visitorGroups = visitorGroups,
        personalized = visitorGroups.Any(),
        activeExperiments = GetActiveExperiments()
    };
}

private string[] GetActiveExperiments()
{
    // Integrate with Optimizely Web Experimentation if used
    return new string[0];
}

Form Tracking Data Layer

Track Episerver Forms via data layer.

[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class FormDataLayerInitialization : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        FormsEvents.Instance.FormSubmitted += OnFormSubmitted;
    }

    private void OnFormSubmitted(object sender, FormsEventArgs e)
    {
        var script = $@"
            <script>
                GTMEvents.push('formSubmitted', {{
                    'formName': '{e.FormsContent.Name}',
                    'formId': '{e.FormsContent.ContentLink.ID}',
                    'formType': 'episerver_form'
                }});
            </script>
        ";

        HttpContext.Current.Response.Write(script);
    }

    public void Uninitialize(InitializationEngine context)
    {
        FormsEvents.Instance.FormSubmitted -= OnFormSubmitted;
    }
}

Testing Data Layer

1. Browser Console

// View data layer
console.log(window.dataLayer);

// View latest push
console.log(window.dataLayer[window.dataLayer.length - 1]);

// Push test event
window.dataLayer.push({
  'event': 'testEvent',
  'testData': 'test value'
});

2. GTM Preview Mode

  1. Enable GTM Preview
  2. Visit your site
  3. Check "Data Layer" tab
  4. Verify your variables appear

3. Data Layer Viewer Extension

Install browser extension:

Best Practices

1. Naming Conventions

Use camelCase for consistency:

{
  'event': 'buttonClick',
  'elementType': 'cta',
  'elementName': 'subscribe'
}

2. Data Layer Structure

Keep it flat when possible:

// Good
{
  'productId': '123',
  'productName': 'Widget'
}

// Avoid deep nesting unless necessary
{
  'product': {
    'details': {
      'identification': {
        'id': '123'
      }
    }
  }
}

3. Avoid PII

Never include personally identifiable information:

  • Email addresses
  • Full names
  • Phone numbers
  • Addresses

Use hashed IDs instead:

var hashedUserId = GetHashedUserId(contact.Email);

4. Initialize Early

Initialize data layer BEFORE GTM loads:

<script>
  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({ /* initial data */ });
</script>
<!-- GTM script here -->

5. Use Events

Push events for actions:

dataLayer.push({
  'event': 'productAdded',
  'productId': '123'
});

Common Data Layer Variables

Create these GTM variables to access data layer values:

Variable Name Type Data Layer Variable
Page Type Data Layer pageType
Page ID Data Layer pageId
Site Name Data Layer siteName
Language Data Layer language
User Status Data Layer userStatus
Product ID Data Layer ecommerce.detail.products.0.id
Cart Total Data Layer ecommerce.value

Next Steps

Additional Resources