MODX Data Layer Structure for GTM | OpsBlu Docs

MODX Data Layer Structure for GTM

Complete reference for implementing a MODX data layer with Google Tag Manager including resource data, user information, and custom events.

Implement a comprehensive data layer on your MODX site to pass resource data, user information, and custom events to Google Tag Manager.

Data Layer Overview

The data layer is a JavaScript object that holds all the information you want to pass from MODX to GTM. GTM uses this data to:

  • Trigger tags based on conditions
  • Pass dynamic values to analytics platforms
  • Create custom variables and reports

How MODX Data Layer Works

  1. MODX generates page with resource data
  2. Data layer populates with MODX placeholders
  3. GTM reads data from window.dataLayer
  4. Tags fire with MODX data as parameters
  5. Analytics platforms receive enriched data

Base Data Layer Implementation

Template-Based Data Layer

Add to your MODX template before GTM code:

<!-- MODX Data Layer -->
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    // Page Information
    'event': 'page_view',
    'pageType': '[[*template:is=`1`:then=`home`:else=`[[*template:is=`2`:then=`content`:else=`other`]]`]]',
    'resourceId': '[[*id]]',
    'pageTitle': '[[*pagetitle]]',
    'pageUri': '[[*uri]]',
    'pageUrl': '[[*uri:fullurl]]',

    // Resource Metadata
    'template': '[[*template]]',
    'templateName': '[[*template:select=`templatename`]]',
    'parentId': '[[*parent]]',
    'parentTitle': '[[*parent:is=`0`:then=`No Parent`:else=`[[*parent:select=`pagetitle`]]`]]',
    'published': [[*published]],
    'publishedOn': '[[*publishedon:strtotime:date=`%Y-%m-%d`]]',
    'context': '[[!++context_key]]',

    // Site Information
    'siteName': '[[++site_name]]',
    'siteUrl': '[[++site_url]]',
    'language': '[[++cultureKey]]',

    // User Information
    'userLoggedIn': [[!+modx.user.id:notempty=`true`:default=`false`]],
    [[!+modx.user.id:notempty=`
    'userId': '[[!+modx.user.id]]',
    'username': '[[!+modx.user.username]]',
    `]]
  });
</script>

Plugin-Based Data Layer

Create a plugin to automatically generate data layer:

<?php
/**
 * MODX Data Layer Plugin
 * Generates data layer for GTM
 *
 * System Events: OnWebPagePrerender
 */

$resource = $modx->resource;
$user = $modx->user;

// Build data layer array
$dataLayer = [
    'event' => 'page_view',
    'pageType' => ($resource->get('template') == 1) ? 'home' : 'content',
    'resourceId' => $resource->get('id'),
    'pageTitle' => $resource->get('pagetitle'),
    'pageUri' => $resource->get('uri'),
    'template' => $resource->get('template'),
    'parentId' => $resource->get('parent'),
    'published' => (bool)$resource->get('published'),
    'context' => $modx->context->get('key'),
    'siteName' => $modx->getOption('site_name'),
    'language' => $modx->getOption('cultureKey'),
    'userLoggedIn' => $user->isAuthenticated('web')
];

// Add user data if logged in
if ($user->isAuthenticated('web')) {
    $dataLayer['userId'] = $user->get('id');
    $dataLayer['username'] = $user->get('username');
}

// Add template name
$template = $modx->getObject('modTemplate', $resource->get('template'));
if ($template) {
    $dataLayer['templateName'] = $template->get('templatename');
}

// Add parent title
if ($resource->get('parent') > 0) {
    $parent = $modx->getObject('modResource', $resource->get('parent'));
    if ($parent) {
        $dataLayer['parentTitle'] = $parent->get('pagetitle');
    }
}

