Kentico Xperience Analytics: Page Builder Widgets | OpsBlu Docs

Kentico Xperience Analytics: Page Builder Widgets

Implement analytics on Kentico Xperience. Covers the Page Builder widget system, custom TagHelpers for data layers, content tree page types, marketing...

Analytics Architecture on Kentico Xperience

Kentico Xperience (formerly Kentico CMS) is a .NET-based digital experience platform that uses a content tree, page types, and a Page Builder widget system. Analytics tracking integrates through four mechanisms:

  • Page Builder widgets are drag-and-drop components that editors place on pages. A dedicated analytics widget can inject scripts and data layer values without developer intervention on every page
  • Layout views (_Layout.cshtml) control the HTML shell for all pages, making them the standard location for global GTM or GA4 snippets
  • Custom TagHelpers provide reusable server-side logic that renders tracking markup based on the current page context, content type, and user state
  • Content tree page types define structured data (fields, tabs) that map directly to data layer values. Each page type has its own set of fields available for analytics extraction

Kentico Xperience uses output caching at the page level with cache dependencies on content tree nodes. When a page is cached, the HTML output including any inline data layer scripts is frozen. Dynamic data layer values require either cache variation (by user, query string) or client-side resolution after page load.

For ecommerce, Kentico Xperience includes a built-in E-commerce module with product pages, shopping cart, and checkout workflows that expose server-side events through the EcommerceEvents API.


Installing Tracking Scripts

Add GTM to the shared layout that wraps all Kentico Xperience pages:

<!-- Views/Shared/_Layout.cshtml -->
@using Kentico.Web.Mvc
<!DOCTYPE html>
<html lang="@System.Globalization.CultureInfo.CurrentUICulture.TwoLetterISOLanguageName">
<head>
  <meta charset="utf-8" />
  <page-builder-styles />

  <!-- 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','GTM-XXXXXX');</script>

  @RenderSection("head", required: false)
</head>
<body>
  <!-- GTM noscript -->
  <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXX"
  height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>

  @RenderBody()
  <page-builder-scripts />
</body>
</html>

Via Kentico Settings Key

For environments where developers should not hard-code container IDs, store the GTM ID in a Kentico Settings key and read it in the layout:

// Retrieve from Settings > Custom Settings in admin
@{
    var gtmId = CMS.DataEngine.SettingsKeyInfoProvider
        .GetValue("GTMContainerID");
}
@if (!string.IsNullOrEmpty(gtmId))
{
    <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>
}

Data Layer Setup

Custom TagHelper for Page Context

Create a TagHelper that extracts data from the current Kentico page and renders the data layer automatically:

// TagHelpers/DataLayerTagHelper.cs
using Microsoft.AspNetCore.Razor.TagHelpers;
using CMS.DocumentEngine;
using Kentico.Content.Web.Mvc;

[HtmlTargetElement("analytics-datalayer")]
public class DataLayerTagHelper : TagHelper
{
    private readonly IPageDataContextRetriever _pageContext;

    public DataLayerTagHelper(IPageDataContextRetriever pageContext)
    {
        _pageContext = pageContext;
    }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "script";

        var page = _pageContext.Retrieve<TreeNode>().Page;

        var data = new
        {
            page_title = page.DocumentName,
            page_type = page.ClassName,
            node_id = page.NodeID,
            culture = page.DocumentCulture,
            node_alias_path = page.NodeAliasPath,
            published = page.IsPublished
        };

        var json = System.Text.Json.JsonSerializer.Serialize(data);
        output.Content.SetHtmlContent(
            $"window.dataLayer=window.dataLayer||[];dataLayer.push({json});"
        );
    }
}

Use it in your layout above the GTM snippet:

<!-- Views/Shared/_Layout.cshtml -->
<analytics-datalayer />

<!-- GTM snippet follows -->
<script>(function(w,d,s,l,i){ ... })(window,document,'script','dataLayer','GTM-XXXXXX');</script>

Page Type-Specific Data

For structured page types (e.g., Article, Product), extract custom fields in the page controller and pass them to the view:

// Controllers/ArticleController.cs
using Kentico.Content.Web.Mvc;
using Kentico.Content.Web.Mvc.Routing;

[assembly: RegisterPageRoute(Article.CLASS_NAME, typeof(ArticleController))]

public class ArticleController : Controller
{
    private readonly IPageDataContextRetriever _pageContext;

    public ArticleController(IPageDataContextRetriever pageContext)
    {
        _pageContext = pageContext;
    }

    public IActionResult Index()
    {
        var article = _pageContext.Retrieve<Article>().Page;

        ViewBag.AnalyticsJson = System.Text.Json.JsonSerializer.Serialize(new
        {
            content_type = "article",
            author = article.ArticleAuthor,
            publish_date = article.DocumentPublishFrom?.ToString("yyyy-MM-dd"),
            category = article.ArticleCategory
        });

        return View(article);
    }
}
<!-- Views/Article/Index.cshtml -->
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push(@Html.Raw(ViewBag.AnalyticsJson));
</script>

Ecommerce Tracking

Kentico Xperience's E-commerce module provides server-side hooks for cart and checkout events. Track purchases using the order completion event handler:

// EventHandlers/OrderAnalyticsHandler.cs
using CMS;
using CMS.Ecommerce;
using CMS.DataEngine;

[assembly: RegisterModule(typeof(OrderAnalyticsModule))]

