Install Google Tag Manager with Contentful | OpsBlu Docs

Install Google Tag Manager with Contentful

How to install and configure Google Tag Manager on websites and applications powered by Contentful headless CMS.

Google Tag Manager (GTM) allows you to manage all your tracking tags from a single interface without editing code. For Contentful-powered sites, GTM is installed in your frontend framework, not in Contentful itself.

Why Use GTM with Contentful?

Benefits:

  • Centralized tag management - All tracking tags in one place
  • No code deployments - Update tracking without redeploying your app
  • Version control - Track changes to tags and triggers
  • Testing tools - Preview mode to test before publishing
  • Collaboration - Multiple team members can manage tags
  • Performance - Single container load vs. multiple scripts

Best for:

  • Marketing teams that need to update tracking frequently
  • Sites with multiple analytics platforms (GA4, Meta, TikTok, etc.)
  • A/B testing and conversion rate optimization
  • Dynamic event tracking based on user behavior

Before You Begin

Prerequisites:

  • Google Tag Manager account (free at tagmanager.google.com)
  • GTM Container ID (format: GTM-XXXXXXX)
  • Frontend framework with Contentful integrated
  • Developer access to your codebase

Implementation by Framework

Method 1: Next.js (App Router)

For Next.js 13+ with the App Router.

1. Create GTM Component

Create app/components/GoogleTagManager.tsx:

'use client'

import Script from 'next/script'

export function GoogleTagManager() {
  const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID

  if (!GTM_ID) {
    console.warn('GTM ID not found')
    return null
  }

  return (
    <>
      {/* GTM Script */}
      <Script
        id="gtm-script"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
            (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}');
          `,
        }}
      />
    </>
  )
}

export function GoogleTagManagerNoScript() {
  const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID

  if (!GTM_ID) return null

  return (
    <noscript>
      <iframe
        src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
        height="0"
        width="0"
        style={{ display: 'none', visibility: 'hidden' }}
      />
    </noscript>
  )
}

2. Add to Root Layout

Update app/layout.tsx:

import { GoogleTagManager, GoogleTagManagerNoScript } from './components/GoogleTagManager'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <head>
        <GoogleTagManager />
      </head>
      <body>
        <GoogleTagManagerNoScript />
        {children}
      </body>
    </html>
  )
}

3. Set Environment Variables

Create .env.local:

NEXT_PUBLIC_GTM_ID=GTM-XXXXXXX

4. Initialize Data Layer (Optional)

Create app/components/DataLayerInit.tsx:

'use client'

import { useEffect } from 'react'

export function DataLayerInit() {
  useEffect(() => {
    window.dataLayer = window.dataLayer || []
    window.dataLayer.push({
      event: 'dataLayer_initialized',
      platform: 'contentful',
      framework: 'nextjs',
    })
  }, [])

  return null
}

Method 2: Next.js (Pages Router)

For Next.js using Pages Router.

1. Update _document.tsx

Create or update pages/_document.tsx:

import { Html, Head, Main, NextScript } from 'next/document'

export default function Document() {
  const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID

  return (
    <Html>
      <Head>
        {/* GTM Script */}
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (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}');
            `,
          }}
        />
      </Head>
      <body>
        {/* GTM noscript */}
        <noscript>
          <iframe
            src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
            height="0"
            width="0"
            style={{ display: 'none', visibility: 'hidden' }}
          />
        </noscript>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

2. Track Route Changes

Update pages/_app.tsx:

import { useEffect } from 'react'
import { useRouter } from 'next/router'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  const router = useRouter()

  useEffect(() => {
    const handleRouteChange = (url: string) => {
      window.dataLayer?.push({
        event: 'pageview',
        page: url,
      })
    }

    router.events.on('routeChangeComplete', handleRouteChange)
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange)
    }
  }, [router.events])

  return <Component {...pageProps} />
}

Method 3: Gatsby

Perfect for static Contentful sites.

1. Install Plugin

npm install gatsby-plugin-google-tagmanager

2. Configure in gatsby-config.js

module.exports = {
  plugins: [
    {
      resolve: 'gatsby-plugin-google-tagmanager',
      options: {
        id: process.env.GTM_ID,

        // Include GTM in development (optional)
        includeInDevelopment: false,

        // Default dataLayer name
        defaultDataLayer: { platform: 'contentful' },

        // GTM script load timing
        enableWebVitalsTracking: true,

        // Route change event name
        routeChangeEventName: 'gatsby-route-change',
      },
    },
  ],
}