// Convert to JSON
$dataLayerJson = json_encode($dataLayer, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

$dataLayerCode = <<<HTML
<!-- MODX Data Layer -->
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({$dataLayerJson});
</script>
HTML;

// Inject before GTM code (or </head> if GTM not present)
$output = $modx->resource->_output;
$output = str_replace('</head>', $dataLayerCode . '</head>', $output);
$modx->resource->_output = $output;

Resource-Specific Data

Template Variables (TVs)

Include custom Template Variables in data layer:

<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    'event': 'page_view',
    'resourceId': '[[*id]]',

    // Product TVs (for e-commerce sites)
    'productPrice': [[*product_price:default=`0`]],
    'productCategory': '[[*product_category:default=`Uncategorized`]]',
    'productSku': '[[*product_sku]]',
    'inStock': [[*in_stock:is=`1`:then=`true`:else=`false`]],

    // Content TVs
    'author': '[[*author]]',
    'contentType': '[[*content_type:default=`article`]]',
    'tags': '[[*tags]]',

    // Custom TVs
    'customField1': '[[*custom_field_1]]',
    'customField2': '[[*custom_field_2]]'
  });
</script>

Dynamic Content Type Detection

Determine content type based on template:

<script>
  var contentType = 'page';

  [[*template:is=`1`:then=`contentType = 'home';`]]
  [[*template:is=`2`:then=`contentType = 'article';`]]
  [[*template:is=`3`:then=`contentType = 'product';`]]
  [[*template:is=`4`:then=`contentType = 'contact';`]]

  dataLayer.push({
    'event': 'page_view',
    'contentType': contentType,
    'resourceId': '[[*id]]'
  });
</script>

User Data

Logged-in User Information

<script>
  window.dataLayer = window.dataLayer || [];

  [[!+modx.user.id:notempty=`
  dataLayer.push({
    'event': 'user_data',
    'userId': '[[!+modx.user.id]]',
    'username': '[[!+modx.user.username]]',
    'userEmail': '[[!+modx.user.email]]',
    'userRole': '[[!+modx.user.role]]',
    'memberSince': '[[!+modx.user.createdon:strtotime:date=`%Y-%m-%d`]]'
  });
  `]]
</script>

Privacy Note: Hash or encrypt sensitive data before sending to GTM:

// In plugin
if ($user->isAuthenticated('web')) {
    $dataLayer['userIdHashed'] = hash('sha256', $user->get('id'));
    $dataLayer['emailHashed'] = hash('sha256', strtolower($user->get('email')));
}

User Groups

<script>
  [[!+modx.user.id:notempty=`
  dataLayer.push({
    'userGroups': '[[!+modx.user.usergroups:implode=`,`]]'
  });
  `]]
</script>

Custom Events

Form Submission Events

FormIt Integration

<!-- In FormIt call -->
[[!FormIt?
  &hooks=`email,FormItSaveForm,gtmFormHook`
  &emailTo=`info@example.com`
]]

<!-- Form HTML -->
<form id="contact-form" action="[[~[[*id]]]]" method="post">
  [[!+fi.error_message]]

  <input type="text" name="name" value="[[!+fi.name]]" placeholder="Name">
  <input type="email" name="email" value="[[!+fi.email]]" placeholder="Email">
  <textarea name="message" placeholder="Message">[[!+fi.message]]</textarea>

  <button type="submit">Submit</button>
</form>

<!-- Push event on success -->
[[!+fi.successMessage:notempty=`
<script>
  dataLayer.push({
    'event': 'form_submit',
    'formName': 'contact_form',
    'formId': 'contact-form',
    'resourceId': '[[*id]]',
    'formSuccess': true
  });
</script>
`]]

Custom FormIt Hook Plugin

<?php
/**
 * GTM Form Hook
 * Custom FormIt hook to push form data to data layer
 */

$formName = $hook->getValue('form_name') ?: 'unknown_form';
$formFields = $hook->getValues();

// Store in session to push on thank you page
$_SESSION['gtm_form_submission'] = [
    'event' => 'form_submit',
    'formName' => $formName,
    'formId' => $formFields['form_id'] ?? 'contact',
    'resourceId' => $modx->resource->get('id'),
    'timestamp' => time()
];

return true;

Download Events

Track file downloads:

<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Track PDF and other downloads
    document.querySelectorAll('a[href$=".pdf"], a[href$=".zip"], a[href$=".doc"]').forEach(function(link) {
      link.addEventListener('click', function(e) {
        const fileUrl = this.href;
        const fileName = fileUrl.split('/').pop();
        const fileExtension = fileName.split('.').pop();

        dataLayer.push({
          'event': 'file_download',
          'fileName': fileName,
          'fileExtension': fileExtension,
          'fileUrl': fileUrl,
          'resourceId': '[[*id]]',
          'linkText': this.textContent
        });
      });
    });
  });
