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 |
Related Guides
- Google Analytics Setup -- GA4 measurement ID and config tag placement
- GTM Setup -- Container installation and trigger configuration
- Data Layer Implementation -- ORM-powered data layer variables for GTM
- Troubleshooting -- Performance and tracking issue resolution