GTM Data Layer Configuration on Umbraco | OpsBlu Docs

GTM Data Layer Configuration on Umbraco

Configure advanced data layer with Razor, track e-commerce events, and implement custom variables

Configure a robust data layer in Umbraco to pass contextual data to Google Tag Manager. This guide covers Razor-based data layer implementation, e-commerce tracking, member data, and custom variables for advanced analytics.

Prerequisites

Before configuring the data layer:

  • GTM Setup Complete - Follow GTM Setup Guide
  • GTM Container Configured - Container ID installed on site
  • Umbraco Development Environment - Visual Studio or VS Code
  • Understanding of Data Layer - Familiarity with GTM data layer concepts

Basic Data Layer Implementation

Initialize Data Layer in Master Layout

Location: /Views/Master.cshtml

@using Umbraco.Cms.Core.Models.PublishedContent
@using Umbraco.Cms.Core.Security
@inject IMemberManager MemberManager
@inject Microsoft.Extensions.Hosting.IHostEnvironment HostEnvironment
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage

@{
    Layout = null;
    var member = await MemberManager.GetCurrentMemberAsync();
}

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

    <script>
        // Initialize dataLayer BEFORE GTM
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({
            'event': 'umbraco_page_data',
            'pageData': {
                'pageType': '@Model?.ContentType.Alias',
                'pageName': '@Model?.Name',
                'pageId': '@Model?.Id',
                'pageUrl': '@Context.Request.Path',
                'pageTitle': '@(Model?.Value("title") ?? Model?.Name)',
                'template': '@ViewData["TemplateAlias"]',
                'culture': '@Model?.GetCultureFromDomains()?.Culture',
                'createDate': '@Model?.CreateDate.ToString("yyyy-MM-dd")',
                'updateDate': '@Model?.UpdateDate.ToString("yyyy-MM-dd")'
            },
            'userData': {
                @if (member != null)
                {
                    <text>
                    'status': 'logged_in',
                    'memberType': '@member.ContentType.Alias',
                    'memberId': '@member.Key'
                    </text>
                }
                else
                {
                    <text>
                    'status': 'visitor'
                    </text>
                }
            },
            'siteData': {
                'environment': '@HostEnvironment.EnvironmentName',
                'siteName': '@Model?.Root()?.Name',
                'siteId': '@Model?.Root()?.Id'
            }
        });
    </script>

    @await Html.PartialAsync("~/Views/Partials/Analytics/GoogleTagManager.cshtml")
</head>
<body>
    @await Html.PartialAsync("~/Views/Partials/Analytics/GoogleTagManagerNoScript.cshtml")
    @RenderBody()
</body>
</html>

Content-Specific Data Layer

Article/Blog Post Data Layer

Location: /Views/ArticlePage.cshtml

@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@{
    Layout = "Master.cshtml";

    var author = Model?.Value<IPublishedContent>("author");
    var category = Model?.Value<IPublishedContent>("category");
    var tags = Model?.Value<IEnumerable<IPublishedContent>>("tags");
}

<script>
    dataLayer.push({
        'event': 'article_view',
        'articleData': {
            'title': '@Model?.Name',
            'author': '@(author?.Name ?? "Unknown")',
            'category': '@(category?.Name ?? "Uncategorized")',
            'tags': [@string.Join(", ", tags?.Select(t => $"'{t.Name}'") ?? new List<string>())],
            'publishDate': '@Model?.Value<DateTime>("publishDate").ToString("yyyy-MM-dd")',
            'wordCount': @(Model?.Value<string>("bodyText")?.Split(' ').Length ?? 0),
            'readTime': @Math.Ceiling((Model?.Value<string>("bodyText")?.Split(' ').Length ?? 0) / 200.0) // Assuming 200 words/min
        }
    });
</script>

<!-- Article content -->
<article>
    <h1>@Model?.Name</h1>
    @Html.Raw(Model?.Value("bodyText"))
</article>

Product Page Data Layer

Location: /Views/ProductPage.cshtml

@using Vendr.Core.Api
@inject IVendrApi VendrApi
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage

