Joomla Data Layer for GTM | OpsBlu Docs

Joomla Data Layer for GTM

Configure and populate the GTM data layer with Joomla-specific variables for advanced tracking

The data layer is a JavaScript object that passes information from your Joomla site to Google Tag Manager. This guide covers Joomla-specific data layer implementation for enhanced tracking.

Data Layer Basics

The data layer stores variables that GTM can access:

window.dataLayer = window.dataLayer || [];
dataLayer.push({
    'variable_name': 'value',
    'another_variable': 'another_value'
});

GTM reads these variables to:

  • Fire tags conditionally
  • Pass data to analytics platforms
  • Create custom dimensions
  • Track user behavior

Basic Joomla Data Layer

Template Implementation

Add to your template's index.php before GTM container:

<?php
defined('_JEXEC') or die;

$app = JFactory::getApplication();
$doc = JFactory::getDocument();
$user = JFactory::getUser();
$input = $app->input;
$menu = $app->getMenu();
$activeMenu = $menu->getActive();

// Build data layer object
$dataLayer = [
    'pageType' => 'standard',
    'userType' => $user->guest ? 'guest' : 'logged_in',
    'language' => JFactory::getLanguage()->getTag(),
    'component' => $input->get('option', '', 'cmd'),
    'view' => $input->get('view', '', 'cmd'),
    'menuItemId' => $activeMenu ? $activeMenu->id : null,
];

// Add user ID for logged-in users
if (!$user->guest) {
    $dataLayer['userId'] = $user->id;
    $dataLayer['userRole'] = implode(',', $user->getAuthorisedGroups());
}
?>
<!DOCTYPE html>
<html>
<head>
    <!-- Data Layer -->
    <script>
        window.dataLayer = window.dataLayer || [];
        dataLayer.push(<?php echo json_encode($dataLayer); ?>);
    </script>

    <!-- Google Tag Manager -->
    <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','GTM-XXXXXXX');</script>
    <!-- End Google Tag Manager -->
</head>
<body>
    <!-- Content -->
</body>
</html>

System Plugin Implementation

Create reusable data layer plugin:

<?php
defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;

class PlgSystemDataLayer extends CMSPlugin
{
    protected $app;

    public function onBeforeCompileHead()
    {
        $doc = Factory::getDocument();

        if ($doc->getType() !== 'html') {
            return;
        }

        $dataLayer = $this->buildDataLayer();

        $dataLayerScript = "window.dataLayer = window.dataLayer || [];\ndataLayer.push(" . json_encode($dataLayer) . ");";

        $doc->addScriptDeclaration($dataLayerScript);
    }

    protected function buildDataLayer()
    {
        $app = $this->app;
        $user = Factory::getUser();
        $input = $app->input;
        $menu = $app->getMenu();
        $activeMenu = $menu->getActive();

        $dataLayer = [
            'pageType' => $this->getPageType(),
            'userType' => $user->guest ? 'guest' : 'logged_in',
            'language' => Factory::getLanguage()->getTag(),
            'component' => $input->get('option', '', 'cmd'),
            'view' => $input->get('view', '', 'cmd'),
        ];

        if (!$user->guest) {
            $dataLayer['userId'] = $user->id;
            $dataLayer['userRole'] = $this->getUserPrimaryRole($user);
        }

        if ($activeMenu) {
            $dataLayer['menuItemId'] = $activeMenu->id;
            $dataLayer['menuTitle'] = $activeMenu->title;
        }

        return $dataLayer;
    }

    protected function getPageType()
    {
        $input = $this->app->input;
        $option = $input->get('option', '', 'cmd');
        $view = $input->get('view', '', 'cmd');

        // Determine page type
        if ($option === 'com_content') {
            if ($view === 'article') {
                return 'article';
            } elseif ($view === 'category') {
                return 'category';
            }
        } elseif ($option === 'com_virtuemart') {
            if ($view === 'productdetails') {
                return 'product';
            } elseif ($view === 'category') {
                return 'category';
            } elseif ($view === 'cart') {
                return 'cart';
            }
        } elseif ($option === 'com_users' && $view === 'registration') {
            return 'registration';
        }

        return 'standard';
    }

