A well-structured data layer is essential for effective tracking with Google Tag Manager on DatoCMS-powered sites. This guide covers data layer architecture and implementation patterns.
Data Layer Architecture
Core Principles
- Consistency: Use same structure across all DatoCMS content types
- Completeness: Include all relevant DatoCMS metadata
- Performance: Push data at optimal times
- Privacy: Exclude PII and sensitive data
Basic Structure
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
// Event identifier
event: 'datocms_page_view',
// DatoCMS Content Data
content: {
id: record.id,
type: record._modelApiKey,
title: record.title,
slug: record.slug,
},
// Page Metadata
page: {
path: window.location.pathname,
locale: currentLocale,
template: templateName,
},
// User Context (if applicable)
user: {
loggedIn: false,
preferences: {},
}
});
DatoCMS-Specific Data Layer Events
Page Load Event
Push on initial page load:
// lib/dataLayer.ts
export function pushPageView(record: any, locale: string) {
window.dataLayer = window.dataLayer || []
window.dataLayer.push({
event: 'datocms_page_view',
contentId: record.id,
contentType: record._modelApiKey,
contentTitle: record.title,
contentSlug: record.slug,
locale: locale,
publishedAt: record._publishedAt,
updatedAt: record._updatedAt,
hasStructuredText: !!record.content,
blockCount: record.content?.blocks?.length || 0,
})
}
// Usage in component
'use client'
import { useEffect } from 'react'
import { pushPageView } from '@/lib/dataLayer'
export function DataLayerProvider({ record, locale }) {
useEffect(() => {
pushPageView(record, locale)
}, [record, locale])
return null
}
Content Interaction Events
// Content block click
export function pushBlockInteraction(block: any, contentId: string) {
window.dataLayer.push({
event: 'datocms_block_interaction',
blockType: block._modelApiKey,
blockId: block.id,
contentId: contentId,
})
}
// Content share
export function pushContentShare(record: any, method: string) {
window.dataLayer.push({
event: 'datocms_content_share',
contentId: record.id,
contentType: record._modelApiKey,
shareMethod: method,
})
}
// Content search
export function pushSearch(query: string, results: any[]) {
window.dataLayer.push({
event: 'datocms_search',
searchQuery: query,
resultsCount: results.length,
hasResults: results.length > 0,
})
}
Form Submission Events
export function pushFormSubmit(
formBlock: any,
formData: any,
contentId: string
) {
window.dataLayer.push({
event: 'datocms_form_submit',
formName: formBlock.formName,
formId: formBlock.id,
formType: formBlock._modelApiKey,
contentId: contentId,
// Don't include form field values (privacy)
})
}
Content Type-Specific Data Layers
Blog Posts
export function pushBlogPostView(post: any) {
window.dataLayer.push({
event: 'datocms_blog_view',
content: {
id: post.id,
type: 'blog_post',
title: post.title,
slug: post.slug,
category: post.category?.title,
tags: post.tags?.map(t => t.title).join(','),
author: post.author?.name,
publishedAt: post._publishedAt,
readingTime: estimateReadingTime(post.content),
}
})
}
Products (E-commerce)
export function pushProductView(product: any) {
window.dataLayer.push({
event: 'view_item',
ecommerce: {
currency: 'USD',
value: product.price,
items: [{
item_id: product.id,
item_name: product.title,
item_brand: product.brand?.name,
item_category: product.category?.title,
item_variant: product.variant?.name,
price: product.price,
quantity: 1,
}]
},
// Additional DatoCMS metadata
content: {
id: product.id,
type: product._modelApiKey,
inStock: product.inStock,
sku: product.sku,
}
})
}
Landing Pages
export function pushLandingPageView(page: any) {
window.dataLayer.push({
event: 'datocms_landing_page_view',
content: {
id: page.id,
title: page.title,
campaign: page.campaign?.name,
variant: page.variant?.name,
},
marketing: {
campaign: page.campaign?.name,
source: new URLSearchParams(window.location.search).get('utm_source'),
medium: new URLSearchParams(window.location.search).get('utm_medium'),
}
})
}
Modular Content Data Layer
Structured Text Blocks
export function pushStructuredTextEngagement(
content: any,
contentId: string,
scrollDepth: number
) {
window.dataLayer.push({
event: 'datocms_structured_text_engagement',
contentId: contentId,
scrollDepth: scrollDepth,
blockCount: content.blocks?.length || 0,
wordCount: estimateWordCount(content),
hasEmbeds: content.blocks?.some(b => b._modelApiKey !== 'text') || false,
})
}
Image Blocks
export function pushImageView(image: any, contentId: string) {
const { responsiveImage } = image
window.dataLayer.push({
event: 'datocms_image_view',
contentId: contentId,
image: {
format: responsiveImage.webpSrcSet ? 'webp' : 'original',
width: responsiveImage.width,
height: responsiveImage.height,
usesResponsive: !!responsiveImage.srcSet,
usesBlurPlaceholder: !!responsiveImage.base64,
}
})
}
Video Blocks
export function pushVideoInteraction(
action: 'play' | 'pause' | 'complete',
video: any,
contentId: string
) {
window.dataLayer.push({
event: 'datocms_video_' + action,
contentId: contentId,
video: {
provider: video.provider,
videoId: video.providerUid,
title: video.title,
}
})
}
Multi-locale Data Layer
export function pushLocaleSwitch(
contentId: string,
fromLocale: string,
toLocale: string,
availableLocales: string[]
) {
window.dataLayer.push({
event: 'datocms_locale_switch',
contentId: contentId,
locale: {
from: fromLocale,
to: toLocale,
available: availableLocales.join(','),
}
})
}
E-commerce Data Layer
Add to Cart
export function pushAddToCart(product: any, quantity: number) {
window.dataLayer.push({
event: 'add_to_cart',
ecommerce: {
currency: 'USD',
value: product.price * quantity,
items: [{
item_id: product.id,
item_name: product.title,
item_category: product.category?.title,
price: product.price,
quantity: quantity,
}]
}
})
}
Purchase
export function pushPurchase(order: any) {
window.dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: order.id,
value: order.total,
currency: 'USD',
tax: order.tax,
shipping: order.shipping,
items: order.items.map((item: any) => ({
item_id: item.product.id,
item_name: item.product.title,
item_category: item.product.category?.title,
price: item.price,
quantity: item.quantity,
}))
}
})
}
User Context Data Layer
export function pushUserContext(preferences: any) {
window.dataLayer.push({
event: 'datocms_user_context',
user: {
preferredLocale: preferences.locale,
contentPreferences: preferences.contentTypes,
subscriptionStatus: preferences.subscription,
}
})
}
Implementation Patterns
Server-Side Rendering (SSR)
// Next.js example
export default function BlogPost({ post }: { post: any }) {
return (
<>
<Script
id="datocms-data-layer"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'datocms_page_view',
contentId: '${post.id}',
contentType: '${post._modelApiKey}',
contentTitle: '${escapeHtml(post.title)}',
});
`,
}}
/>
<article>{/* Content */}</article>
</>
)
}
function escapeHtml(text: string): string {
return text.replace(/[&<>"']/g, char => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
}[char] || char))
}
Client-Side Rendering (CSR)
'use client'
import { useEffect } from 'react'
export function DataLayerProvider({ record }: { record: any }) {
useEffect(() => {
if (typeof window !== 'undefined') {
window.dataLayer = window.dataLayer || []
window.dataLayer.push({
event: 'datocms_page_view',
contentId: record.id,
contentType: record._modelApiKey,
})
}
}, [record])
return null
}
Route Changes
'use client'
import { usePathname } from 'next/navigation'
import { useEffect } from 'react'
export function RouteChangeTracker() {
const pathname = usePathname()
useEffect(() => {
window.dataLayer = window.dataLayer || []
window.dataLayer.push({
event: 'route_change',
page: {
path: pathname,
url: window.location.href,
}
})
}, [pathname])
return null
}
Data Layer Helper Functions
// lib/dataLayer.ts
export const dataLayer = {
// Initialize data layer
init() {
if (typeof window !== 'undefined') {
window.dataLayer = window.dataLayer || []
}
},
// Push event
push(data: Record<string, any>) {
if (typeof window !== 'undefined') {
window.dataLayer = window.dataLayer || []
window.dataLayer.push(data)
}
},
// Get current data layer
get() {
if (typeof window !== 'undefined') {
return window.dataLayer || []
}
return []
},
// Debug data layer
debug() {
if (typeof window !== 'undefined') {
console.table(window.dataLayer)
}
}
}
TypeScript Definitions
// types/dataLayer.d.ts
interface DatoCMSContent {
id: string
type: string
title: string
slug: string
locale?: string
publishedAt?: string
updatedAt?: string
}
interface DataLayerEvent {
event: string
content?: DatoCMSContent
[key: string]: any
}
declare global {
interface Window {
dataLayer: DataLayerEvent[]
}
}
export {}
Testing Data Layer
Console Testing
// View data layer
console.table(window.dataLayer)
// Filter events
window.dataLayer.filter(item => item.event === 'datocms_page_view')
// Get latest event
window.dataLayer[window.dataLayer.length - 1]
GTM Preview Mode
- Enable GTM Preview mode
- Navigate to your DatoCMS page
- Check "Data Layer" tab
- Verify all expected fields present
Best Practices
- Sanitize Data: Escape HTML in titles and text
- No PII: Never include personal information
- Consistent Keys: Use same naming across events
- Performance: Push data after critical rendering
- Debugging: Use GTM Preview mode extensively
Next Steps
- GTM Setup Guide - Install GTM
- Event Tracking - Configure events
- Troubleshoot Issues - Debug problems