@{
    Layout = "Master.cshtml";

    var sku = Model?.GetProperty("sku")?.Value<string>();
    var price = Model?.GetProperty("price")?.Value<decimal>() ?? 0;
    var category = Model?.Parent?.Name;
    var brand = Model?.GetProperty("brand")?.Value<string>();
    var inStock = Model?.GetProperty("inStock")?.Value<bool>() ?? false;
}

<script>
    dataLayer.push({
        'event': 'view_item',
        'ecommerce': {
            'currency': 'USD',
            'value': @price,
            'items': [{
                'item_id': '@sku',
                'item_name': '@Model?.Name',
                'item_brand': '@brand',
                'item_category': '@category',
                'price': @price,
                'quantity': 1,
                'in_stock': @(inStock ? "true" : "false")
            }]
        }
    });
</script>

<!-- Product content -->

E-commerce Data Layer

Cart Data Layer Helper

Create Service: Services/DataLayerService.cs

using System.Collections.Generic;
using System.Linq;
using Vendr.Core.Models;

namespace YourProject.Services
{
    public class DataLayerService
    {
        public object BuildCartDataLayer(OrderReadOnly order)
        {
            return new
            {
                @event = "view_cart",
                ecommerce = new
                {
                    currency = order.CurrencyCode,
                    value = order.TotalPrice.Value.WithoutAdjustments.Value,
                    items = order.OrderLines.Select((line, index) => new
                    {
                        item_id = line.Sku,
                        item_name = line.Name,
                        price = line.TotalPrice.Value.WithoutAdjustments.Value / line.Quantity,
                        quantity = line.Quantity,
                        index = index
                    }).ToArray()
                }
            };
        }

        public object BuildCheckoutDataLayer(OrderReadOnly order, int step)
        {
            return new
            {
                @event = "begin_checkout",
                ecommerce = new
                {
                    currency = order.CurrencyCode,
                    value = order.TotalPrice.Value.WithoutAdjustments.Value,
                    checkout_step = step,
                    items = order.OrderLines.Select(line => new
                    {
                        item_id = line.Sku,
                        item_name = line.Name,
                        price = line.TotalPrice.Value.WithoutAdjustments.Value / line.Quantity,
                        quantity = line.Quantity
                    }).ToArray()
                }
            };
        }

        public object BuildPurchaseDataLayer(OrderReadOnly order)
        {
            return new
            {
                @event = "purchase",
                ecommerce = new
                {
                    transaction_id = order.OrderNumber,
                    value = order.TotalPrice.Value.WithoutAdjustments.Value,
                    tax = order.TotalPrice.Value.TotalAdjustment.Value,
                    shipping = order.ShippingInfo?.TotalPrice.Value.WithoutAdjustments.Value ?? 0,
                    currency = order.CurrencyCode,
                    coupon = order.DiscountCodes.FirstOrDefault()?.Code,
                    items = order.OrderLines.Select(line => new
                    {
                        item_id = line.Sku,
                        item_name = line.Name,
                        price = line.TotalPrice.Value.WithoutAdjustments.Value / line.Quantity,
                        quantity = line.Quantity,
                        item_category = line.Properties.GetValue("category")
                    }).ToArray()
                }
            };
        }
    }
}

Cart Page Data Layer

@using Vendr.Core.Api
@using System.Text.Json
@inject IVendrApi VendrApi
@inject YourProject.Services.DataLayerService DataLayerService
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage

@{
    var order = VendrApi.GetCurrentOrder(Model.Value<Guid>("store"));
}

@if (order != null)
{
    var dataLayer = DataLayerService.BuildCartDataLayer(order);

    <script>
        dataLayer.push(@Html.Raw(JsonSerializer.Serialize(dataLayer)));
    </script>
}

Purchase Confirmation Data Layer

@using Vendr.Core.Api
@using System.Text.Json
@inject IVendrApi VendrApi
@inject YourProject.Services.DataLayerService DataLayerService
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage

@{
    var orderRef = Request.Query["order"];
    var order = VendrApi.GetOrder(Guid.Parse(orderRef));
}

