SilverStripe Analytics: Template Includes, DataExtensions, | OpsBlu Docs

SilverStripe Analytics: Template Includes, DataExtensions,

Implement analytics on SilverStripe CMS. Covers .ss template script injection, DataExtension hooks, ORM-powered data layers, and SilverStripe ecommerce...

Analytics Architecture on SilverStripe

SilverStripe is a PHP CMS built on an MVC framework. Templates use the .ss format with its own syntax for variables, includes, and control structures. The ORM maps database tables to PHP classes, and every page type is a SiteTree subclass with fields accessible in templates.

Analytics scripts enter the page through three mechanisms. First, .ss template includes let you add script blocks to the <head> or <body> of your layout. Second, the Requirements PHP API (Requirements::customScript(), Requirements::javascript()) injects scripts programmatically from controllers or DataExtensions. Third, DataExtensions let you attach tracking methods to any page type without modifying the original class. The ORM provides typed, structured data (page class, URL segment, parent hierarchy) that feeds directly into data layers.

SilverStripe's template caching applies to partial templates. Inline scripts that reference $Variable syntax render at request time, so data layer values from the ORM stay current.


Installing Tracking Scripts

The primary layout template is Page.ss in your theme. Use SilverStripe's <% include %> tag to keep tracking code in a separate partial.

Main layout with GTM include:

<%-- themes/mytheme/templates/Page.ss --%>
<html>
<head>
    <% require themedCSS('style') %>
    <% include GoogleTagManager %>
    $MetaTags
</head>
<body>
    <% include GoogleTagManagerNoScript %>
    $Layout
</body>
</html>

GTM head snippet:

<%-- themes/mytheme/templates/Includes/GoogleTagManager.ss --%>
<% if $SiteConfig.GTMContainerID %>
<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','$SiteConfig.GTMContainerID');</script>
<% end_if %>

GTM noscript fallback:

<%-- themes/mytheme/templates/Includes/GoogleTagManagerNoScript.ss --%>
<% if $SiteConfig.GTMContainerID %>
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=$SiteConfig.GTMContainerID"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<% end_if %>

The $SiteConfig.GTMContainerID field is added via a SiteConfig extension:

// app/src/Extensions/SiteConfigAnalytics.php
use SilverStripe\ORM\DataExtension;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;

class SiteConfigAnalytics extends DataExtension
{
    private static $db = [
        'GTMContainerID' => 'Varchar(20)'
    ];

    public function updateCMSFields(FieldList $fields)
    {
        $fields->addFieldToTab('Root.Analytics',
            TextField::create('GTMContainerID', 'GTM Container ID')
                ->setDescription('Format: GTM-XXXXXXX')
        );
    }
}

Register it in your YAML config:

# app/_config/extensions.yml
SilverStripe\SiteConfig\SiteConfig:
  extensions:
    - SiteConfigAnalytics

Data Layer with DataExtensions

DataExtensions attach methods to page types without subclassing. Create an extension that exposes analytics data to templates.

Analytics DataExtension:

// app/src/Extensions/AnalyticsExtension.php
use SilverStripe\ORM\DataExtension;

class AnalyticsExtension extends DataExtension
{
    public function getAnalyticsData()
    {
        return json_encode([
            'page_type' => $this->owner->ClassName,
            'page_title' => $this->owner->Title,
            'page_id' => $this->owner->ID,
            'url_segment' => $this->owner->URLSegment,
            'last_edited' => $this->owner->LastEdited,
            'parent_title' => $this->owner->Parent()->exists()
                ? $this->owner->Parent()->Title
                : 'none'
        ]);
    }
}

Register the extension:

# app/_config/extensions.yml
SilverStripe\CMS\Model\SiteTree:
  extensions:
    - AnalyticsExtension

Use it in the template:

<%-- themes/mytheme/templates/Includes/AnalyticsDataLayer.ss --%>
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push($AnalyticsData);
</script>

