BigCommerce Data Layer for GTM | Blue Frog Docs

BigCommerce Data Layer for GTM

Build a comprehensive data layer for Google Tag Manager using BigCommerce Stencil context and checkout data.

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:

  1. Variables > New
  2. Variable Type: Data Layer Variable
  3. Data Layer Variable Name: (example: page.type)
  4. 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

  1. Create Trigger:

    • Type: Custom Event
    • Event Name: productPageView
    • Condition: page.type equals pages/product
  2. Apply to 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

  1. Open GTM, click Preview
  2. Enter store URL
  3. In debug panel, click Data Layer tab
  4. Inspect each data layer push
  5. 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: productId not id, customerEmail not email
  • 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 items array 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

Next Steps

Additional Resources

// SYS.FOOTER