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