3. Set Environment Variables

Create .env.production:

GTM_ID=GTM-XXXXXXX

4. Manual Implementation (Alternative)

If not using plugin, update gatsby-ssr.js:

export const setHeadComponents, setPreBodyComponents }) => {
  const GTM_ID = process.env.GTM_ID

  if (!GTM_ID) return

  setHeadComponents([
    <script
      key="gtm-script"
      dangerouslySetInnerHTML={{
        __html: `
          (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}');
        `,
      }}
    />,
  ])

  setPreBodyComponents([
    <noscript key="gtm-noscript">
      <iframe
        src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
        height="0"
        width="0"
        style={{ display: 'none', visibility: 'hidden' }}
      />
    </noscript>,
  ])
}

Track route changes in gatsby-browser.js:

export const location }) => {
  if (typeof window !== 'undefined' && window.dataLayer) {
    window.dataLayer.push({
      event: 'pageview',
      page: location.pathname,
    })
  }
}

Method 4: Nuxt.js

For Vue developers using Contentful.

1. Install Module

npm install @nuxtjs/gtm

2. Configure in nuxt.config.js

export default {
  modules: [
    '@nuxtjs/gtm',
  ],

  gtm: {
    id: process.env.GTM_ID,
    enabled: true,
    debug: process.env.NODE_ENV !== 'production',

    // Auto-track page views
    pageTracking: true,

    // Optional: Custom pageView event
    pageViewEventName: 'nuxtRoute',

    // Load GTM script after page load
    defer: false,

    // Respect Do Not Track
    respectDoNotTrack: true,
  },

  publicRuntimeConfig: {
    gtmId: process.env.GTM_ID,
  },
}

3. Push Custom Events

<template>
  <button @click="trackClick">Click Me</button>
</template>

<script>
export default {
  methods: {
    trackClick() {
      this.$gtm.push({
        event: 'button_click',
        buttonName: 'cta_button',
      })
    },
  },
}
</script>

Method 5: React SPA (Vite, Create React App)

For single-page applications.

1. Add to index.html

