BigCommerce Data Layer for GTM
A properly structured data layer makes tag management easier, enables advanced tracking, and ensures consistent data across all marketing platforms. This guide shows how to build a comprehensive data layer for BigCommerce using Stencil context and checkout data.
What is a Data Layer?
A data layer is a JavaScript object that contains structured information about the page, user, and interactions. Google Tag Manager reads this data to:
- Fire tags conditionally based on data layer values
- Pass dynamic values to tags (product names, prices, user IDs)
- Track events without hardcoding tag logic
- Ensure consistency across all marketing pixels
Data Layer Structure
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'pageview',
'pageType': 'product',
'pageName': 'Blue T-Shirt - Product Page',
'product': {
'id': '123',
'name': 'Blue T-Shirt',
'price': 29.99,
'brand': 'MyBrand',
'category': 'Apparel/Men/T-Shirts'
},
'customer': {
'loggedIn': true,
'id': '456',
'group': 'VIP'
}
});
Implementing Data Layer on BigCommerce
Base Data Layer (All Pages)
File: templates/layout/base.html
Add this before the GTM container script in the <head>:
{{!-- BigCommerce Data Layer --}}
<script>
window.dataLayer = window.dataLayer || [];
// Base page data
dataLayer.push({
'event': 'pageDataReady',
'page': {
'type': '{{template}}',
'url': window.location.href,
'path': window.location.pathname,
'title': document.title,
'referrer': document.referrer
},
'site': {
'name': '{{settings.store_name}}',
'currency': '{{currency.code}}',
'locale': '{{locale_name}}',
'storeHash': '{{settings.store_hash}}'
},
{{#if customer}}
'customer': {
'loggedIn': true,
'id': '{{customer.id}}',
'email': '{{customer.email}}',
'name': '{{customer.name}}',
'group': {
'id': '{{customer.customer_group_id}}',
'name': '{{customer.customer_group_name}}'
}
},
{{else}}
'customer': {
'loggedIn': false
},
{{/if}}
'cart': {
'id': '{{cart_id}}',
'itemCount': {{cart.quantity}},
'value': {{cart.grand_total}},
'currency': '{{currency.code}}'
}
});
</script>
{{!-- End BigCommerce Data Layer --}}
Product Page Data Layer
File: templates/pages/product.html
{{#with product}}
<script>
// Product page specific data
dataLayer.push({
'event': 'productPageView',
'ecommerce': {
'detail': {
'products': [{
'id': '{{id}}',
'name': '{{title}}',
'price': {{price.without_tax.value}},
'brand': '{{brand.name}}',
'category': '{{category.[0]}}',
{{#if category.[1]}}
'category2': '{{category.[1]}}',
{{/if}}
{{#if category.[2]}}
'category3': '{{category.[2]}}',
{{/if}}
'variant': '{{selected_variant}}',
'sku': '{{sku}}',
'stock': '{{stock}}',
'availability': '{{availability}}',
'rating': {{rating}},
'reviewCount': {{num_reviews}}
}]
}
},
'product': {
'id': '{{id}}',
'name': '{{title}}',
'price': {{price.without_tax.value}},
'priceWithTax': {{price.with_tax.value}},
'salePrice': {{#if price.sale_price}}{{price.sale_price.value}}{{else}}null{{/if}},
'onSale': {{#if price.sale_price}}true{{else}}false{{/if}},
'brand': '{{brand.name}}',
'sku': '{{sku}}',
'inStock': {{#compare stock '>' 0}}true{{else}}false{{/compare}},
'stockLevel': {{stock}},
'images': [
{{#each images}}
'{{data}}{{../image.data}}'{{#unless @last}},{{/unless}}
{{/each}}
]
}
});
</script>
{{/with}}
Category Page Data Layer
File: templates/pages/category.html
<script>
// Category page data
dataLayer.push({
'event': 'categoryPageView',
'category': {
'id': '{{category.id}}',
'name': '{{category.name}}',
'url': '{{category.url}}',
'productCount': {{category.product_count}}
},
'ecommerce': {
'impressions': [
{{#each products}}
{
'id': '{{id}}',
'name': '{{name}}',
'price': {{price.without_tax.value}},
'brand': '{{brand.name}}',
'category': '{{../category.name}}',
'position': {{@index}},
'list': 'Category: {{../category.name}}'
}{{#unless @last}},{{/unless}}
{{/each}}
]
}
});
</script>
Cart Page Data Layer
File: templates/pages/cart.html
{{#if cart.items}}
<script>
// Cart page data
dataLayer.push({
'event': 'cartPageView',
'ecommerce': {
'checkout': {
'actionField': {'step': 0, 'option': 'View Cart'},
'products': [
{{#each cart.items}}
{
'id': '{{product_id}}',
'name': '{{name}}',
'price': {{price.value}},
'brand': '{{brand}}',
'category': '{{category}}',
'variant': '{{variant}}',
'quantity': {{quantity}}
}{{#unless @last}},{{/unless}}
{{/each}}
]
}
},
'cart': {
'items': [
{{#each cart.items}}
{
'productId': '{{product_id}}',
'productName': '{{name}}',
'sku': '{{sku}}',
'price': {{price.value}},
'quantity': {{quantity}},
'lineTotal': {{extended_sale_price.value}}
}{{#unless @last}},{{/unless}}
{{/each}}
],
'subtotal': {{cart.sub_total}},
'discount': {{cart.discount_total}},
'shipping': {{cart.shipping_cost}},
'tax': {{cart.tax_total}},
'total': {{cart.grand_total}},
'itemCount': {{cart.quantity}},
'currency': '{{currency.code}}'
}
});
</script>
{{/if}}
Search Results Data Layer
File: templates/pages/search.html
<script>
// Search page data
dataLayer.push({
'event': 'searchResults',
'search': {
'query': '{{sanitize forms.search.query}}',
'resultsCount': {{products.length}},
'hasResults': {{#if products}}true{{else}}false{{/if}}
},
{{#if products}}
'ecommerce': {
'impressions': [
{{#each products}}
{
'id': '{{id}}',
'name': '{{name}}',
'price': {{price.without_tax.value}},
'brand': '{{brand.name}}',
'position': {{@index}},
'list': 'Search Results: {{../forms.search.query}}'
}{{#unless @last}},{{/unless}}
{{/each}}
]
}
{{/if}}
});
</script>
Checkout Data Layer
BigCommerce's optimized checkout requires special handling since it's hosted separately.
Checkout Page Data Layer
Location: Settings > Checkout > Header Scripts
<script>
window.dataLayer = window.dataLayer || [];
// Poll for BigCommerce checkout data
(function waitForCheckoutData() {
if (typeof BCData !== 'undefined' && BCData.checkout) {
var checkout = BCData.checkout;
// Check if we're on order confirmation
var isConfirmation = window.location.pathname.includes('order-confirmation');
if (isConfirmation) {
// Order confirmation data layer
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'purchase': {
'actionField': {
'id': checkout.order ? checkout.order.orderId : 'unknown',
'revenue': checkout.cart.baseAmount,
'tax': checkout.cart.taxTotal,
'shipping': checkout.cart.shippingCostTotal,
'coupon': checkout.cart.coupons && checkout.cart.coupons.length > 0
? checkout.cart.coupons[0].code
: ''
},
'products': checkout.cart.lineItems.physicalItems.map(function(item, index) {
return {
'id': item.productId.toString(),
'name': item.name,
'price': item.salePrice,
'brand': item.brand || '',
'category': item.categoryNames ? item.categoryNames[0] : '',
'variant': item.variantId ? item.variantId.toString() : '',
'quantity': item.quantity
};
})
}
},
'transaction': {
'id': checkout.order ? checkout.order.orderId : 'unknown',
'total': checkout.cart.baseAmount,
'subtotal': checkout.cart.cartAmount,
'tax': checkout.cart.taxTotal,
'shipping': checkout.cart.shippingCostTotal,
'currency': checkout.cart.currency.code,
'itemCount': checkout.cart.lineItems.physicalItems.length
}
});
} else {
// Checkout page data layer
dataLayer.push({
'event': 'checkoutStart',
'ecommerce': {
'checkout': {
'actionField': {'step': 1},
'products': checkout.cart.lineItems.physicalItems.map(function(item, index) {
return {
'id': item.productId.toString(),
'name': item.name,
'price': item.salePrice,
'brand': item.brand || '',
'category': item.categoryNames ? item.categoryNames[0] : '',
'variant': item.variantId ? item.variantId.toString() : '',
'quantity': item.quantity
};
})
}
},
'checkout': {
'step': 'start',
'cart': {
'total': checkout.cart.baseAmount,
'subtotal': checkout.cart.cartAmount,
'tax': checkout.cart.taxTotal,
'shipping': checkout.cart.shippingCostTotal,
'currency': checkout.cart.currency.code,
'itemCount': checkout.cart.lineItems.physicalItems.length
}
}
});
}
} else {
// Retry if BCData not available yet
setTimeout(waitForCheckoutData, 100);
}
})();
</script>
Event-Based Data Layer Pushes
For user interactions, push data layer events dynamically.
Add to Cart Event
File: assets/js/theme/common/cart-item-details.js
export function pushAddToCartEvent(productId, quantity) {
// Fetch full product details
fetch(`/api/storefront/products/${productId}`)
.then(response => response.json())
.then(product => {
dataLayer.push({
'event': 'addToCart',
'ecommerce': {
'add': {
'products': [{
'id': product.id.toString(),
'name': product.name,
'price': product.price.without_tax.value,
'brand': product.brand?.name || '',
'category': product.categories?.[0]?.name || '',
'quantity': quantity
}]
}
}
});
});
}
// Hook into BigCommerce cart event
$('body').on('cart-item-add', (event, productId, quantity) => {
pushAddToCartEvent(productId, quantity);
});
Remove from Cart Event
export function pushRemoveFromCartEvent(itemId, itemData) {
dataLayer.push({
'event': 'removeFromCart',
'ecommerce': {
'remove': {
'products': [{
'id': itemData.productId.toString(),
'name': itemData.name,
'price': itemData.price,
'quantity': itemData.quantity
}]
}
}
});
}
Product Click Event
File: templates/components/products/card.html
<a href="{{url}}"
data-product-id="{{id}}"
data-product-name="{{name}}"
data-product-price="{{price.without_tax.value}}"
data-product-brand="{{brand.name}}"
data-product-category="{{category}}"
onclick="pushProductClickEvent(this)">
{{!-- Product card content --}}
</a>
<script>
function pushProductClickEvent(element) {
dataLayer.push({
'event': 'productClick',
'ecommerce': {
'click': {
'actionField': {'list': 'Category Page'},
'products': [{
'id': element.getAttribute('data-product-id'),
'name': element.getAttribute('data-product-name'),
'price': parseFloat(element.getAttribute('data-product-price')),
'brand': element.getAttribute('data-product-brand'),
'category': element.getAttribute('data-product-category')
}]
}
}
});
}
</script>
Advanced Data Layer Patterns
User Identification
For logged-in users, include hashed identifiers:
{{#if customer}}
// Hash email for privacy
async function hashEmail(email) {
const msgBuffer = new TextEncoder().encode(email.toLowerCase().trim());
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
hashEmail('{{customer.email}}').then(hashedEmail => {
dataLayer.push({
'user': {
'id': '{{customer.id}}',
'hashedEmail': hashedEmail,
'customerGroup': '{{customer.customer_group_name}}',
'status': 'logged_in'
}
});
});
{{/if}}
Enhanced Product Data
Include additional product attributes:
dataLayer.push({
'event': 'productView',
'product': {
'id': '{{product.id}}',
'name': '{{product.name}}',
'price': {{product.price.without_tax.value}},
'brand': '{{product.brand.name}}',
'category': '{{product.category.[0]}}',
'sku': '{{product.sku}}',
'availability': '{{product.availability}}',
'condition': '{{product.condition}}',
'customFields': {
{{#each product.custom_fields}}
'{{name}}': '{{value}}'{{#unless @last}},{{/unless}}
{{/each}}
},
'options': [
{{#each product.options}}
{
'id': {{id}},
'displayName': '{{display_name}}',
'values': [
{{#each values}}
'{{label}}'{{#unless @last}},{{/unless}}
{{/each}}
]
}{{#unless @last}},{{/unless}}
{{/each}}
]
}
});
Promotion Tracking
Track banner clicks and promotions:
// On promotional banner click
function trackPromotionClick(promoId, promoName, creativeName, position) {
dataLayer.push({
'event': 'promotionClick',
'ecommerce': {
'promoClick': {
'promotions': [{
'id': promoId,
'name': promoName,
'creative': creativeName,
'position': position
}]
}
}
});
}
Using Data Layer Variables in GTM
Create Data Layer Variables
In GTM:
- Variables > New
- Variable Type: Data Layer Variable
- Data Layer Variable Name: (example:
page.type) - Save
Common variables to create:
| Variable Name | Data Layer Path | Use Case |
|---|---|---|
| Page Type | page.type |
Conditional tag firing |
| Customer ID | customer.id |
User identification |
| Customer Logged In | customer.loggedIn |
Conditional logic |
| Product ID | product.id |
Product tracking |
| Product Name | product.name |
Event parameters |
| Product Price | product.price |
Ecommerce events |
| Cart Total | cart.value |
Cart value tracking |
| Transaction ID | transaction.id |
Purchase tracking |
Use in Tag Configuration
Example: Fire tag only on product pages
Create Trigger:
- Type: Custom Event
- Event Name:
productPageView - Condition:
page.typeequalspages/product
Apply to Tag:
- Attach trigger to your GA4 or Meta Pixel tag
Example: Pass product data to GA4
In GA4 event tag, add parameters:
- item_id:
\{\{Product ID\}\}(data layer variable) - item_name:
\{\{Product Name\}\} - price:
\{\{Product Price\}\}
Testing the Data Layer
Browser Console Inspection
// View entire data layer
console.table(dataLayer);
// Find specific events
dataLayer.filter(item => item.event === 'productPageView');
// Check current values
dataLayer[dataLayer.length - 1];
GTM Preview Mode
- Open GTM, click Preview
- Enter store URL
- In debug panel, click Data Layer tab
- Inspect each data layer push
- Verify variables populate correctly
Use dataLayer Validator
Add to browser console:
// Simple validator
function validateDataLayer() {
const events = dataLayer.filter(item => item.event);
console.log('Events found:', events.length);
events.forEach((event, index) => {
console.group(`Event ${index + 1}: ${event.event}`);
console.log('Data:', event);
// Check for common issues
if (event.ecommerce && event.ecommerce.products) {
event.ecommerce.products.forEach((product, pIndex) => {
if (!product.id) console.warn(`Product ${pIndex} missing ID`);
if (!product.name) console.warn(`Product ${pIndex} missing name`);
if (product.price === undefined) console.warn(`Product ${pIndex} missing price`);
});
}
console.groupEnd();
});
}
validateDataLayer();
Best Practices
1. Initialize Data Layer Early
Always initialize before GTM container:
{{!-- Data layer first --}}
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({/* initial data */});
</script>
{{!-- Then GTM container --}}
<script>(function(w,d,s,l,i){/* GTM code */})(/* ... */);</script>
2. Use Consistent Naming
- camelCase for property names
- Descriptive names:
productIdnotid,customerEmailnotemail - Consistent structure across pages
3. Don't Store Sensitive Data
Never include:
- Full credit card numbers
- Passwords
- Social security numbers
- Unencrypted PII
Hash sensitive data:
// Good: hashed email
'hashedEmail': 'a1b2c3d4e5...'
// Bad: plain email
'email': 'customer@example.com'
4. Version Your Data Layer
Document changes:
dataLayer.push({
'dataLayerVersion': '2.0', // Track schema version
'event': 'pageview',
// ... rest of data
});
5. Handle Missing Data Gracefully
{{!-- Use conditional helpers --}}
'brand': '{{#if product.brand}}{{product.brand.name}}{{else}}Unknown{{/if}}',
{{!-- Or default values --}}
'category': '{{product.category.[0]}}' || 'Uncategorized',
Troubleshooting
Data Layer Not Defined
Cause: GTM loading before data layer initialization
Solution: Ensure data layer script comes before GTM container
Variables Showing Undefined
Cause: Handlebars context not available or incorrect path
Solution:
- Check Stencil context object structure
- Use
\{\{jsContext\}\}to inspect available data - Verify template has access to product/category/customer objects
Ecommerce Events Not Recognized
Cause: Incorrect ecommerce object structure
Solution:
- Follow GA4 ecommerce format exactly
- Verify
itemsarray structure - Check property names match GA4 spec (item_id, item_name, etc.)
Checkout Data Not Populating
Cause: BCData object timing or availability
Solution:
- Use polling function to wait for BCData
- Check BigCommerce plan supports checkout scripts
- Verify scripts in correct checkout script section