Include this partial in Page.ss before the GTM snippet so the data layer is populated when GTM initializes:

<head>
    <% include AnalyticsDataLayer %>
    <% include GoogleTagManager %>
</head>

The $AnalyticsData variable calls getAnalyticsData() on the current page object. SilverStripe's template engine automatically calls getter methods when you reference $PropertyName.


Requirements API for Programmatic Injection

SilverStripe's Requirements class injects JavaScript and CSS from PHP controllers. Use this when tracking logic depends on controller state rather than template variables.

Injecting tracking from a controller:

// app/src/Controllers/PageController.php
use SilverStripe\View\Requirements;
use SilverStripe\CMS\Controllers\ContentController;

class PageController extends ContentController
{
    protected function init()
    {
        parent::init();

        Requirements::customScript(
            "window.dataLayer = window.dataLayer || [];
             dataLayer.push({
               'page_type': '" . addslashes($this->ClassName) . "',
               'page_id': " . (int)$this->ID . ",
               'url_segment': '" . addslashes($this->URLSegment) . "'
             });"
        );
    }
}

Key Requirements API methods for analytics:

Method Use Case
Requirements::javascript($url) Load an external analytics library
Requirements::customScript($code) Inject inline tracking JavaScript
Requirements::insertHeadTags($html) Add raw HTML to <head> (meta pixels, verification tags)
Requirements::block($file) Prevent a script from loading on specific pages

The Requirements::block() method is useful for excluding analytics on admin pages or staging environments:

if (Director::isDev()) {
    Requirements::block('themes/mytheme/javascript/analytics.js');
}

Custom Page Type Tracking

SilverStripe's page type system lets you add type-specific tracking. Each page type can define its own data layer fields through its controller.

Product page with ecommerce data layer:

// app/src/Controllers/ProductPageController.php
use SilverStripe\View\Requirements;

class ProductPageController extends PageController
{
    protected function init()
    {
        parent::init();

        $product = $this->data();
        Requirements::customScript(
            "dataLayer.push({
               'event': 'view_item',
               'ecommerce': {
                 'items': [{
                   'item_id': '" . addslashes($product->SKU) . "',
                   'item_name': '" . addslashes($product->Title) . "',
                   'price': " . (float)$product->Price . ",
                   'item_category': '" . addslashes($product->Category()->Title) . "'
                 }]
               }
             });"
        );
    }
}

The $this->data() call returns the underlying ProductPage DataObject, giving access to all custom fields defined on the page type. This pattern works for any SilverStripe page type: BlogPost, EventPage, or custom types.

For pages that use Elemental blocks (SilverStripe's content block system), inject block-level tracking through the block's controller rather than the page controller, since each block renders independently.


Common Errors

Symptom Cause Fix
GTM container ID renders as literal $SiteConfig.GTMContainerID SiteConfig extension not registered or database not rebuilt Run sake dev/build "flush=1" to rebuild the database schema
Data layer outputs empty JSON getAnalyticsData() method not found on page object Verify the DataExtension is registered in _config/*.yml and flush the config cache
Duplicate scripts on every page load Requirements::customScript() called in both init() and template include Use one injection method per script; remove the template include if using Requirements
Scripts missing on specific page types Controller does not extend PageController Ensure custom page type controllers call parent::init() which contains the tracking injection
Analytics fires on admin panel (/admin) No admin route exclusion in tracking code Add if (!$this->getRequest()->getURL() == 'admin') check before injecting scripts
$AnalyticsData renders as escaped HTML Template auto-escaping the JSON output Use $AnalyticsData.RAW in the template to prevent HTML encoding of the JSON string
SiteConfig field not appearing in CMS YAML config not flushed after adding extension Navigate to /dev/build?flush=1 in the browser or run sake dev/build "flush=1"
Page hierarchy data missing from data layer Parent() returns null for top-level pages Add an exists() check before accessing parent fields to avoid null reference errors