Google Tag Manager on Commercetools
This guide covers implementing Google Tag Manager (GTM) on Commercetools headless storefronts. GTM provides centralized tag management for all your marketing and analytics tools.
Prerequisites
Create a GTM Container
- Sign in to tagmanager.google.com
- Create a new container (Web)
- Copy your Container ID (format:
GTM-XXXXXXX)
Frontend Application Ready
- Commercetools SDK configured
- E-commerce flows implemented
Installation by Framework
Next.js / React
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID;
return (
<html lang="en">
<head>
<Script id="gtm-script" strategy="afterInteractive">
{`
(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_ID}');
`}
</Script>
</head>
<body>
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
height="0"
width="0"
style={{ display: 'none', visibility: 'hidden' }}
/>
</noscript>
{children}
</body>
</html>
);
}
Nuxt / Vue
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
script: [
{
children: `(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','${process.env.GTM_ID}');`
}
],
noscript: [
{
children: `<iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.GTM_ID}"
height="0" width="0" style="display:none;visibility:hidden"></iframe>`
}
]
}
}
});
Data Layer Implementation
Create Data Layer Hook
// hooks/useDataLayer.ts
declare global {
interface Window {
dataLayer: any[];
}
}
export function useDataLayer() {
const push = (data: Record<string, any>) => {
if (typeof window !== 'undefined') {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(data);
}
};
const pushEcommerce = (eventName: string, ecommerceData: any) => {
push({ ecommerce: null }); // Clear previous ecommerce data
push({
event: eventName,
ecommerce: ecommerceData
});
};
return { push, pushEcommerce };
}
E-commerce Data Layer Events
// utils/commercetoolsToDataLayer.ts
import { Cart, LineItem, Order, ProductProjection } from '@commercetools/platform-sdk';
export function formatProductForDataLayer(product: ProductProjection) {
const variant = product.masterVariant;
const price = variant.prices?.[0];
return {
item_id: product.id,
item_name: product.name['en-US'],
item_brand: variant.attributes?.find(a => a.name === 'brand')?.value,
item_category: product.categories?.[0]?.obj?.name?.['en-US'],
item_variant: variant.sku,
price: price ? price.value.centAmount / 100 : 0,
currency: price?.value.currencyCode || 'USD'
};
}
export function formatCartForDataLayer(cart: Cart) {
return {
currency: cart.totalPrice.currencyCode,
value: cart.totalPrice.centAmount / 100,
items: cart.lineItems.map((item, index) => ({
item_id: item.productId,
item_name: item.name['en-US'],
item_variant: item.variant.sku,
price: item.price.value.centAmount / 100,
quantity: item.quantity,
index: index
}))
};
}
export function formatOrderForDataLayer(order: Order) {
return {
transaction_id: order.orderNumber || order.id,
value: order.totalPrice.centAmount / 100,
tax: order.taxedPrice?.totalTax?.centAmount
? order.taxedPrice.totalTax.centAmount / 100
: 0,
shipping: order.shippingInfo?.price?.centAmount
? order.shippingInfo.price.centAmount / 100
: 0,
currency: order.totalPrice.currencyCode,
items: order.lineItems.map((item, index) => ({
item_id: item.productId,
item_name: item.name['en-US'],
item_variant: item.variant.sku,
price: item.price.value.centAmount / 100,
quantity: item.quantity,
index: index
}))
};
}
Implementation Examples
// Product List Page
import { useDataLayer } from '@/hooks/useDataLayer';
import { formatProductForDataLayer } from '@/utils/commercetoolsToDataLayer';
export function ProductListPage({ products, categoryName }: Props) {
const { pushEcommerce } = useDataLayer();
useEffect(() => {
pushEcommerce('view_item_list', {
item_list_id: categoryName.toLowerCase().replace(/\s+/g, '_'),
item_list_name: categoryName,
items: products.map((product, index) => ({
...formatProductForDataLayer(product),
index: index
}))
});
}, [products, categoryName]);
return (/* JSX */);
}
// Product Detail Page
export function ProductDetailPage({ product }: Props) {
const { pushEcommerce } = useDataLayer();
useEffect(() => {
const variant = product.masterVariant;
const price = variant.prices?.[0];
pushEcommerce('view_item', {
currency: price?.value.currencyCode || 'USD',
value: price ? price.value.centAmount / 100 : 0,
items: [formatProductForDataLayer(product)]
});
}, [product]);
return (/* JSX */);
}
// Add to Cart
export function useAddToCart() {
const { pushEcommerce } = useDataLayer();
const addToCart = async (product: ProductProjection, quantity: number) => {
// API call to add to cart
await commercetoolsAddToCart(product.id, quantity);
const variant = product.masterVariant;
const price = variant.prices?.[0];
pushEcommerce('add_to_cart', {
currency: price?.value.currencyCode || 'USD',
value: price ? (price.value.centAmount / 100) * quantity : 0,
items: [{
...formatProductForDataLayer(product),
quantity: quantity
}]
});
};
return { addToCart };
}
// Checkout Success
export function CheckoutSuccessPage({ order }: Props) {
const { pushEcommerce } = useDataLayer();
useEffect(() => {
// Prevent duplicate tracking
const tracked = sessionStorage.getItem(`order_${order.id}_tracked`);
if (!tracked) {
pushEcommerce('purchase', formatOrderForDataLayer(order));
sessionStorage.setItem(`order_${order.id}_tracked`, 'true');
}
}, [order]);
return (/* JSX */);
}
GTM Container Configuration
Variables
Create these Data Layer Variables in GTM:
| Variable Name | Data Layer Variable Name |
|---|---|
| DL - Ecommerce Items | ecommerce.items |
| DL - Ecommerce Value | ecommerce.value |
| DL - Ecommerce Currency | ecommerce.currency |
| DL - Transaction ID | ecommerce.transaction_id |
| DL - Shipping | ecommerce.shipping |
| DL - Tax | ecommerce.tax |
Triggers
Create Custom Event triggers:
| Trigger Name | Event Name |
|---|---|
| CE - view_item_list | view_item_list |
| CE - view_item | view_item |
| CE - add_to_cart | add_to_cart |
| CE - remove_from_cart | remove_from_cart |
| CE - view_cart | view_cart |
| CE - begin_checkout | begin_checkout |
| CE - add_shipping_info | add_shipping_info |
| CE - add_payment_info | add_payment_info |
| CE - purchase | purchase |
GA4 Configuration Tag
Tag Type: Google Analytics: GA4 Configuration
Measurement ID: G-XXXXXXXXXX
Send Page View: true
Firing Trigger: All Pages
GA4 E-commerce Event Tags
Create one tag per e-commerce event:
Tag Type: Google Analytics: GA4 Event
Configuration Tag: [Your GA4 Config Tag]
Event Name: {{Event}}
Event Parameters:
- items: {{DL - Ecommerce Items}}
- value: {{DL - Ecommerce Value}}
- currency: {{DL - Ecommerce Currency}}
Firing Trigger: [Corresponding event trigger]
For purchase events, add additional parameters:
- transaction_id: {{DL - Transaction ID}}
- shipping: {{DL - Shipping}}
- tax: {{DL - Tax}}
Server-Side GTM
For enhanced privacy and accuracy, consider Server-Side GTM:
Client Configuration
// Client sends to your server-side GTM endpoint
const GTM_SERVER_URL = 'https://gtm.yourdomain.com';
export function useServerSideDataLayer() {
const push = async (data: Record<string, any>) => {
// Still push to client dataLayer for debugging
window.dataLayer?.push(data);
// Also send to server-side GTM
await fetch(`${GTM_SERVER_URL}/data`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...data,
client_id: getClientId(),
timestamp: Date.now()
})
});
};
return { push };
}
Server-Side Container Setup
- Create Server container in GTM
- Deploy to Cloud Run, App Engine, or similar
- Configure GA4 client to receive events
- Forward to GA4 API endpoint
Testing and Debugging
GTM Preview Mode
Data Layer Inspector
// Console: View current dataLayer
console.table(window.dataLayer);
// Console: Monitor dataLayer pushes
const originalPush = window.dataLayer.push.bind(window.dataLayer);
window.dataLayer.push = function(...args) {
console.log('dataLayer.push:', args);
return originalPush(...args);
};
Common Issues
Events not firing:
- Check event name matches trigger exactly
- Verify dataLayer variable paths
- Ensure GTM container is loading
Ecommerce data missing:
- Clear ecommerce object before pushing new data
- Check items array format
- Verify price is a number (not string)
Next Steps
- Data Layer Reference - Complete data layer spec
- Meta Pixel Setup - Add Facebook tracking
- Troubleshooting - Debug common issues
Related Resources
- GTM Fundamentals - Universal GTM concepts
- Server-Side Tracking - Advanced implementation
- E-commerce Best Practices - Revenue tracking guides