@if (order != null && Session["OrderTracked_@order.OrderNumber"] == null)
{
    Session["OrderTracked_@order.OrderNumber"] = true;
    var dataLayer = DataLayerService.BuildPurchaseDataLayer(order);

    <script>
        dataLayer.push(@Html.Raw(JsonSerializer.Serialize(dataLayer)));
    </script>
}

Form Interaction Data Layer

Umbraco Forms Data Layer

Create custom workflow: FormDataLayerWorkflow.cs

using System.Collections.Generic;
using Umbraco.Forms.Core;
using Umbraco.Forms.Core.Enums;
using Umbraco.Forms.Core.Persistence.Dtos;

namespace YourProject.FormWorkflows
{
    public class FormDataLayerWorkflow : WorkflowType
    {
        public FormDataLayerWorkflow()
        {
            Id = new Guid("YOUR-WORKFLOW-GUID");
            Name = "Push to Data Layer";
            Description = "Pushes form submission data to GTM data layer";
            Icon = "icon-google";
            Group = "Analytics";
        }

        public override WorkflowExecutionStatus Execute(WorkflowExecutionContext context)
        {
            var form = context.Form;
            var record = context.Record;

            // Build data layer object
            var dataLayerEvent = new Dictionary<string, object>
            {
                { "event", "form_submit" },
                {
                    "formData", new Dictionary<string, object>
                    {
                        { "formId", form.Id },
                        { "formName", form.Name },
                        { "recordId", record.Id },
                        { "pageId", record.PageId }
                    }
                }
            };

            // Store in TempData to access in view
            context.HttpContext.Items["FormDataLayer"] = dataLayerEvent;

            return WorkflowExecutionStatus.Completed;
        }

        public override List<Exception> ValidateSettings()
        {
            return new List<Exception>();
        }
    }
}

Form Submission Data Layer (Client-Side)

@using Umbraco.Forms.Web
@model Umbraco.Forms.Web.Models.FormViewModel

<script>
    var formElement = document.querySelector('.umbraco-forms-form');

    // Track form start
    var formStarted = false;
    formElement.querySelectorAll('input, textarea, select').forEach(function(field) {
        field.addEventListener('focus', function() {
            if (!formStarted) {
                formStarted = true;

                dataLayer.push({
                    'event': 'form_start',
                    'formData': {
                        'formId': '@Model.FormId',
                        'formName': '@Model.FormName',
                        'pageUrl': window.location.href
                    }
                });
            }
        });
    });

    // Track form submission
    formElement.addEventListener('submit', function() {
        dataLayer.push({
            'event': 'form_submit',
            'formData': {
                'formId': '@Model.FormId',
                'formName': '@Model.FormName',
                'pageUrl': window.location.href
            }
        });
    });
</script>

Member/User Data Layer

Enhanced Member Data Layer

@using Umbraco.Cms.Core.Security
@inject IMemberManager MemberManager
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage

@{
    var member = await MemberManager.GetCurrentMemberAsync();
}

@if (member != null)
{
    var memberGroups = member.Roles?.ToList() ?? new List<string>();
    var membershipDate = member.GetValue<DateTime>("membershipDate");
    var lifetimeValue = member.GetValue<decimal>("lifetimeValue");

    <script>
        dataLayer.push({
            'event': 'member_page_view',
            'memberData': {
                'memberId': '@member.Key',
                'memberType': '@member.ContentType.Alias',
                'memberGroups': [@string.Join(", ", memberGroups.Select(g => $"'{g}'"))],
                'membershipDate': '@membershipDate.ToString("yyyy-MM-dd")',
                'daysSinceMembership': @((DateTime.Now - membershipDate).Days),
                'lifetimeValue': @lifetimeValue,
                'emailVerified': @(member.EmailConfirmed ? "true" : "false")
            }
        });
    </script>
}

Custom Events Data Layer

Scroll Depth Tracking

<script>
    var scrollDepths = [25, 50, 75, 100];
    var triggeredDepths = [];

    window.addEventListener('scroll', function() {
        var scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;

        scrollDepths.forEach(function(depth) {
            if (scrollPercent >= depth && !triggeredDepths.includes(depth)) {
                triggeredDepths.push(depth);

                dataLayer.push({
                    'event': 'scroll_depth',
                    'scrollData': {
                        'depth': depth,
                        'pagePath': window.location.pathname,
                        'pageTitle': document.title
                    }
                });
            }
        });
    });