    protected function getUserPrimaryRole($user)
    {
        $groups = $user->getAuthorisedGroups();

        // Map Joomla group IDs to readable names
        $groupMap = [
            1 => 'Public',
            2 => 'Registered',
            3 => 'Author',
            4 => 'Editor',
            5 => 'Publisher',
            6 => 'Manager',
            7 => 'Administrator',
            8 => 'Super User'
        ];

        // Return highest permission group
        foreach ([8, 7, 6, 5, 4, 3, 2] as $groupId) {
            if (in_array($groupId, $groups)) {
                return $groupMap[$groupId] ?? 'Unknown';
            }
        }

        return 'Public';
    }
}

Article/Content Data Layer

Article Detail Page

Push article-specific data to the data layer:

<?php
// In template override: /templates/your-template/html/com_content/article/default.php
defined('_JEXEC') or die;

$doc = JFactory::getDocument();
$article = $this->item;

$articleData = [
    'event' => 'articleView',
    'article' => [
        'id' => $article->id,
        'title' => $article->title,
        'author' => $article->created_by_alias ?: JFactory::getUser($article->created_by)->name,
        'category' => $article->category_title,
        'categoryId' => $article->catid,
        'publishDate' => $article->publish_up,
        'tags' => array_map(function($tag) { return $tag->title; }, $article->tags->itemTags ?? [])
    ]
];

$articleScript = "
    dataLayer.push(" . json_encode($articleData) . ");
";

$doc->addScriptDeclaration($articleScript);
?>

Category Listing Page

<?php
// In template override: /templates/your-template/html/com_content/category/default.php
defined('_JEXEC') or die;

$doc = JFactory::getDocument();
$category = $this->category;

$categoryData = [
    'event' => 'categoryView',
    'category' => [
        'id' => $category->id,
        'title' => $category->title,
        'description' => strip_tags($category->description),
        'articleCount' => count($this->items)
    ]
];

$doc->addScriptDeclaration("dataLayer.push(" . json_encode($categoryData) . ");");
?>

E-commerce Data Layer

VirtueMart Product View

<?php
// In VirtueMart product details template
defined('_JEXEC') or die;

$doc = JFactory::getDocument();
$product = $this->product;

$productData = [
    'event' => 'productView',
    'ecommerce' => [
        'detail' => [
            'products' => [[
                'id' => $product->virtuemart_product_id,
                'name' => $product->product_name,
                'price' => $product->prices['salesPrice'],
                'brand' => $product->mf_name ?? '',
                'category' => $product->category_name,
                'variant' => '',
                'currency' => $this->currency->currency_code_3
            ]]
        ]
    ]
];

$doc->addScriptDeclaration("dataLayer.push(" . json_encode($productData) . ");");
?>

VirtueMart Product List

<?php
// In VirtueMart category template
defined('_JEXEC') or die;

$doc = JFactory::getDocument();

$products = [];
$position = 1;

foreach ($this->products as $product) {
    $products[] = [
        'id' => $product->virtuemart_product_id,
        'name' => $product->product_name,
        'price' => $product->prices['salesPrice'],
        'category' => $this->category->category_name,
        'position' => $position++,
        'currency' => $this->currency->currency_code_3
    ];
}

$listData = [
    'event' => 'productImpression',
    'ecommerce' => [
        'currencyCode' => $this->currency->currency_code_3,
        'impressions' => $products
    ]
];

$doc->addScriptDeclaration("dataLayer.push(" . json_encode($listData) . ");");
?>

Add to Cart Event

