DatoCMS Data Layer Configuration for GTM | OpsBlu Docs

DatoCMS Data Layer Configuration for GTM

Configure a comprehensive data layer structure for DatoCMS content to power Google Tag Manager tracking.

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

  1. Consistency: Use same structure across all DatoCMS content types
  2. Completeness: Include all relevant DatoCMS metadata
  3. Performance: Push data at optimal times
  4. 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 => ({
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
  }[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

  1. Enable GTM Preview mode
  2. Navigate to your DatoCMS page
  3. Check "Data Layer" tab
  4. Verify all expected fields present

Best Practices

  1. Sanitize Data: Escape HTML in titles and text
  2. No PII: Never include personal information
  3. Consistent Keys: Use same naming across events
  4. Performance: Push data after critical rendering
  5. Debugging: Use GTM Preview mode extensively

Next Steps