</script>

Search Events

Track SimpleSearch or custom search:

[[!SimpleSearch?
  &landing=`[[*id]]`
  &searchIndex=`search`
]]

<form action="[[~[[*id]]]]" method="get">
  <input type="text" name="search" value="[[!+search]]" placeholder="Search...">
  <button type="submit">Search</button>
</form>

<!-- Push search event -->
[[!+search:notempty=`
<script>
  dataLayer.push({
    'event': 'search',
    'searchTerm': '[[!+search]]',
    'searchResults': [[!+total:default=`0`]],
    'resourceId': '[[*id]]'
  });
</script>
`]]

Resource Action Events

Track when users create/edit resources (Manager context):

<?php
/**
 * Resource Action Tracking Plugin
 * Events: OnDocFormSave
 */

if ($modx->context->key !== 'mgr') return;

$resource = $modx->event->params['resource'];
$mode = $modx->event->params['mode'];

$action = ($mode == modSystemEvent::MODE_NEW) ? 'resource_created' : 'resource_updated';

// Store in user session to track on next page load
$_SESSION['modx_resource_action'] = [
    'event' => $action,
    'resourceId' => $resource->id,
    'resourceTitle' => $resource->pagetitle,
    'template' => $resource->template,
    'userId' => $modx->user->get('id')
];

E-Commerce Data Layer

For sites using MiniShop2, SimpleCart, or custom e-commerce:

Product Page Data

<script>
  dataLayer.push({
    'event': 'view_item',
    'ecommerce': {
      'currency': '[[++minishop2.currency:default=`USD`]]',
      'value': [[*price:default=`0`]],
      'items': [{
        'item_id': '[[*id]]',
        'item_name': '[[*pagetitle]]',
        'item_category': '[[*parent:select=`pagetitle`]]',
        'price': [[*price:default=`0`]],
        'item_brand': '[[*brand]]',
        'item_variant': '[[*variant]]',
        'quantity': 1
      }]
    }
  });
</script>

Add to Cart Event

<script>
  // Listen for MiniShop2 add to cart event
  document.addEventListener('msminicart:add', function(e) {
    dataLayer.push({
      'event': 'add_to_cart',
      'ecommerce': {
        'currency': '[[++minishop2.currency]]',
        'value': e.detail.price * e.detail.count,
        'items': [{
          'item_id': e.detail.id,
          'item_name': e.detail.name,
          'price': e.detail.price,
          'quantity': e.detail.count
        }]
      }
    });
  });
</script>

Purchase Event

<!-- Order confirmation template -->
[[!msOrder? &to=`orderConfirmation.tpl`]]

<!-- In orderConfirmation.tpl chunk -->
<script>
  dataLayer.push({
    'event': 'purchase',
    'ecommerce': {
      'transaction_id': '[[+num]]',
      'value': [[+cost]],
      'currency': '[[++minishop2.currency]]',
      'tax': [[+tax:default=`0`]],
      'shipping': [[+shipping:default=`0`]],
      'items': [
        [[+products]]
      ]
    }
  });
</script>

Creating GTM Variables

Data Layer Variables (Simple Values)

For simple values in the data layer:

  1. GTMVariablesNew
  2. Variable Type: Data Layer Variable
  3. Data Layer Variable Name: resourceId
  4. Name: DLV - Resource ID

Common MODX Variables to Create:

Variable Name Data Layer Path GTM Name
Resource ID resourceId DLV - Resource ID
Page Title pageTitle DLV - Page Title
Template template DLV - Template
Template Name templateName DLV - Template Name
Parent ID parentId DLV - Parent ID
Context context DLV - Context
User ID userId DLV - User ID
User Logged In userLoggedIn DLV - User Logged In

Custom JavaScript Variables

For complex data transformations:

// Variable: Get Template Name from ID
function() {
  var templateMap = {
    '1': 'Home',
    '2': 'Content',
    '3': 'Product',
    '4': 'Contact'
  };

  var templateId = {{DLV - Template}};
  return templateMap[templateId] || 'Unknown';
}
// Variable: Format Publish Date
function() {
  var publishDate = {{DLV - Published On}};
  if (!publishDate) return null;

  var date = new Date(publishDate);
  return date.toISOString().split('T')[0]; // YYYY-MM-DD
}

