Data Layer Implementation for Kentico | OpsBlu Docs

Data Layer Implementation for Kentico

Build robust data layer implementations for Google Tag Manager on Kentico Xperience websites

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

  1. Open GTM → Preview
  2. Navigate to your Kentico site
  3. Check Data Layer tab in debugger
  4. 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

  1. Initialize Early: Place data layer before GTM snippet
  2. Use Consistent Naming: Follow a naming convention (camelCase recommended)
  3. Avoid PII: Don't send personal identifiable information
  4. Clear Event Names: Use descriptive event names
  5. Structured Data: Group related data in objects
  6. Test Thoroughly: Verify all values in GTM Preview mode
  7. Document Schema: Maintain documentation of your data layer structure
  8. 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

Additional Resources