MODX Analytics Implementation Guide | OpsBlu Docs

MODX Analytics Implementation Guide

Install tracking scripts, build data layers, and debug analytics on MODX Revolution using chunks, snippets, plugins, and system events.

Analytics Architecture on MODX

MODX Revolution separates content from presentation through a layered system of templates, chunks, snippets, and plugins. Analytics implementation leverages this architecture at multiple levels:

  • Templates define page structure and include chunk references for tracking scripts
  • Chunks store reusable HTML/JS fragments (tracking code lives here)
  • Snippets execute PHP logic and return output (dynamic data layer generation)
  • Plugins attach to system events for server-side tracking hooks
  • Template Variables (TVs) store per-resource metadata exposed to analytics

The MODX tag syntax [[*field]] and [[+placeholder]] provides direct access to resource fields and snippet output within templates, making data layer population straightforward without additional PHP.


Installing Tracking Scripts

Create a chunk called analyticsHead containing your tracking scripts:

<!-- analyticsHead chunk -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXX');
</script>

Reference it in your template before </head>:

<head>
  <title>[[*pagetitle]]</title>
  [[$analyticsHead]]
</head>

This approach lets you update tracking code in one chunk and have it propagate across all templates that reference it.

Method 2: Plugin on System Event

For scripts that need server-side logic or conditional loading, create a plugin bound to the OnWebPagePrerender event:

<?php
// Plugin: AnalyticsInjector
// System Events: OnWebPagePrerender

$output = &$modx->resource->_output;

// Skip for admin previews or specific contexts
if ($modx->context->get('key') === 'mgr') return;

$trackingCode = '
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag("js", new Date());
  gtag("config", "G-XXXXXXX");
</script>';

$output = str_replace('</head>', $trackingCode . '</head>', $output);

Method 3: OnLoadWebDocument for Early Injection

Use OnLoadWebDocument when you need to modify resource properties before rendering:

<?php
// Plugin: EarlyAnalytics
// System Events: OnLoadWebDocument

$resource = &$modx->resource;
$templateId = $resource->get('template');

// Only inject on specific templates
if (in_array($templateId, [1, 3, 5])) {
    $modx->setPlaceholder('analytics_enabled', true);
}

Then in your template:

[[!If? &subject=`[[+analytics_enabled]]` &operator=`=` &operand=`1` &then=`[[$analyticsHead]]`]]

Data Layer Implementation

Using Snippets for Dynamic Data

Create a snippet called buildDataLayer that assembles page metadata:

<?php
// Snippet: buildDataLayer

$resource = $modx->resource;

$data = [
    'page_type' => $resource->get('class_key'),
    'page_id' => $resource->get('id'),
    'page_title' => $resource->get('pagetitle'),
    'template' => $resource->get('template'),
    'parent_id' => $resource->get('parent'),
    'published' => $resource->get('publishedon'),
    'content_type' => $resource->get('content_type'),
];

// Pull Template Variable values
$tvValues = ['product_category', 'page_section', 'author_name'];
foreach ($tvValues as $tvName) {
    $tv = $modx->getObject('modTemplateVar', ['name' => $tvName]);
    if ($tv) {
        $data[$tvName] = $tv->getValue($resource->get('id'));
    }
}

// Build context from resource groups
$groups = $resource->getResourceGroupNames();
if (!empty($groups)) {
    $data['content_groups'] = $groups;
}

return '<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(' . json_encode($data, JSON_UNESCAPED_SLASHES) . ');
</script>';

Call it in your template before the GTM container:

[[!buildDataLayer]]
[[$gtmContainer]]

Using MODX Tags Directly

For simpler data layers, use MODX resource tags inline:

<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  'page_title': '[[*pagetitle]]',
  'page_id': '[[*id]]',
  'parent_id': '[[*parent]]',
  'template_id': '[[*template]]',
  'longtitle': '[[*longtitle]]',
  'description': '[[*description]]',
  'alias': '[[*alias]]',
  'published_on': '[[*publishedon]]',
  'product_sku': '[[*productSKU]]',
  'page_section': '[[*pageSection]]'
});
</script>