Lookup Tables

Map MODX values to custom values:

  1. Variable Type: Lookup Table
  2. Input Variable: \{\{DLV - Template\}\}
  3. Mappings:
    • 1Homepage
    • 2Article
    • 3Product Page
    • 4Contact Form
  4. Default Value: Other

GTM Triggers

Page-Specific Triggers

Homepage Only:

  • Type: Page View - DOM Ready
  • Condition: resourceId equals 1

Specific Template:

  • Type: Page View - DOM Ready
  • Condition: template equals 3

Parent-Based:

  • Type: Page View - DOM Ready
  • Condition: parentId equals 5

Event-Based Triggers

Form Submission:

  • Type: Custom Event
  • Event name: form_submit
  • Fires on: All Custom Events

File Download:

  • Type: Custom Event
  • Event name: file_download
  • Fires on: All Custom Events

Search:

  • Type: Custom Event
  • Event name: search
  • Fires on: All Custom Events
  • Condition: searchTerm does not equal undefined

User-Based Triggers

Logged In Users Only:

  • Type: Page View
  • Condition: userLoggedIn equals true

Specific User Group:

  • Type: Page View
  • Condition: userGroups contains Administrator

Debugging Data Layer

Browser Console

View entire data layer:

console.table(window.dataLayer);

Find specific event:

window.dataLayer.filter(obj => obj.event === 'form_submit');

Monitor new pushes:

const originalPush = window.dataLayer.push;
window.dataLayer.push = function() {
  console.log('Data Layer Push:', arguments[0]);
  originalPush.apply(window.dataLayer, arguments);
};

GTM Preview Mode

  1. Enable Preview in GTM
  2. Navigate to MODX site
  3. Click Data Layer tab in Tag Assistant
  4. Inspect each data layer push
  5. Verify MODX values populate correctly

Common Debugging Commands

// Check if data layer exists
console.log(window.dataLayer);

// Get latest data layer push
console.log(window.dataLayer[window.dataLayer.length - 1]);

// Get specific value
const resourceId = window.dataLayer.find(obj => obj.resourceId)?.resourceId;
console.log('Resource ID:', resourceId);

Best Practices

1. Initialize Data Layer Before GTM

<!-- CORRECT ORDER -->
<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({...MODX data...});
</script>
<!-- Then GTM code -->
<script>(function(w,d,s,l,i){...GTM code...})</script>

2. Use Consistent Naming

Follow naming conventions:

  • camelCase for variable names
  • Descriptive, clear names
  • Avoid abbreviations unless obvious

3. Handle Missing Data

// Always provide defaults for MODX placeholders
[[*tv_value:default=`Not Set`]]
[[*price:default=`0`]]
[[*template:gt=`0`:then=`[[*template]]`:else=`1`]]

4. Sanitize Data

// In plugin - escape for JavaScript
$pageTitle = addslashes($resource->get('pagetitle'));

// Or use JSON encoding
$dataLayerJson = json_encode($dataLayer, JSON_UNESCAPED_SLASHES);

5. Don't Overload Data Layer

Only include data you'll actually use:

  • Essential page metadata
  • User information (if needed)
  • Event-specific data
  • Custom tracking parameters

Avoid:

  • Entire resource object
  • Unused template variables
  • Redundant information

Troubleshooting

Data Layer is Empty

Check:

  • Data layer code appears before GTM
  • No JavaScript errors in console
  • MODX cache is cleared
  • Template/plugin is properly saved

Variables Return Undefined

Check:

  • Variable path matches data layer exactly (case-sensitive)
  • Event has fired before variable accessed
  • Data exists on current page/resource

MODX Placeholders Not Parsing

Check:

  • Proper MODX tag syntax: [[*id]] not {*id}
  • Tags are not in cached chunk (use [[!uncached]] if needed)
  • Template is saved and cache cleared

Events Fire Multiple Times

Cause: Data layer push in multiple locations or event listeners attached multiple times.

Fix:

  • Check template, plugins, and chunks for duplicate code
  • Use browser console to monitor pushes
  • Ensure events fire only once per action

Next Steps

For general data layer concepts, see Data Layer Guide.