PrestaShop GTM Data Layer
Implement a robust data layer structure for your PrestaShop store to enable advanced tracking with Google Tag Manager. This guide covers PrestaShop-specific implementations using hooks, Smarty templates, and the override system.
Data Layer Fundamentals
What is the Data Layer?
The data layer is a JavaScript object that stores information about the page, user, products, and interactions. GTM reads this data to fire tags with the correct information.
// Basic data layer structure
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'page_view',
'pageType': 'product',
'pageCategory': 'Electronics',
'user': {
'id': '12345',
'status': 'logged_in'
}
});
PrestaShop Context Data
PrestaShop provides rich context data that should be included in your data layer:
- Page Information: Controller name, page type, category
- User Information: Customer ID, group, login status
- Product Data: ID, name, price, category, stock status
- Cart Data: Products, quantities, totals
- Transaction Data: Order details, payment method, shipping
- Shop Context: Store ID, language, currency (multi-store)
Core Data Layer Structure for PrestaShop
Base Page Data Layer
Implement in every page:
<?php
// In your GTM module - hookDisplayHeader
public function hookDisplayHeader($params)
{
// Get current page controller
$controller = $this->context->controller;
$page_name = get_class($controller);
// Build base data layer
$data_layer = array(
'pageType' => $this->getPageType(),
'pageName' => $page_name,
'language' => $this->context->language->iso_code,
'currency' => $this->context->currency->iso_code,
'country' => $this->context->country->iso_code,
);
// Add shop context (multi-store)
if (Shop::isFeatureActive()) {
$data_layer['shop'] = array(
'id' => (int)$this->context->shop->id,
'name' => $this->context->shop->name,
'groupId' => (int)$this->context->shop->id_shop_group
);
}
// Add customer data
$data_layer['user'] = $this->getUserData();
// Add cart data
if ($this->context->cart) {
$data_layer['cart'] = $this->getCartData();
}
$this->context->smarty->assign(array(
'dataLayer' => $data_layer
));
return $this->display(__FILE__, 'views/templates/hook/datalayer-base.tpl');
}
private function getPageType()
{
$controller = Tools::getValue('controller');
$page_types = array(
'index' => 'home',
'category' => 'category',
'product' => 'product',
'cart' => 'cart',
'order' => 'checkout',
'order-confirmation' => 'purchase',
'search' => 'search',
'cms' => 'content',
'authentication' => 'login',
'my-account' => 'account'
);
return isset($page_types[$controller]) ? $page_types[$controller] : 'other';
}
private function getUserData()
{
$customer = $this->context->customer;
$user_data = array(
'status' => $customer->isLogged() ? 'logged_in' : 'guest'
);
if ($customer->isLogged()) {
$user_data['id'] = (int)$customer->id;
$user_data['email'] = $customer->email; // Use hashed version for privacy
$user_data['email_hash'] = hash('sha256', strtolower($customer->email));
$user_data['groupId'] = (int)$customer->id_default_group;
$user_data['newsletter'] = (bool)$customer->newsletter;
}
return $user_data;
}
private function getCartData()
{
$cart = $this->context->cart;
if (!$cart || !$cart->id) {
return null;
}
$products = $cart->getProducts(true);
$total = $cart->getOrderTotal(true);
return array(
'id' => (int)$cart->id,
'itemCount' => $cart->nbProducts(),
'total' => (float)$total,
'currency' => $this->context->currency->iso_code
);
}
Base Template:
{* views/templates/hook/datalayer-base.tpl *}
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'pageType': '{$dataLayer.pageType|escape:'javascript':'UTF-8'}',
'pageName': '{$dataLayer.pageName|escape:'javascript':'UTF-8'}',
'language': '{$dataLayer.language|escape:'javascript':'UTF-8'}',
'currency': '{$dataLayer.currency|escape:'javascript':'UTF-8'}',
'country': '{$dataLayer.country|escape:'javascript':'UTF-8'}',
{if isset($dataLayer.shop)}
'shop': {
'id': '{$dataLayer.shop.id|intval}',
'name': '{$dataLayer.shop.name|escape:'javascript':'UTF-8'}',
'groupId': '{$dataLayer.shop.groupId|intval}'
},
{/if}
'user': {
'status': '{$dataLayer.user.status|escape:'javascript':'UTF-8'}',
{if $dataLayer.user.status == 'logged_in'}
'id': '{$dataLayer.user.id|intval}',
'emailHash': '{$dataLayer.user.email_hash|escape:'javascript':'UTF-8'}',
'groupId': '{$dataLayer.user.groupId|intval}',
'newsletter': {if $dataLayer.user.newsletter}true{else}false{/if}
{/if}
},
{if isset($dataLayer.cart)}
'cart': {
'id': '{$dataLayer.cart.id|intval}',
'itemCount': {$dataLayer.cart.itemCount|intval},
'total': {$dataLayer.cart.total|floatval},
'currency': '{$dataLayer.cart.currency|escape:'javascript':'UTF-8'}'
}
{/if}
});
</script>
Page-Specific Data Layers
Homepage Data Layer
// Hook: displayHome
public function hookDisplayHome($params)
{
$data_layer = array(
'event' => 'homepage_view',
'pageCategory' => 'homepage'
);
$this->context->smarty->assign('homeDataLayer', $data_layer);
return $this->display(__FILE__, 'views/templates/hook/datalayer-home.tpl');
}
{* Template *}
<script>
dataLayer.push({
'event': 'homepage_view',
'pageCategory': 'homepage'
});
</script>
Category Page Data Layer
// Hook: displayFooterCategory or actionProductSearchAfter
public function hookDisplayFooterCategory($params)
{
$category = $params['category'];
$products = $this->getCurrentCategoryProducts();
// Format products for data layer
$product_items = array();
$index = 0;
foreach ($products as $product) {
$product_items[] = $this->formatProductForDataLayer($product, $index, 'category_' . $category->id);
$index++;
}
$data_layer = array(
'event' => 'view_item_list',
'ecommerce' => array(
'item_list_id' => 'category_' . $category->id,
'item_list_name' => $category->name,
'items' => $product_items
),
'category' => array(
'id' => (int)$category->id,
'name' => $category->name,
'parentId' => (int)$category->id_parent,
'level' => $category->level_depth
)
);
$this->context->smarty->assign('categoryDataLayer', json_encode($data_layer));
return $this->display(__FILE__, 'views/templates/hook/datalayer-category.tpl');
}
private function formatProductForDataLayer($product, $index = 0, $list_id = '')
{
// Get category path
$category = new Category($product['id_category_default'], $this->context->language->id);
$categories = $this->getCategoryPath($category);
// Get manufacturer/brand
$brand = '';
if (isset($product['id_manufacturer']) && $product['id_manufacturer'] > 0) {
$manufacturer = new Manufacturer($product['id_manufacturer']);
$brand = $manufacturer->name;
}
return array(
'item_id' => (string)$product['id_product'],
'item_name' => $product['name'],
'item_brand' => $brand,
'item_category' => isset($categories[0]) ? $categories[0] : '',
'item_category2' => isset($categories[1]) ? $categories[1] : '',
'item_category3' => isset($categories[2]) ? $categories[2] : '',
'item_list_id' => $list_id,
'item_list_name' => $category->name,
'price' => (float)$product['price'],
'quantity' => isset($product['quantity']) ? (int)$product['quantity'] : 1,
'index' => $index
);
}
private function getCategoryPath($category)
{
$categories = array();
$current = $category;
while ($current->id > 2) { // Stop at root category
$categories[] = $current->name;
$current = new Category($current->id_parent, $this->context->language->id);
}
return array_reverse($categories);
}
{* views/templates/hook/datalayer-category.tpl *}
<script>
dataLayer.push({$categoryDataLayer nofilter});
</script>
Product Page Data Layer
// Hook: displayFooterProduct
public function hookDisplayFooterProduct($params)
{
$product = $params['product'];
// Get selected combination
$id_product_attribute = Tools::getValue('id_product_attribute', 0);
$product_data = new Product($product->id, true, $this->context->language->id);
// Get product images
$images = $product_data->getImages($this->context->language->id);
$cover_image = '';
foreach ($images as $image) {
if ($image['cover']) {
$cover_image = $this->context->link->getImageLink(
$product_data->link_rewrite,
$image['id_image'],
'large_default'
);
break;
}
}
// Get stock
$stock_available = StockAvailable::getQuantityAvailableByProduct($product->id, $id_product_attribute);
// Format for data layer
$item = $this->formatProductForDataLayer(array(
'id_product' => $product->id,
'name' => $product_data->name,
'id_manufacturer' => $product_data->id_manufacturer,
'id_category_default' => $product_data->id_category_default,
'price' => $product_data->getPrice(true, $id_product_attribute),
'quantity' => 1
));
$data_layer = array(
'event' => 'view_item',
'ecommerce' => array(
'currency' => $this->context->currency->iso_code,
'value' => (float)$product_data->getPrice(true, $id_product_attribute),
'items' => array($item)
),
'product' => array(
'id' => (int)$product->id,
'name' => $product_data->name,
'reference' => $product_data->reference,
'ean13' => $product_data->ean13,
'upc' => $product_data->upc,
'stock' => (int)$stock_available,
'available' => $stock_available > 0,
'image' => $cover_image,
'url' => $this->context->link->getProductLink($product_data)
)
);
$this->context->smarty->assign('productDataLayer', json_encode($data_layer));
return $this->display(__FILE__, 'views/templates/hook/datalayer-product.tpl');
}
{* views/templates/hook/datalayer-product.tpl *}
<script>
dataLayer.push({$productDataLayer nofilter});
</script>
Cart Page Data Layer
// Hook: displayShoppingCart
public function hookDisplayShoppingCart($params)
{
$cart = $this->context->cart;
$products = $cart->getProducts(true);
$items = array();
foreach ($products as $product) {
$items[] = $this->formatProductForDataLayer($product);
}
$data_layer = array(
'event' => 'view_cart',
'ecommerce' => array(
'currency' => $this->context->currency->iso_code,
'value' => (float)$cart->getOrderTotal(true),
'items' => $items
)
);
$this->context->smarty->assign('cartDataLayer', json_encode($data_layer));
return $this->display(__FILE__, 'views/templates/hook/datalayer-cart.tpl');
}
Checkout Data Layer
// Hook: displayBeforeCheckout or custom checkout step
public function hookDisplayBeforeCheckout($params)
{
$cart = $this->context->cart;
$products = $cart->getProducts(true);
$items = array();
foreach ($products as $product) {
$items[] = $this->formatProductForDataLayer($product);
}
// Get checkout step
$checkout_step = $this->getCheckoutStep();
$data_layer = array(
'event' => 'begin_checkout',
'ecommerce' => array(
'currency' => $this->context->currency->iso_code,
'value' => (float)$cart->getOrderTotal(true),
'items' => $items
),
'checkout' => array(
'step' => $checkout_step,
'option' => $this->getCheckoutStepOption($checkout_step)
)
);
$this->context->smarty->assign('checkoutDataLayer', json_encode($data_layer));
return $this->display(__FILE__, 'views/templates/hook/datalayer-checkout.tpl');
}
private function getCheckoutStep()
{
$controller = Tools::getValue('controller');
// PrestaShop checkout steps vary by version and theme
// Adjust based on your checkout process
if (strpos($_SERVER['REQUEST_URI'], 'delivery') !== false) {
return 'shipping';
} elseif (strpos($_SERVER['REQUEST_URI'], 'payment') !== false) {
return 'payment';
} else {
return 'information';
}
}
Order Confirmation Data Layer (Purchase Event)
Most critical ecommerce event:
// Hook: displayOrderConfirmation
public function hookDisplayOrderConfirmation($params)
{
$order = $params['order'];
// Get order products
$order_detail = $order->getOrderDetailList();
$items = array();
foreach ($order_detail as $product) {
$items[] = $this->formatProductForDataLayer($product);
}
// Get applied cart rules (coupons/discounts)
$cart_rules = $order->getCartRules();
$coupon_codes = array();
$discount_total = 0;
foreach ($cart_rules as $rule) {
$coupon_codes[] = $rule['name'];
$discount_total += $rule['value_tax_incl'];
}
// Get shipping method
$carrier = new Carrier($order->id_carrier);
// Get payment method
$payment_module = Module::getInstanceByName($order->module);
$payment_method = $payment_module ? $payment_module->displayName : $order->payment;
$data_layer = array(
'event' => 'purchase',
'ecommerce' => array(
'transaction_id' => $order->reference,
'affiliation' => $this->context->shop->name,
'value' => (float)$order->total_paid_tax_incl,
'tax' => (float)($order->total_paid_tax_incl - $order->total_paid_tax_excl),
'shipping' => (float)$order->total_shipping_tax_incl,
'currency' => $this->context->currency->iso_code,
'coupon' => implode(',', $coupon_codes),
'items' => $items
),
'transaction' => array(
'id' => (int)$order->id,
'reference' => $order->reference,
'paymentMethod' => $payment_method,
'shippingMethod' => $carrier->name,
'discount' => (float)$discount_total
)
);
// Prevent duplicate purchase on refresh
$this->context->smarty->assign(array(
'purchaseDataLayer' => json_encode($data_layer),
'transactionId' => $order->reference
));
return $this->display(__FILE__, 'views/templates/hook/datalayer-purchase.tpl');
}
{* views/templates/hook/datalayer-purchase.tpl *}
<script>
// Check if already tracked (prevent duplicate on refresh)
var transactionId = '{$transactionId|escape:'javascript':'UTF-8'}';
var cookieName = 'purchase_tracked_' + transactionId;
function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
if (!getCookie(cookieName)) {
// Push purchase event
dataLayer.push({$purchaseDataLayer nofilter});
// Set cookie to prevent duplicate
document.cookie = cookieName + '=1; path=/; max-age=86400';
}
</script>
Dynamic Event Tracking
Add to Cart Event
Using JavaScript to capture AJAX cart additions:
// modules/customgtm/views/js/cart-tracking.js
document.addEventListener('DOMContentLoaded', function() {
// Listen for PrestaShop cart update events
prestashop.on('updateCart', function(event) {
if (event && event.reason && event.reason.linkAction === 'add-to-cart') {
// Get product data from page or event
var productData = getProductDataFromPage();
// Push add_to_cart event to data layer
dataLayer.push({
'event': 'add_to_cart',
'ecommerce': {
'currency': prestashop.currency.iso_code,
'value': parseFloat(productData.price),
'items': [{
'item_id': productData.id,
'item_name': productData.name,
'item_brand': productData.brand,
'item_category': productData.category,
'price': parseFloat(productData.price),
'quantity': parseInt(productData.quantity)
}]
}
});
}
});
// Also capture direct add to cart button clicks
var addToCartButtons = document.querySelectorAll('[data-button-action="add-to-cart"]');
addToCartButtons.forEach(function(button) {
button.addEventListener('click', function(e) {
var productData = getProductDataFromPage();
dataLayer.push({
'event': 'add_to_cart',
'ecommerce': {
'currency': prestashop.currency.iso_code,
'value': parseFloat(productData.price),
'items': [{
'item_id': productData.id,
'item_name': productData.name,
'price': parseFloat(productData.price),
'quantity': parseInt(productData.quantity)
}]
}
});
});
});
});
function getProductDataFromPage() {
return {
id: prestashop.page.id_product || document.querySelector('[data-product-id]')?.dataset.productId,
name: document.querySelector('h1.product-title')?.textContent.trim(),
brand: document.querySelector('[itemprop="brand"]')?.textContent.trim() || '',
category: document.querySelector('.breadcrumb .category')?.textContent.trim() || '',
price: document.querySelector('[data-product-price]')?.dataset.productPrice ||
document.querySelector('.product-price .current-price-value')?.textContent.replace(/[^0-9.]/g, ''),
quantity: document.querySelector('#quantity_wanted')?.value || 1
};
}
Register JavaScript in module:
// In hookDisplayHeader
$this->context->controller->addJS($this->_path . 'views/js/cart-tracking.js');
Remove from Cart Event
// Track cart item removal
document.addEventListener('click', function(e) {
if (e.target.matches('[data-link-action="delete-from-cart"]')) {
var cartItem = e.target.closest('.cart-item, .product-line-grid');
var productData = {
id: cartItem.dataset.idProduct,
name: cartItem.querySelector('.product-name')?.textContent.trim(),
price: cartItem.querySelector('.product-price')?.textContent.replace(/[^0-9.]/g, ''),
quantity: cartItem.querySelector('.product-quantity')?.textContent.trim() || 1
};
dataLayer.push({
'event': 'remove_from_cart',
'ecommerce': {
'currency': prestashop.currency.iso_code,
'value': parseFloat(productData.price) * parseInt(productData.quantity),
'items': [{
'item_id': productData.id,
'item_name': productData.name,
'price': parseFloat(productData.price),
'quantity': parseInt(productData.quantity)
}]
}
});
}
});
Product Click Tracking (select_item)
// Track product clicks in listings
document.addEventListener('click', function(e) {
var productLink = e.target.closest('.product-miniature a');
if (productLink) {
var miniature = productLink.closest('.product-miniature');
var listName = document.querySelector('.page-category-heading')?.textContent.trim() || 'product_list';
var productData = {
id: miniature.dataset.idProduct,
name: miniature.querySelector('.product-title')?.textContent.trim(),
price: miniature.querySelector('.product-price .price')?.textContent.replace(/[^0-9.]/g, ''),
index: Array.from(miniature.parentElement.children).indexOf(miniature)
};
dataLayer.push({
'event': 'select_item',
'ecommerce': {
'item_list_name': listName,
'items': [{
'item_id': productData.id,
'item_name': productData.name,
'price': parseFloat(productData.price),
'index': productData.index
}]
}
});
}
}, true);
Advanced Data Layer Features
User Registration Event
// Hook: actionCustomerAccountAdd
public function hookActionCustomerAccountAdd($params)
{
$customer = $params['newCustomer'];
$data_layer = array(
'event' => 'sign_up',
'method' => 'prestashop_registration',
'user' => array(
'id' => (int)$customer->id,
'newsletter' => (bool)$customer->newsletter
)
);
// Store in session/cookie for template retrieval
$this->context->cookie->__set('gtm_signup_event', json_encode($data_layer));
}
Search Tracking
// Hook: actionSearch
public function hookActionSearch($params)
{
$search_query = $params['search_query'];
$results_count = isset($params['total']) ? $params['total'] : 0;
$data_layer = array(
'event' => 'search',
'search_term' => $search_query,
'results_count' => $results_count
);
$this->context->smarty->assign('searchDataLayer', json_encode($data_layer));
return $this->display(__FILE__, 'views/templates/hook/datalayer-search.tpl');
}
Newsletter Signup
// Track newsletter subscriptions
var newsletterForm = document.querySelector('.block_newsletter form, #newsletter-form');
if (newsletterForm) {
newsletterForm.addEventListener('submit', function(e) {
dataLayer.push({
'event': 'newsletter_signup',
'method': 'footer_form'
});
});
}
GTM Variable Configuration
Create Variables in GTM
Data Layer Variables:
Page Type
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
pageType
User ID
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
user.id
Shop ID (Multi-store)
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
shop.id
Transaction ID
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
ecommerce.transaction_id
Ecommerce Items
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
ecommerce.items
Custom JavaScript Variables:
// Get cart item count
function() {
return window.dataLayer.find(function(item) {
return item.cart && item.cart.itemCount;
})?.cart?.itemCount || 0;
}
Testing the Data Layer
Browser Console Testing
// View entire data layer
console.log(window.dataLayer);
// Find specific events
dataLayer.filter(item => item.event === 'purchase')
// Check latest push
dataLayer[dataLayer.length - 1]
GTM Preview Mode
- Enable Preview in GTM
- Navigate through store
- Check "Data Layer" tab in debugger
- Verify all expected variables populated
- Check ecommerce object structure
Data Layer Validator
Use browser extension or create validation script:
// Validate purchase event structure
function validatePurchaseEvent(event) {
var required = ['transaction_id', 'value', 'currency', 'items'];
var missing = [];
required.forEach(function(field) {
if (!event.ecommerce[field]) {
missing.push(field);
}
});
if (missing.length > 0) {
console.error('Missing required fields:', missing);
return false;
}
return true;
}
// Test
var purchaseEvent = dataLayer.find(e => e.event === 'purchase');
if (purchaseEvent) {
validatePurchaseEvent(purchaseEvent);
}
Multi-Store Data Layer Strategy
Include shop context in all events:
// Add to all data layer pushes
if (Shop::isFeatureActive()) {
$data_layer['shop'] = array(
'id' => (int)$this->context->shop->id,
'name' => $this->context->shop->name,
'group' => (int)$this->context->shop->id_shop_group,
'url' => $this->context->shop->getBaseURL()
);
}
Create GTM variable to segment by shop:
- Variable Name: Shop ID
- Type: Data Layer Variable
- Data Layer Variable Name:
shop.id
Use in GTM triggers:
- Fire Tag X only when Shop ID equals 1
- Fire Tag Y only for Shop Group 2
Performance Considerations
Optimize Data Layer Size:
- Only include necessary data
- Avoid large nested objects
- Remove temporary data after use
Async Data Layer Pushes:
- Don't block page render
- Use
setTimeoutfor non-critical pushes - Batch related pushes
Cache Compatibility:
- Ensure dynamic data isn't cached
- Use client-side JavaScript for user-specific data
- Test with full page cache enabled
Next Steps
- GTM Setup - Install GTM container
- GA4 Ecommerce - Use data layer for GA4
- Meta Pixel - Use data layer for Facebook events
- Events Not Firing - Debug data layer issues