Update public/index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- Google Tag Manager -->
    <script>
      (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','%VITE_GTM_ID%');
    </script>
    <!-- End Google Tag Manager -->
  </head>
  <body>
    <!-- Google Tag Manager (noscript) -->
    <noscript>
      <iframe
        src="https://www.googletagmanager.com/ns.html?id=%VITE_GTM_ID%"
        height="0"
        width="0"
        style="display:none;visibility:hidden"
      ></iframe>
    </noscript>
    <!-- End Google Tag Manager (noscript) -->

    <div id="root"></div>
  </body>
</html>

2. Track Route Changes

// App.tsx
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'

function App() {
  const location = useLocation()

  useEffect(() => {
    window.dataLayer?.push({
      event: 'pageview',
      page: location.pathname,
    })
  }, [location])

  return <Router>{/* Routes */}</Router>
}

3. Use Environment Variables

Create .env:

VITE_GTM_ID=GTM-XXXXXXX
# or for CRA
REACT_APP_GTM_ID=GTM-XXXXXXX

For runtime replacement, use a plugin or replace manually in build process.

Configure GTM Container

1. Create Basic Tags

GA4 Configuration Tag:

  1. In GTM, go to TagsNew
  2. Click Tag ConfigurationGoogle Analytics: GA4 Configuration
  3. Enter your Measurement ID (G-XXXXXXXXXX)
  4. Triggering: Select All Pages
  5. Save and name it "GA4 - Configuration"

GA4 Page View Tag:

  1. TagsNew
  2. Tag ConfigurationGoogle Analytics: GA4 Event
  3. Configuration Tag: Select your GA4 Configuration tag
  4. Event Name: page_view
  5. Triggering: Custom Event trigger for pageview (if tracking SPAs)
  6. Save

2. Create Variables for Contentful Data

See GTM Data Layer for Contentful for detailed variable setup.

Common Variables:

  • Content Type (Data Layer Variable: content.type)
  • Content ID (Data Layer Variable: content.id)
  • Content Title (Data Layer Variable: content.title)
  • Content Category (Data Layer Variable: content.category)

3. Create Triggers

Page View Trigger (SPA):

  • Type: Custom Event
  • Event name: pageview
  • Use for: SPA route changes

Content View Trigger:

  • Type: Custom Event
  • Event name: content_view
  • Use for: Contentful content engagement

Scroll Depth Trigger:

  • Type: Scroll Depth
  • Percentages: 25, 50, 75, 90
  • Use for: Content engagement tracking

Advanced Configuration

Exclude Preview Mode

Don't track Contentful preview sessions:

// Check for preview mode before initializing GTM
const isPreview =
  typeof window !== 'undefined' &&
  (window.location.search.includes('preview=true') ||
   window.location.search.includes('contentful_preview=true'))

// Conditionally initialize GTM
if (!isPreview) {
  // Initialize GTM script
}

Or use GTM trigger exception:

  1. Create variable: URL contains preview=true
  2. Add to trigger exceptions

Multi-Environment Setup

Use different GTM containers for different environments:

const GTM_ID =
  process.env.VERCEL_ENV === 'production'
    ? process.env.NEXT_PUBLIC_GTM_PRODUCTION_ID
    : process.env.VERCEL_ENV === 'preview'
    ? process.env.NEXT_PUBLIC_GTM_STAGING_ID
    : process.env.NEXT_PUBLIC_GTM_DEV_ID

Content Security Policy (CSP)

If using CSP headers, allow GTM:

// next.config.js
const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com;
  connect-src 'self' https://www.google-analytics.com;
  img-src 'self' data: https://www.google-analytics.com;
`

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
          },
        ],
      },
    ]
  },
}

Server-Side GTM (Advanced)

For server-side tagging with Next.js:

// pages/api/gtm-server.ts
export default async function handler(req, res) {
  const GTM_SERVER_URL = process.env.GTM_SERVER_CONTAINER_URL

  await fetch(GTM_SERVER_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      event_name: req.body.event,
      ...req.body.params,
    }),
  })

  res.status(200).json({ success: true })
}

Testing & Verification

1. GTM Preview Mode

  1. In GTM, click Preview
  2. Enter your site URL
  3. Navigate your Contentful-powered site
  4. Verify:
    • GTM container loads
    • Tags fire correctly
    • Variables populate with Contentful data
    • Triggers work as expected

2. Browser Console

// Check if dataLayer exists
console.log(window.dataLayer)

// Monitor dataLayer pushes
const originalPush = window.dataLayer.push
window.dataLayer.push = function() {
  console.log('DataLayer Push:', arguments[0])
  return originalPush.apply(window.dataLayer, arguments)
}

3. Browser Extensions

  • Tag Assistant Legacy - View tags on page
  • dataLayer Inspector - View dataLayer in real-time
  • Google Analytics Debugger - Verify GA4 events

4. Network Tab

Chrome DevTools → Network:

  • Filter by gtm or google-analytics
  • Verify requests are sent
  • Check request payloads

Common GTM Tags for Contentful

Meta Pixel Tag

  1. TagsNew
  2. Custom HTML tag
  3. Add Meta Pixel code:
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '{{Meta Pixel ID}}');
fbq('track', 'PageView');
</script>
  1. Triggering: All Pages
  2. Save

LinkedIn Insight Tag

  1. TagsNew
  2. Custom HTML
  3. Add LinkedIn tracking code
  4. Triggering: All Pages

Custom Event Tag

Track Contentful-specific events:

  1. TagsNew
  2. Google Analytics: GA4 Event
  3. Event Name: content_view
  4. Event Parameters:
    • content_type: \{\{DLV - Content Type\}\}
    • content_id: \{\{DLV - Content ID\}\}
    • title: \{\{DLV - Content Title\}\}
  5. Triggering: Custom Event content_view

Performance Optimization

Async Loading

GTM loads asynchronously by default, but you can optimize further:

// Load GTM after page interactive
<Script
  id="gtm"
  strategy="afterInteractive"  // Next.js
  dangerouslySetInnerHTML={{...}}
/>

Lazy Load Tags

In GTM, set tags to fire after specific user interactions rather than immediately.

Minimize Data Layer Size

Only push necessary data to avoid performance impact:

// Good
dataLayer.push({
  event: 'content_view',
  contentType: 'blog',
  contentId: '123'
})

// Bad (too much data)
dataLayer.push({
  event: 'content_view',
  entireContentfulEntry: {...}  // Too large
})

Troubleshooting

GTM Container Not Loading

Checks:

  • GTM ID is correct (format: GTM-XXXXXXX)
  • No JavaScript errors in console
  • Script isn't blocked by ad blocker
  • CSP headers allow GTM

Tags Not Firing

Checks:

  • GTM container is published
  • Triggers are configured correctly
  • Variables return expected values
  • Preview mode shows tag should fire

Data Layer Variables Undefined

Checks:

  • Data layer is pushed before GTM reads it
  • Variable names match exactly
  • Data layer structure is correct

See Events Not Firing for detailed debugging.

Contentful-Specific Data Layer Integration

Pushing Contentful Data to Data Layer

Structure your data layer to include Contentful metadata:

// utils/contentful-datalayer.ts
export function pushContentfulToDataLayer(entry: any, eventName: string = 'content_view') {
  if (typeof window === 'undefined' || !window.dataLayer) return

  window.dataLayer.push({
    event: eventName,
    contentful: {
      contentType: entry.sys.contentType.sys.id,
      contentId: entry.sys.id,
      title: entry.fields.title,
      category: entry.fields.category,
      tags: entry.fields.tags || [],
      author: {
        name: entry.fields.author?.fields.name,
        id: entry.fields.author?.sys.id,
      },
      metadata: {
        createdAt: entry.sys.createdAt,
        updatedAt: entry.sys.updatedAt,
        publishedAt: entry.sys.publishedAt,
        revision: entry.sys.revision,
        locale: entry.sys.locale,
        space: entry.sys.space?.sys.id,
        environment: entry.sys.environment?.sys.id,
      },
    },
    page: {
      type: 'content',
      path: window.location.pathname,
      url: window.location.href,
    },
  })
}

Component-Level Implementation

Use in your Contentful content components:

// components/ContentfulArticle.tsx
'use client'

import { useEffect } from 'react'
import { pushContentfulToDataLayer } from '@/utils/contentful-datalayer'

export function ContentfulArticle({ entry }) {
  useEffect(() => {
    // Push to data layer when article loads
    pushContentfulToDataLayer(entry, 'article_view')
  }, [entry])

  return (
    <article>
      <h1>{entry.fields.title}</h1>
      {/* ... */}
    </article>
  )
}

GTM Variables for Contentful Data

Create these Data Layer Variables in GTM to capture Contentful data:

Navigate to Variables → User-Defined Variables → New

Variable Name Type Data Layer Variable Name
Contentful - Content Type Data Layer Variable contentful.contentType
Contentful - Content ID Data Layer Variable contentful.contentId
Contentful - Title Data Layer Variable contentful.title
Contentful - Category Data Layer Variable contentful.category
Contentful - Tags Data Layer Variable contentful.tags
Contentful - Author Name Data Layer Variable contentful.author.name
Contentful - Locale Data Layer Variable contentful.metadata.locale
Contentful - Space ID Data Layer Variable contentful.metadata.space
Contentful - Environment Data Layer Variable contentful.metadata.environment
Contentful - Revision Data Layer Variable contentful.metadata.revision

Create Contentful-Specific Triggers

1. Content View Trigger

  • Type: Custom Event
  • Event name: content_view
  • Fires on: All custom events

2. Specific Content Type Trigger

  • Type: Custom Event
  • Event name: content_view
  • Fire on: Some custom events
  • Condition: contentful.contentType equals blogPost (or your content type)

3. Content Category Trigger

  • Type: Custom Event
  • Event name: content_view
  • Fire on: Some custom events
  • Condition: contentful.category equals news (or your category)

Multi-Locale Data Layer Setup

Handle multiple locales in your data layer:

export function pushMultiLocaleContent(entry: any, currentLocale: string) {
  if (!window.dataLayer) return

  // Get all available locales for this entry
  const availableLocales = Object.keys(entry.fields.title || {})

  window.dataLayer.push({
    event: 'multilingual_content_view',
    contentful: {
      contentType: entry.sys.contentType.sys.id,
      contentId: entry.sys.id,
      currentLocale: currentLocale,
      availableLocales: availableLocales,
      defaultLocale: entry.sys.space?.sys.defaultLocale || 'en-US',
      title: entry.fields.title?.[currentLocale] || entry.fields.title,
    },
  })
}

GTM Configuration:

Create these additional variables:

  • Contentful - Current Localecontentful.currentLocale
  • Contentful - Available Localescontentful.availableLocales
  • Contentful - Default Localecontentful.defaultLocale

Reference Field Tracking

Track linked Contentful entries:

export function pushContentWithReferences(entry: any) {
  if (!window.dataLayer) return

  // Extract all referenced entries
  const references = extractReferences(entry)

  window.dataLayer.push({
    event: 'content_with_references',
    contentful: {
      contentType: entry.sys.contentType.sys.id,
      contentId: entry.sys.id,
      references: {
        count: references.length,
        types: [...new Set(references.map(r => r.sys.contentType.sys.id))],
        ids: references.map(r => r.sys.id),
      },
    },
  })
}

function extractReferences(entry: any): any[] {
  const refs: any[] = []

  Object.values(entry.fields).forEach((value: any) => {
    if (value?.sys?.type === 'Entry') {
      refs.push(value)
    } else if (Array.isArray(value)) {
      value.forEach(item => {
        if (item?.sys?.type === 'Entry') {
          refs.push(item)
        }
      })
    }
  })

  return refs
}

Asset Tracking

Track Contentful assets (images, videos, PDFs):

export function trackContentfulAsset(asset: any, action: string = 'view') {
  if (!window.dataLayer) return

  window.dataLayer.push({
    event: 'asset_interaction',
    contentful: {
      asset: {
        id: asset.sys.id,
        type: asset.fields.file.contentType,
        url: asset.fields.file.url,
        size: asset.fields.file.details.size,
        fileName: asset.fields.file.fileName,
        action: action, // 'view', 'download', 'play'
      },
    },
  })
}

// Usage example
export function DownloadButton({ asset }) {
  const handleDownload = () => {
    trackContentfulAsset(asset, 'download')
    // Trigger download...
  }

  return <button
}

Rich Text Content Metrics

Track rich text field metrics:

import { documentToPlainTextString } from '@contentful/rich-text-plain-text-renderer'
import { Document } from '@contentful/rich-text-types'

export function pushRichTextMetrics(richTextField: Document, contentId: string) {
  if (!window.dataLayer) return

  const plainText = documentToPlainTextString(richTextField)
  const wordCount = plainText.split(/\s+/).filter(Boolean).length
  const readingTimeMinutes = Math.ceil(wordCount / 200)

  // Count embedded assets and entries
  const embeddedAssets = richTextField.content.filter(
    node => node.nodeType === 'embedded-asset-block'
  ).length

  const embeddedEntries = richTextField.content.filter(
    node => node.nodeType === 'embedded-entry-block'
  ).length

  window.dataLayer.push({
    event: 'content_metrics_calculated',
    contentful: {
      contentId: contentId,
      metrics: {
        wordCount: wordCount,
        readingTimeMinutes: readingTimeMinutes,
        embeddedAssets: embeddedAssets,
        embeddedEntries: embeddedEntries,
        hasImages: embeddedAssets > 0,
        hasEmbeddedContent: embeddedEntries > 0,
      },
    },
  })
}

Contentful Preview Mode Detection

Exclude preview mode from production tracking:

export function isContentfulPreviewMode(): boolean {
  if (typeof window === 'undefined') return false

  const urlParams = new URLSearchParams(window.location.search)

  return (
    urlParams.get('preview') === 'true' ||
    urlParams.get('contentful_preview') === 'true' ||
    document.cookie.includes('contentful_preview=true')
  )
}

// Initialize GTM only if not in preview
export function initializeGTMConditionally() {
  if (isContentfulPreviewMode()) {
    console.log('Preview mode detected - GTM disabled')
    return null
  }

  return <GoogleTagManager />
}

GTM Trigger Exception:

Create a trigger exception for preview mode:

  1. Create User-Defined Variable:

    • Name: URL - Preview Mode
    • Type: URL
    • Component Type: Query
    • Query Key: preview
  2. Add exception to all triggers:

    • Exception: URL - Preview Mode equals true

Webhook-Triggered Events

Push webhook data to data layer for real-time updates:

// api/contentful-webhook.ts
export async function POST(req: Request) {
  const payload = await req.json()
  const topic = req.headers.get('x-contentful-topic')

  // Broadcast to connected clients via WebSocket or Server-Sent Events
  await broadcastToClients({
    event: 'contentful_webhook',
    topic: topic,
    data: {
      contentType: payload.sys.contentType?.sys.id,
      contentId: payload.sys.id,
      action: topic?.split('.').pop(), // 'publish', 'unpublish', 'delete'
    },
  })

  return Response.json({ success: true })
}

// Client-side listener
export function useContentfulWebhooks() {
  useEffect(() => {
    const eventSource = new EventSource('/api/contentful-stream')

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data)

      if (window.dataLayer) {
        window.dataLayer.push({
          event: 'contentful_realtime_update',
          contentful: {
            contentType: data.contentType,
            contentId: data.contentId,
            action: data.action,
            timestamp: new Date().toISOString(),
          },
        })
      }
    }

    return () => eventSource.close()
  }, [])
}

Content Type-Specific Data Layers

Different structures for different content types:

// For blog posts
export function pushBlogPostData(post: any) {
  window.dataLayer?.push({
    event: 'blog_post_view',
    contentful: {
      type: 'blogPost',
      id: post.sys.id,
      title: post.fields.title,
      category: post.fields.category,
      author: post.fields.author?.fields.name,
      publishDate: post.fields.publishDate,
      tags: post.fields.tags,
      featured: post.fields.featured || false,
      commentCount: post.fields.commentCount || 0,
    },
  })
}

// For products
export function pushProductData(product: any) {
  window.dataLayer?.push({
    event: 'view_item',
    ecommerce: {
      currency: 'USD',
      value: product.fields.price,
      items: [{
        item_id: product.sys.id,
        item_name: product.fields.name,
        item_category: product.fields.category,
        price: product.fields.price,
        item_brand: product.fields.brand,
      }],
    },
    contentful: {
      type: 'product',
      id: product.sys.id,
      inStock: product.fields.inStock,
      sku: product.fields.sku,
    },
  })
}

// For landing pages
export function pushLandingPageData(page: any) {
  window.dataLayer?.push({
    event: 'landing_page_view',
    contentful: {
      type: 'landingPage',
      id: page.sys.id,
      template: page.fields.template,
      campaign: page.fields.campaignId,
      variant: page.fields.variant,
      testGroup: page.fields.abTestGroup,
    },
  })
}

Advanced GTM Setup for Contentful

1. Environment-Based Container Loading

Use different GTM containers for different Contentful environments:

const getGTMContainerId = () => {
  const contentfulEnv = process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT

  const envMap: Record<string, string> = {
    'master': process.env.NEXT_PUBLIC_GTM_PRODUCTION!,
    'staging': process.env.NEXT_PUBLIC_GTM_STAGING!,
    'development': process.env.NEXT_PUBLIC_GTM_DEV!,
  }

  return envMap[contentfulEnv] || null
}

export function ConditionalGTM() {
  const gtmId = getGTMContainerId()

  if (!gtmId) {
    console.log('No GTM container for this environment')
    return null
  }

  return <GoogleTagManager gtmId={gtmId} />
}

2. Content Delivery API Performance Tracking

Monitor Contentful API performance via GTM:

export function trackContentfulAPICall(
  queryType: string,
  duration: number,
  entryCount: number
) {
  window.dataLayer?.push({
    event: 'contentful_api_performance',
    api: {
      type: queryType, // 'REST', 'GraphQL'
      duration_ms: Math.round(duration),
      entries_fetched: entryCount,
      cache_status: duration < 100 ? 'hit' : 'miss',
    },
  })
}

// Usage
const startTime = performance.now()
const entries = await client.getEntries({ content_type: 'blogPost' })
trackContentfulAPICall('REST', performance.now() - startTime, entries.items.length)

3. A/B Testing with Contentful

Track content variants for A/B testing:

export function trackContentVariant(entry: any, variantId: string) {
  window.dataLayer?.push({
    event: 'content_variant_view',
    contentful: {
      contentId: entry.sys.id,
      variantId: variantId,
      experimentId: entry.fields.experimentId,
    },
    experiment: {
      id: entry.fields.experimentId,
      variant: variantId,
    },
  })
}

GTM Tag Configuration:

Create a GA4 Event tag:

  • Event name: content_variant_view
  • Parameters:
    • experiment_id: \{\{Contentful - Experiment ID\}\}
    • variant_id: \{\{Contentful - Variant ID\}\}

Next Steps

For general GTM concepts, see Google Tag Manager Overview.