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:
- In GTM, go to Tags → New
- Click Tag Configuration → Google Analytics: GA4 Configuration
- Enter your Measurement ID (G-XXXXXXXXXX)
- Triggering: Select All Pages
- Save and name it "GA4 - Configuration"
GA4 Page View Tag:
- Tags → New
- Tag Configuration → Google Analytics: GA4 Event
- Configuration Tag: Select your GA4 Configuration tag
- Event Name:
page_view - Triggering: Custom Event trigger for
pageview(if tracking SPAs) - 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:
- Create variable: URL contains
preview=true - 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
- In GTM, click Preview
- Enter your site URL
- Navigate your Contentful-powered site
- 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
gtmorgoogle-analytics - Verify requests are sent
- Check request payloads
Common GTM Tags for Contentful
Meta Pixel Tag
- Tags → New
- Custom HTML tag
- 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>
- Triggering: All Pages
- Save
LinkedIn Insight Tag
- Tags → New
- Custom HTML
- Add LinkedIn tracking code
- Triggering: All Pages
Custom Event Tag
Track Contentful-specific events:
- Tags → New
- Google Analytics: GA4 Event
- Event Name:
content_view - Event Parameters:
content_type:\{\{DLV - Content Type\}\}content_id:\{\{DLV - Content ID\}\}title:\{\{DLV - Content Title\}\}
- 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.contentTypeequalsblogPost(or your content type)
3. Content Category Trigger
- Type: Custom Event
- Event name:
content_view - Fire on: Some custom events
- Condition:
contentful.categoryequalsnews(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 Locale→contentful.currentLocaleContentful - Available Locales→contentful.availableLocalesContentful - Default Locale→contentful.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:
Create User-Defined Variable:
- Name:
URL - Preview Mode - Type: URL
- Component Type: Query
- Query Key:
preview
- Name:
Add exception to all triggers:
- Exception:
URL - Preview Modeequalstrue
- Exception:
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
- Configure Data Layer - Set up custom data layer for Contentful
- Install Meta Pixel - Add Meta Pixel via GTM
- Troubleshoot Tracking - Debug GTM issues
For general GTM concepts, see Google Tag Manager Overview.