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
- MODX generates page with resource data
- Data layer populates with MODX placeholders
- GTM reads data from window.dataLayer
- Tags fire with MODX data as parameters
- 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:
- GTM → Variables → New
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
resourceId - 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:
- Variable Type: Lookup Table
- Input Variable:
\{\{DLV - Template\}\} - Mappings:
1→Homepage2→Article3→Product Page4→Contact Form
- 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
- Enable Preview in GTM
- Navigate to MODX site
- Click Data Layer tab in Tag Assistant
- Inspect each data layer push
- 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
- GTM Setup - Install GTM on MODX
- GA4 Event Tracking - Use data layer for GA4
- Events Not Firing - Debug tracking issues
For general data layer concepts, see Data Layer Guide.