Template variable values are accessed with [[*tvName]] just like built-in resource fields.

User Context Data

Expose logged-in user data via a snippet:

<?php
// Snippet: userDataLayer

if (!$modx->user->hasSessionContext($modx->context->get('key'))) {
    return '';
}

$profile = $modx->user->getOne('Profile');
$data = [
    'user_logged_in' => true,
    'user_id' => $modx->user->get('id'),
    'user_groups' => array_values($modx->user->getUserGroupNames()),
];

return '<script>window.dataLayer.push(' . json_encode($data) . ');</script>';

Common Issues

Cached vs Uncached Tags

MODX caches output by default. Analytics snippets that depend on user state or request data must use uncached syntax:

<!-- Cached (wrong for user-specific data) -->
[[buildDataLayer]]

<!-- Uncached (correct) -->
[[!buildDataLayer]]

Chunks containing only static tracking scripts can remain cached ([[$analyticsHead]]). Snippets generating per-request data must be uncached ([[!snippetName]]).

Plugin Execution Order

When multiple plugins bind to the same system event, execution order matters. Set priority in the plugin properties:

  • OnWebPagePrerender fires after template rendering but before output. Ideal for modifying final HTML.
  • OnLoadWebDocument fires when the resource is loaded. Ideal for setting placeholders consumed by templates.
  • OnPageNotFound fires on 404 responses. Use this for 404 tracking.

If your analytics plugin modifies $modx->resource->_output and another plugin does the same, the last plugin to execute wins. Check the Events tab in each plugin to verify execution order.

Context-Aware Tracking

MODX supports multiple contexts (e.g., web, mobile, staging). Ensure tracking code respects context boundaries:

<?php
// In a plugin or snippet
$ctx = $modx->context->get('key');

if ($ctx === 'staging') {
    // Use debug/staging GA property
    $propertyId = 'G-STAGING';
} else {
    $propertyId = 'G-PRODUCTION';
}

Friendly URLs and Virtual Pageviews

MODX friendly URLs rewrite /index.php?id=5 to /about/. Verify that your analytics tool receives the friendly URL, not the query-string version. Check [[~[[*id]]]] output in your data layer to confirm the resolved URL.

For AJAX-loaded content or tabbed interfaces, fire virtual pageviews manually:

// After AJAX content load
gtag('event', 'page_view', {
  page_path: '/products/detail-tab',
  page_title: 'Product Detail Tab'
});

Platform-Specific Considerations

System Events for Analytics Hooks: MODX exposes 100+ system events. Key ones for analytics:

  • OnWebPagePrerender -- modify final HTML output (script injection)
  • OnLoadWebDocument -- set placeholders before template rendering
  • OnPageNotFound -- track 404 errors
  • OnHandleRequest -- earliest hook for request-level tracking
  • OnWebLogin / OnWebLogout -- user authentication tracking

Template Variable Performance: Accessing TVs via $tv->getValue() in a snippet runs a database query per TV. For pages with many TVs, use $modx->getCollection('modTemplateVarResource', ['contentid' => $resourceId]) to batch-fetch values.

MIGX and Custom TVs: If using MIGX (multi-item grid) TVs for repeating data like product variants, parse the JSON value in your data layer snippet:

$migxData = json_decode($modx->resource->getTVValue('product_variants'), true);
$data['variant_count'] = is_array($migxData) ? count($migxData) : 0;

Multi-Context Sites: For MODX sites serving multiple domains or languages through contexts, ensure each context has its own analytics property or uses a single property with context-based custom dimensions.

Static Elements vs Database Elements: MODX can load chunks, snippets, and plugins from static files (core/components/). If your analytics elements are stored as static files, changes require cache clearing (Site > Clear Cache) to take effect.