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
- Log in to Google Tag Manager
- Click Preview button
- Enter your Umbraco site URL
- Navigate to Variables tab
- Verify custom data layer variables populate
- 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
- GTM Setup - Install Google Tag Manager
- Google Analytics Setup - Configure GA4 via GTM
- Troubleshooting Tracking - Debug data layer issues