public class OrderAnalyticsModule : Module
{
    public OrderAnalyticsModule() : base("OrderAnalytics") { }

    protected override void OnInit()
    {
        EcommerceEvents.OrderPaid.Execute += OnOrderPaid;
    }

    private void OnOrderPaid(object sender, OrderPaidEventArgs e)
    {
        var order = e.Order;
        var items = OrderItemInfoProvider
            .GetOrderItems(order.OrderID)
            .Select(item => new
            {
                item_id = item.OrderItemSKUID.ToString(),
                item_name = item.OrderItemSKUName,
                price = item.OrderItemUnitPrice,
                quantity = item.OrderItemUnitCount
            })
            .ToList();

        var analyticsPayload = System.Text.Json.JsonSerializer.Serialize(new
        {
            transaction_id = order.OrderID.ToString(),
            value = order.OrderGrandTotal,
            currency = order.OrderCurrencyID.ToString(),
            items
        });

        // Store in session for retrieval on confirmation page
        CMS.Helpers.SessionHelper.SetValue(
            "PurchaseAnalytics", analyticsPayload);
    }
}

On the order confirmation view, retrieve and push the stored event:

<!-- Views/Checkout/Confirmation.cshtml -->
@{
    var purchaseJson = CMS.Helpers.SessionHelper
        .GetValue("PurchaseAnalytics") as string;
    CMS.Helpers.SessionHelper.Remove("PurchaseAnalytics");
}
@if (!string.IsNullOrEmpty(purchaseJson))
{
    <script>
      window.dataLayer = window.dataLayer || [];
      dataLayer.push({
        'event': 'purchase',
        'ecommerce': @Html.Raw(purchaseJson)
      });
    </script>
}

For add-to-cart tracking, subscribe to the ShoppingCartItemAdded event:

// In OnInit()
EcommerceEvents.ShoppingCartItemAdded.Execute += (sender, args) =>
{
    var item = args.Item;
    var eventData = System.Text.Json.JsonSerializer.Serialize(new
    {
        item_id = item.SKUID.ToString(),
        item_name = item.SKU.SKUName,
        price = item.SKU.SKUPrice,
        quantity = item.CartItemUnits
    });

    CMS.Helpers.SessionHelper.SetValue("AddToCartEvent", eventData);
};

Marketing Automation Event Tracking

Kentico Xperience includes a marketing automation engine. You can fire custom activities that feed into both Kentico's internal reporting and your external analytics:

// Services/CustomActivityLogger.cs
using CMS.Activities;
using CMS.Core;

public class CustomActivityLogger
{
    public void LogContentDownload(string documentName, string fileType)
    {
        var activityLogger = Service.Resolve<IActivityLogService>();

        activityLogger.Log(new ActivityInitializationData
        {
            ActivityType = "content_download",
            ActivityTitle = $"Downloaded: {documentName}",
            ActivityValue = fileType
        });
    }
}

Surface these activities in GTM by pushing a corresponding event client-side whenever the activity is logged.


Common Errors

Error Cause Fix
Data layer missing on cached pages Kentico output cache serves static HTML that includes the baked-in data layer Use cache dependencies on TreeNode or add VaryByUser/VaryByQueryString to the output cache profile
TagHelper not rendering TagHelper assembly not registered in _ViewImports.cshtml Add @addTagHelper *, YourAssemblyName to Views/_ViewImports.cshtml
Purchase event fires on every page Session value for PurchaseAnalytics not cleared after first read Call SessionHelper.Remove("PurchaseAnalytics") immediately after reading the value
E-commerce events not triggering Event handler module not registered with the [assembly: RegisterModule] attribute Verify the assembly attribute is present and the project builds without errors
GTM container differs per culture Multi-culture site uses one layout for all languages but needs different GTM IDs Read the GTM ID from a Settings key scoped to the current site culture
Page type fields return null Retrieve generic TreeNode instead of the typed page class Use _pageContext.Retrieve<YourPageType>().Page with the correct generated class
Staging server tracks production data Same GTM container ID deployed to staging and production Use Kentico Settings keys with different values per environment, or use GTM environments with separate container versions
Content tree reorder breaks analytics NodeAliasPath changes when content editors move pages in the tree Use NodeGUID or NodeID as the stable page identifier in your data layer instead of the alias path
Custom activity not appearing in reports Activity type not registered in Kentico admin Register the custom activity type under Online marketing > Activity types in the admin interface

Performance Considerations

  • Output cache dependencies: Kentico caches pages aggressively. Your analytics TagHelper adds a small overhead (under 1ms) but the cached result eliminates repeat computation. Avoid disabling cache for analytics; instead, use cache variation keys
  • Async script loading: GTM loads asynchronously by default. Do not add defer alongside the GTM snippet as it changes execution order and can delay data layer availability
  • Reduce data layer payload: Only push fields you actually use in GTM triggers and variables. Each additional field adds bytes to every page response
  • E-commerce session storage: Storing analytics payloads in the Kentico session works for low-traffic sites. For high-traffic stores, consider writing the purchase event data to a cookie or URL parameter that the confirmation page reads, avoiding session lock contention
  • Page Builder widget overhead: Each widget on a page adds a rendering cycle. If your analytics widget runs expensive queries, cache its output separately with a short expiration
  • Disable Kentico web analytics when using GA4: Kentico Xperience includes its own web analytics module that logs page views server-side. If you rely entirely on GA4 or GTM, disable the built-in module under Settings > Web analytics to save database writes on every request