<script>
document.addEventListener('DOMContentLoaded', function() {
    const addToCartForms = document.querySelectorAll('.addtocart-bar form');

    addToCartForms.forEach(function(form) {
        form.addEventListener('submit', function(e) {
            const productId = this.querySelector('input[name="virtuemart_product_id[]"]')?.value;
            const quantity = this.querySelector('input[name="quantity[]"]')?.value || 1;
            const productName = document.querySelector('.product-title')?.textContent || 'Unknown';
            const price = document.querySelector('.PricebasePriceWithTax')?.textContent.replace(/[^0-9.]/g, '') || 0;

            dataLayer.push({
                'event': 'addToCart',
                'ecommerce': {
                    'add': {
                        'products': [{
                            'id': productId,
                            'name': productName,
                            'price': parseFloat(price),
                            'quantity': parseInt(quantity)
                        }]
                    }
                }
            });
        });
    });
});
</script>

Purchase Event

<?php
// In VirtueMart thank you page
defined('_JEXEC') or die;

$doc = JFactory::getDocument();
$order = $this->orderDetails;

$session = JFactory::getSession();
$orderTracked = $session->get('order_datalayer_' . $order['details']['BT']->order_number, false);

if (!$orderTracked) {
    $products = [];
    foreach ($order['items'] as $item) {
        $products[] = [
            'id' => $item->virtuemart_product_id,
            'name' => $item->order_item_name,
            'price' => $item->product_final_price,
            'quantity' => $item->product_quantity
        ];
    }

    $purchaseData = [
        'event' => 'purchase',
        'ecommerce' => [
            'purchase' => [
                'actionField' => [
                    'id' => $order['details']['BT']->order_number,
                    'revenue' => $order['details']['BT']->order_total,
                    'tax' => $order['details']['BT']->order_tax,
                    'shipping' => $order['details']['BT']->order_shipment,
                    'currency' => $order['details']['BT']->order_currency
                ],
                'products' => $products
            ]
        ]
    ];

    $doc->addScriptDeclaration("dataLayer.push(" . json_encode($purchaseData) . ");");
    $session->set('order_datalayer_' . $order['details']['BT']->order_number, true);
}
?>

Form Submission Data Layer

Contact Form

<?php
// In com_contact template override
defined('_JEXEC') or die;

$doc = JFactory::getDocument();

$formScript = "
    document.addEventListener('DOMContentLoaded', function() {
        const contactForm = document.querySelector('.contact-form');

        if (contactForm) {
            contactForm.addEventListener('submit', function(e) {
                dataLayer.push({
                    'event': 'formSubmit',
                    'formType': 'contact',
                    'formName': 'Joomla Contact Form',
                    'contactId': '{$this->contact->id}'
                });
            });
        }
    });
";

$doc->addScriptDeclaration($formScript);
?>

RSForm Pro

<script>
document.addEventListener('DOMContentLoaded', function() {
    const rsForms = document.querySelectorAll('.rsform');

    rsForms.forEach(function(form) {
        form.addEventListener('submit', function(e) {
            const formId = form.querySelector('input[name="formId"]')?.value || 'unknown';
            const formName = form.getAttribute('data-rsform-name') || 'RSForm';

            dataLayer.push({
                'event': 'formSubmit',
                'formType': 'rsform',
                'formId': formId,
                'formName': formName
            });
        });
    });
});
</script>

User Interaction Data Layer

Login Event

<?php
defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;

class PlgSystemUserTracking extends CMSPlugin
{
    protected $app;

    public function onUserLogin($user, $options = array())
    {
        $doc = Factory::getDocument();

        if ($doc->getType() !== 'html') {
            return;
        }

        $loginScript = "
            dataLayer.push({
                'event': 'userLogin',
                'userId': '{$user['id']}',
                'loginMethod': 'joomla'
            });
        ";

        $doc->addScriptDeclaration($loginScript);
    }
}
?>

Registration Event