</script>

Video Tracking Data Layer

<video id="product-video" controls>
    <source src="@Model?.Value("videoUrl")" type="video/mp4">
</video>

<script>
    var video = document.getElementById('product-video');
    var videoTitle = '@Model?.Value("videoTitle")';

    video.addEventListener('play', function() {
        dataLayer.push({
            'event': 'video_start',
            'videoData': {
                'title': videoTitle,
                'url': '@Model?.Value("videoUrl")',
                'duration': video.duration,
                'currentTime': video.currentTime
            }
        });
    });

    video.addEventListener('ended', function() {
        dataLayer.push({
            'event': 'video_complete',
            'videoData': {
                'title': videoTitle,
                'url': '@Model?.Value("videoUrl")',
                'duration': video.duration
            }
        });
    });

    // Track 25%, 50%, 75% milestones
    var milestones = [0.25, 0.5, 0.75];
    var firedMilestones = [];

    video.addEventListener('timeupdate', function() {
        var progress = video.currentTime / video.duration;

        milestones.forEach(function(milestone) {
            if (progress >= milestone && !firedMilestones.includes(milestone)) {
                firedMilestones.push(milestone);

                dataLayer.push({
                    'event': 'video_progress',
                    'videoData': {
                        'title': videoTitle,
                        'progress': milestone * 100
                    }
                });
            }
        });
    });
</script>

Server-Side Data Layer Push

Push Data Layer from Controller

using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Web.Common.Controllers;

namespace YourProject.Controllers
{
    public class SearchController : SurfaceController
    {
        [HttpGet]
        public IActionResult Search(string query)
        {
            var results = PerformSearch(query);

            // Store data layer info in ViewData
            ViewData["DataLayerEvent"] = new
            {
                @event = "search",
                searchData = new
                {
                    searchTerm = query,
                    resultsCount = results.Count(),
                    hasResults = results.Any()
                }
            };

            return View("SearchResults", results);
        }
    }
}

In SearchResults.cshtml:

@using System.Text.Json

@if (ViewData["DataLayerEvent"] != null)
{
    <script>
        dataLayer.push(@Html.Raw(JsonSerializer.Serialize(ViewData["DataLayerEvent"])));
    </script>
}

Data Layer Testing and Validation

Debug Data Layer

@inject Microsoft.Extensions.Hosting.IHostEnvironment HostEnvironment

@if (HostEnvironment.IsDevelopment())
{
    <script>
        // Log all data layer pushes
        var originalPush = window.dataLayer.push;
        window.dataLayer.push = function() {
            console.log('Data Layer Push:', arguments);
            return originalPush.apply(window.dataLayer, arguments);
        };

        // Log current data layer state
        console.log('Current Data Layer:', window.dataLayer);
    </script>
}

Validate Data Layer in GTM

  1. Log in to Google Tag Manager
  2. Click Preview button
  3. Enter your Umbraco site URL
  4. Navigate to Variables tab
  5. Verify custom data layer variables populate
  6. Check Data Layer tab for all push events

Common Issues

Data Layer Not Defined

Cause: GTM loads before data layer initialization Solution: Always initialize data layer BEFORE GTM script

<script>
    // Initialize first
    window.dataLayer = window.dataLayer || [];
    dataLayer.push({ /* your data */ });
</script>

<!-- Then load GTM -->
@await Html.PartialAsync("~/Views/Partials/Analytics/GoogleTagManager.cshtml")

Null Reference Errors

Cause: Razor variables are null Solution: Use null-conditional operators

'pageName': '@(Model?.Name ?? "Unknown")',
'category': '@(category?.Name ?? "Uncategorized")'

JSON Serialization Issues

Cause: Special characters in Razor output Solution: Use System.Text.Json

@using System.Text.Json

<script>
    dataLayer.push(@Html.Raw(JsonSerializer.Serialize(dataObject)));
</script>

Next Steps