Learn how to implement a comprehensive data layer for Google Tag Manager on Kentico Xperience, enabling powerful tracking and marketing automation without code deployments.
Data Layer Fundamentals
The data layer is a JavaScript object that stores information about the page, user, and events. GTM reads from this data layer to populate tags and make decisions about when to fire them.
Basic Structure
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'page_view',
'page': {
'type': 'home',
'category': 'landing'
},
'user': {
'authenticated': false
}
});
Initial Data Layer Setup
Base Data Layer (All Pages)
Add this code in your layout file before the GTM container snippet:
MVC Implementation
@using CMS.DocumentEngine
@using CMS.Membership
@using CMS.SiteProvider
@{
var currentDoc = DocumentContext.CurrentDocument;
var currentUser = MembershipContext.AuthenticatedUser;
var currentSite = SiteContext.CurrentSite;
}
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'pageData': {
'pageType': '@(currentDoc?.ClassName ?? "unknown")',
'pageTemplate': '@(currentDoc?.DocumentPageTemplateID ?? 0)',
'pageName': '@(currentDoc?.DocumentName ?? "")',
'pageID': '@(currentDoc?.DocumentID ?? 0)',
'nodeID': '@(currentDoc?.NodeID ?? 0)',
'nodeAliasPath': '@(currentDoc?.NodeAliasPath ?? "")',
'documentCulture': '@(currentDoc?.DocumentCulture ?? "")',
'publishedDate': '@(currentDoc?.DocumentPublishFrom?.ToString("yyyy-MM-dd") ?? "")'
},
'siteData': {
'siteName': '@currentSite.SiteName',
'siteID': '@currentSite.SiteID',
'siteDomain': '@currentSite.DomainName'
},
'userData': {
'authenticated': @(currentUser != null && !currentUser.IsPublic()).ToString().ToLower(),
'userID': '@(currentUser != null && !currentUser.IsPublic() ? currentUser.UserID.ToString() : "")',
'userName': '@(currentUser != null && !currentUser.IsPublic() ? currentUser.UserName : "")',
'userRoles': [@if(currentUser != null && !currentUser.IsPublic()) {
@:@string.Join(",", currentUser.UserRoles.Select(r => $"'{r.RoleName}'"))
}]
}
});
</script>
<!-- GTM Container Snippet Here -->
Portal Engine Implementation
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
var currentDoc = DocumentContext.CurrentDocument;
var currentUser = MembershipContext.AuthenticatedUser;
ltlDataLayer.Text = BuildDataLayer(currentDoc, currentUser);
}
private string BuildDataLayer(TreeNode doc, CurrentUserInfo user)
{
var isAuthenticated = user != null && !user.IsPublic();
return $@"
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({{
'pageData': {{
'pageType': '{doc?.ClassName ?? "unknown"}',
'pageName': '{doc?.DocumentName ?? ""}',
'pageID': '{doc?.DocumentID ?? 0}'
}},
'userData': {{
'authenticated': {isAuthenticated.ToString().ToLower()},
'userID': '{(isAuthenticated ? user.UserID.ToString() : "")}'
}}
}});
</script>";
}
</script>
<asp:Literal ID="ltlDataLayer" runat="server" />
Page-Specific Data Layers
Home Page
@if (ViewBag.IsHomePage)
{
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'page_loaded',
'pageCategory': 'home',
'pageType': 'landing'
});
</script>
}
Article/Blog Page
@using CMS.DocumentEngine
@{
var article = DocumentContext.CurrentDocument;
}
@if (article.ClassName == "Custom.Article")
{
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'article_view',
'article': {
'title': '@article.DocumentName',
'category': '@article.GetValue("ArticleCategory")',
'author': '@article.GetValue("ArticleAuthor")',
'publishDate': '@article.DocumentPublishFrom?.ToString("yyyy-MM-dd")',
'tags': [@string.Join(",", article.GetValue("Tags")?.ToString().Split(',').Select(t => $"'{t.Trim()}'") ?? new string[0])],
'wordCount': '@article.GetValue("WordCount")'
}
});
</script>
}
Product Page (Non-E-commerce)
@{
var product = DocumentContext.CurrentDocument;
}
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'product_view',
'product': {
'id': '@product.DocumentID',
'name': '@product.DocumentName',
'category': '@product.GetValue("ProductCategory")',
'price': '@product.GetValue("Price")',
'availability': '@product.GetValue("InStock")',
'sku': '@product.GetValue("SKU")'
}
});
</script>
Category/Listing Page
@model IEnumerable<TreeNode>
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'listing_view',
'listing': {
'category': '@ViewBag.CategoryName',
'itemCount': @Model.Count(),
'pageNumber': @ViewBag.PageNumber,
'sortBy': '@ViewBag.SortOption'
}
});
</script>
E-commerce Data Layer
Product Detail Page
@using CMS.Ecommerce
@model SKUInfo
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'view_item',
'ecommerce': {
'currency': '@CurrencyInfoProvider.GetMainCurrency(SiteContext.CurrentSiteID).CurrencyCode',
'value': @Model.SKUPrice,
'items': [{
'item_id': '@Model.SKUNumber',
'item_name': '@Model.SKUName',
'item_brand': '@(Model.Brand?.BrandDisplayName ?? "")',
'item_category': '@(Model.PrimaryCategory?.CategoryDisplayName ?? "")',
'price': @Model.SKUPrice,
'quantity': 1
}]
}
});
</script>
Add to Cart Event
<script>
function addToCartDataLayer(product, quantity) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'add_to_cart',
'ecommerce': {
'currency': product.currency,
'value': product.price * quantity,
'items': [{
'item_id': product.skuNumber,
'item_name': product.skuName,
'item_brand': product.brand,
'item_category': product.category,
'price': product.price,
'quantity': quantity
}]
}
});
}
// Usage
document.getElementById('addToCartBtn').addEventListener('click', function() {
var product = {
skuNumber: '@Model.SKUNumber',
skuName: '@Model.SKUName',
brand: '@(Model.Brand?.BrandDisplayName ?? "")',
category: '@(Model.PrimaryCategory?.CategoryDisplayName ?? "")',
price: @Model.SKUPrice,
currency: '@CurrencyInfoProvider.GetMainCurrency(SiteContext.CurrentSiteID).CurrencyCode'
};
addToCartDataLayer(product, 1);
});
</script>
Shopping Cart Page
@using CMS.Ecommerce
@{
var cart = ECommerceContext.CurrentShoppingCart;
}
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'view_cart',
'ecommerce': {
'currency': '@cart.Currency.CurrencyCode',
'value': @cart.TotalPrice,
'items': [
@foreach (var item in cart.CartItems)
{
var sku = item.SKU;
@:{
'item_id': '@sku.SKUNumber',
'item_name': '@sku.SKUName',
'item_brand': '@(sku.Brand?.BrandDisplayName ?? "")',
'item_category': '@(sku.PrimaryCategory?.CategoryDisplayName ?? "")',
'price': @item.UnitPrice,
'quantity': @item.CartItemUnits
}@(item != cart.CartItems.Last() ? "," : "")
}
]
}
});
</script>
Begin Checkout
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'begin_checkout',
'ecommerce': {
'currency': '@cart.Currency.CurrencyCode',
'value': @cart.TotalPrice,
'coupon': '@(cart.CouponCodes.FirstOrDefault()?.Code ?? "")',
'items': @Html.Raw(GetCartItemsJson())
}
});
</script>
Purchase Confirmation
@using CMS.Ecommerce
@model OrderInfo
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'purchase',
'ecommerce': {
'transaction_id': '@Model.OrderID',
'affiliation': '@SiteContext.CurrentSite.SiteName',
'value': @Model.OrderGrandTotal,
'tax': @Model.OrderTotalTax,
'shipping': @Model.OrderTotalShipping,
'currency': '@Model.OrderCurrency.CurrencyCode',
'coupon': '@(Model.OrderCouponCodes ?? "")',
'items': [
@foreach (var item in Model.OrderItems)
{
var sku = item.OrderItemSKU;
@:{
'item_id': '@sku.SKUNumber',
'item_name': '@sku.SKUName',
'item_brand': '@(sku.Brand?.BrandDisplayName ?? "")',
'item_category': '@(sku.PrimaryCategory?.CategoryDisplayName ?? "")',
'price': @item.OrderItemUnitPrice,
'quantity': @item.OrderItemUnitCount
}@(item != Model.OrderItems.Last() ? "," : "")
}
]
}
});
</script>
Form Tracking Data Layer
Form Submission
<script>
document.getElementById('contactForm').addEventListener('submit', function(e) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'form_submit',
'formData': {
'formName': 'contact_form',
'formID': 'contactForm',
'formLocation': window.location.pathname,
'formType': 'contact'
}
});
});
</script>
Form Field Interactions
<script>
// Track form starts (first field interaction)
var formStarted = false;
document.querySelectorAll('#contactForm input, #contactForm textarea').forEach(function(field) {
field.addEventListener('focus', function() {
if (!formStarted) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'form_start',
'formData': {
'formName': 'contact_form',
'formID': 'contactForm'
}
});
formStarted = true;
}
});
});
</script>
Form Errors
@if (!ViewData.ModelState.IsValid)
{
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'form_error',
'formData': {
'formName': 'contact_form',
'errorFields': [@string.Join(",", ViewData.ModelState.Where(x => x.Value.Errors.Count > 0).Select(x => $"'{x.Key}'"))],
'errorCount': @ViewData.ModelState.Values.SelectMany(v => v.Errors).Count()
}
});
</script>
}
User Interaction Events
Click Tracking
<script>
// Track CTA clicks
document.querySelectorAll('.cta-button').forEach(function(button) {
button.addEventListener('click', function(e) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'cta_click',
'eventData': {
'ctaText': this.innerText,
'ctaLocation': window.location.pathname,
'ctaDestination': this.getAttribute('href') || this.getAttribute('data-href')
}
});
});
});
</script>
Download Tracking
<script>
document.querySelectorAll('a[href$=".pdf"], a[href$=".zip"], a[href$=".doc"]').forEach(function(link) {
link.addEventListener('click', function(e) {
var fileName = this.getAttribute('href').split('/').pop();
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'file_download',
'fileData': {
'fileName': fileName,
'fileType': fileName.split('.').pop(),
'fileUrl': this.getAttribute('href'),
'linkText': this.innerText
}
});
});
});
</script>
Video Tracking
<script>
var video = document.getElementById('mainVideo');
var videoTracked = {
started: false,
progress25: false,
progress50: false,
progress75: false,
completed: false
};
video.addEventListener('play', function() {
if (!videoTracked.started) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'video_start',
'videoData': {
'videoTitle': this.getAttribute('data-title') || document.title,
'videoDuration': this.duration,
'videoUrl': this.currentSrc
}
});
videoTracked.started = true;
}
});
video.addEventListener('timeupdate', function() {
var percent = (this.currentTime / this.duration) * 100;
if (percent >= 25 && !videoTracked.progress25) {
dataLayerPush('video_progress', 25);
videoTracked.progress25 = true;
}
// Similar for 50%, 75%, 100%
});
function dataLayerPush(eventName, progress) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': eventName,
'videoData': {
'videoProgress': progress,
'videoTitle': video.getAttribute('data-title') || document.title
}
});
}
</script>
Search Tracking
@if (!string.IsNullOrEmpty(ViewBag.SearchQuery))
{
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'search',
'searchData': {
'searchTerm': '@ViewBag.SearchQuery',
'searchResults': @ViewBag.ResultCount,
'searchCategory': '@ViewBag.SearchCategory'
}
});
</script>
}
Custom Kentico Events
Content Personalization
@using CMS.OnlineMarketing
@{
var variantName = ViewBag.PersonalizationVariant;
}
@if (variantName != null)
{
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'personalization_displayed',
'personalizationData': {
'variantName': '@variantName',
'pageType': '@DocumentContext.CurrentDocument.ClassName'
}
});
</script>
}
A/B Test Variant
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'ab_test_view',
'testData': {
'testName': '@ViewBag.ABTestName',
'variantName': '@ViewBag.VariantName',
'variantID': '@ViewBag.VariantID'
}
});
</script>
Newsletter Signup
<script>
document.getElementById('newsletterForm').addEventListener('submit', function(e) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'newsletter_signup',
'newsletterData': {
'source': window.location.pathname,
'formType': 'footer_newsletter'
}
});
});
</script>
Advanced Data Layer Patterns
Data Layer Helper Function
Create a reusable helper:
<script>
window.pushToDataLayer = function(eventName, eventData) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': eventName,
...eventData
});
};
// Usage
window.pushToDataLayer('custom_event', {
'category': 'engagement',
'action': 'scroll',
'label': '75%'
});
</script>
E-commerce Data Layer Helper (C#)
public static class DataLayerHelper
{
public static string BuildEcommerceDataLayer(ShoppingCartInfo cart, string eventName)
{
var items = cart.CartItems.Select(item => new
{
item_id = item.SKU.SKUNumber,
item_name = item.SKU.SKUName,
item_brand = item.SKU.Brand?.BrandDisplayName ?? "",
item_category = item.SKU.PrimaryCategory?.CategoryDisplayName ?? "",
price = item.UnitPrice,
quantity = item.CartItemUnits
});
var dataLayer = new
{
@event = eventName,
ecommerce = new
{
currency = cart.Currency.CurrencyCode,
value = cart.TotalPrice,
items = items
}
};
return JsonConvert.SerializeObject(dataLayer);
}
}
// Usage in view:
// <script>window.dataLayer.push(@Html.Raw(DataLayerHelper.BuildEcommerceDataLayer(cart, "view_cart")));</script>
Delayed Data Layer Push
For AJAX-loaded content:
<script>
function waitForElement(selector, callback) {
if (document.querySelector(selector)) {
callback();
} else {
setTimeout(function() {
waitForElement(selector, callback);
}, 100);
}
}
waitForElement('.product-loaded', function() {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'product_ajax_loaded',
'productData': {
// product data here
}
});
});
</script>
Debugging Data Layer
Console Logging
<script>
// Log all data layer pushes
(function() {
var originalPush = window.dataLayer.push;
window.dataLayer.push = function() {
console.log('DataLayer Push:', arguments);
return originalPush.apply(window.dataLayer, arguments);
};
})();
</script>
View Data Layer in Console
// In browser console:
console.table(window.dataLayer);
// View specific event:
window.dataLayer.filter(function(item) {
return item.event === 'purchase';
});
GTM Preview Mode
- Open GTM → Preview
- Navigate to your Kentico site
- Check Data Layer tab in debugger
- Verify all values are populated correctly
Common Issues
Data Layer Not Defined
Problem: window.dataLayer is undefined
Solution:
// Always initialize before pushing
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ /* data */ });
Timing Issues
Problem: Data layer pushes before GTM loads
Solution:
<!-- Ensure data layer is before GTM snippet -->
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ /* initial data */ });
</script>
<!-- Then GTM container -->
<script>(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
Special Characters in Data
Problem: Quotes breaking JavaScript
Solution:
@{
var safeName = Html.Raw(Json.Encode(Model.ProductName));
}
<script>
window.dataLayer.push({
'productName': @safeName
});
</script>
Null Values
Problem: Null reference errors
Solution:
'brand': '@(Model.Brand?.BrandDisplayName ?? "")',
'category': '@(Model.Category?.CategoryName ?? "uncategorized")'
Best Practices
- Initialize Early: Place data layer before GTM snippet
- Use Consistent Naming: Follow a naming convention (camelCase recommended)
- Avoid PII: Don't send personal identifiable information
- Clear Event Names: Use descriptive event names
- Structured Data: Group related data in objects
- Test Thoroughly: Verify all values in GTM Preview mode
- Document Schema: Maintain documentation of your data layer structure
- Handle Nulls: Always provide fallback values
Data Layer Schema Documentation
Maintain a schema document:
/**
* Data Layer Schema for Kentico Implementation
*
* pageData: {
* pageType: string, // Kentico class name
* pageName: string, // Document name
* pageID: number, // Document ID
* nodeAliasPath: string // Node path
* }
*
* userData: {
* authenticated: boolean, // Is user logged in
* userID: string, // User ID (if authenticated)
* userRoles: array // User role names
* }
*
* ecommerce: {
* event: string, // E-commerce event name
* currency: string, // ISO currency code
* value: number, // Transaction value
* items: array // Product items
* }
*/
Next Steps
- Configure GTM Variables
- Set Up GA4 Tags in GTM
- Implement Enhanced E-commerce
- Troubleshoot Tracking Issues