public function onUserAfterSave($user, $isNew, $success, $msg)
{
    if (!$isNew || !$success) {
        return;
    }

    $doc = Factory::getDocument();

    if ($doc->getType() !== 'html') {
        return;
    }

    $registrationScript = "
        dataLayer.push({
            'event': 'userRegistration',
            'userId': '{$user['id']}',
            'registrationMethod': 'joomla'
        });
    ";

    $doc->addScriptDeclaration($registrationScript);
}

Search Data Layer

<?php
// In com_search template override
defined('_JEXEC') or die;

$app = JFactory::getApplication();
$input = $app->input;
$searchword = $input->get('searchword', '', 'string');
$doc = JFactory::getDocument();

if (!empty($searchword)) {
    $searchData = [
        'event' => 'siteSearch',
        'searchTerm' => $searchword,
        'searchComponent' => 'com_search',
        'resultsCount' => count($this->results)
    ];

    $doc->addScriptDeclaration("dataLayer.push(" . json_encode($searchData) . ");");
}
?>

Smart Search (Finder)

<?php
// In com_finder template override
defined('_JEXEC') or die;

$input = JFactory::getApplication()->input;
$query = $input->get('q', '', 'string');
$doc = JFactory::getDocument();

if (!empty($query)) {
    $searchData = [
        'event' => 'siteSearch',
        'searchTerm' => $query,
        'searchComponent' => 'com_finder',
        'resultsCount' => $this->total
    ];

    $doc->addScriptDeclaration("dataLayer.push(" . json_encode($searchData) . ");");
}
?>

GTM Variable Configuration

Create Variables in GTM

1. Component Variable:

GTM → Variables → User-Defined Variables → New
Variable Type: Data Layer Variable
Data Layer Variable Name: component
Default Value: unknown
Save as: DL - Component

2. Page Type Variable:

Variable Type: Data Layer Variable
Data Layer Variable Name: pageType
Default Value: standard
Save as: DL - Page Type

3. User Type Variable:

Variable Type: Data Layer Variable
Data Layer Variable Name: userType
Default Value: guest
Save as: DL - User Type

4. Article ID Variable:

Variable Type: Data Layer Variable
Data Layer Variable Name: article.id
Save as: DL - Article ID

5. E-commerce Product ID:

Variable Type: Data Layer Variable
Data Layer Variable Name: ecommerce.detail.products.0.id
Save as: DL - Product ID

Testing Data Layer

Browser Console

// View entire data layer
console.log(window.dataLayer);

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

// Search for specific event
window.dataLayer.filter(item => item.event === 'productView');

GTM Preview Mode

1. GTM → Preview
2. Enter Joomla site URL
3. Click Connect
4. Browse site
5. Check "Data Layer" tab in debug panel
6. Verify variables populate correctly

Tag Assistant

1. Install Tag Assistant extension
2. Connect to site
3. View data layer values
4. Verify events fire with correct data

Common Data Layer Issues

Data layer empty:

  • Check data layer code runs before GTM container
  • Verify JavaScript has no errors (check console)
  • Ensure JSON encoding is valid

Variables not available in GTM:

  • Check variable names match exactly (case-sensitive)
  • Verify data layer pushes before GTM loads
  • Check for typos in variable names

User-specific data cached:

  • Exclude data layer from page caching for logged-in users
  • Use session storage for dynamic values
  • Consider server-side data layer generation

E-commerce events missing:

  • Verify template overrides are in correct location
  • Check session deduplication isn't blocking events
  • Ensure product data is available in template context

Best Practices

1. Consistent Naming:

  • Use camelCase for variable names
  • Prefix custom events: joomla_eventName
  • Use descriptive names: articleId not aid

2. Data Types:

  • Numbers as integers/floats (not strings)
  • Booleans as true/false (not "true"/"false")
  • Arrays for multiple values

3. Security:

  • Don't include sensitive data (passwords, credit cards)
  • Hash or pseudonymize user IDs if needed
  • Sanitize user input before adding to data layer

4. Performance:

  • Push data layer before GTM container
  • Avoid pushing on every DOM change
  • Batch multiple variables in single push

Next Steps