Since DatoCMS is a headless CMS, GA4 is installed in your frontend application, not in DatoCMS itself. The implementation method depends on your framework (Next.js, Gatsby, Nuxt, React, etc.).
Before You Begin
Prerequisites:
- Active GA4 property with Measurement ID (format:
G-XXXXXXXXXX) - DatoCMS content integrated into your frontend application
- Developer access to your frontend codebase
- Understanding of your framework's structure
Important: DatoCMS only stores and delivers content via GraphQL API. All analytics code lives in your frontend framework.
Implementation by Framework
Method 1: Next.js (App Router) - Recommended for New Projects
Next.js 13+ with App Router is the recommended approach for modern DatoCMS sites.
Setup Steps
1. Create Analytics Component
Create app/components/Analytics.tsx:
'use client'
import Script from 'next/script'
export function Analytics() {
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
if (!GA_MEASUREMENT_ID) {
console.warn('GA4 Measurement ID not found')
return null
}
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_MEASUREMENT_ID}', {
page_path: window.location.pathname,
});
`}
</Script>
</>
)
}
2. Add to Root Layout
Update app/layout.tsx:
import { Analytics } from './components/Analytics'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{children}
<Analytics />
</body>
</html>
)
}
3. Track Route Changes
Create app/components/PageViewTracker.tsx:
'use client'
import { usePathname, useSearchParams } from 'next/navigation'
import { useEffect } from 'react'
export function PageViewTracker() {
const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
if (typeof window !== 'undefined' && window.gtag) {
const url = pathname + (searchParams?.toString() ? `?${searchParams.toString()}` : '')
window.gtag('config', process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID!, {
page_path: url,
})
}
}, [pathname, searchParams])
return null
}
Add to root layout:
import { PageViewTracker } from './components/PageViewTracker'
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<PageViewTracker />
</body>
</html>
)
}
4. Set Environment Variables
Create .env.local:
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
NEXT_PUBLIC_DATOCMS_API_TOKEN=your-datocms-token
5. Track DatoCMS Content Views
In your DatoCMS content pages:
// app/blog/[slug]/page.tsx
import { performRequest } from '@/lib/datocms'
const BLOG_POST_QUERY = `
query BlogPost($slug: String!) {
blogPost(filter: { slug: { eq: $slug } }) {
id
title
slug
_modelApiKey
author {
name
}
category {
title
}
}
}
`
export default async function BlogPost({ params }) {
const { data } = await performRequest({
query: BLOG_POST_QUERY,
variables: { slug: params.slug },
})
return (
<div>
<DatoContentTracker
contentId={data.blogPost.id}
contentType={data.blogPost._modelApiKey}
title={data.blogPost.title}
category={data.blogPost.category?.title}
/>
<article>{/* Content */}</article>
</div>
)
}
// components/DatoContentTracker.tsx
'use client'
import { useEffect } from 'react'
export function DatoContentTracker({ contentId, contentType, title, category }) {
useEffect(() => {
if (window.gtag) {
window.gtag('event', 'datocms_content_view', {
content_type: contentType,
content_id: contentId,
title: title,
category: category,
})
}
}, [contentId, contentType, title, category])
return null
}
Method 2: Next.js (Pages Router)
For Next.js projects using the Pages Router.
1. Create Custom Document
Create or update pages/_document.tsx:
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
return (
<Html>
<Head>
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
/>
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_MEASUREMENT_ID}', {
page_path: window.location.pathname,
});
`,
}}
/>
</Head>
<body>
<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) => {
if (window.gtag) {
window.gtag('config', process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID!, {
page_path: url,
})
}
}
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])
return <Component {...pageProps} />
}
3. Fetch DatoCMS Content with SSG
// pages/blog/[slug].tsx
import { request } from '@/lib/datocms'
export async function getStaticProps({ params }) {
const data = await request({
query: `
query BlogPost($slug: String!) {
blogPost(filter: { slug: { eq: $slug } }) {
id
title
slug
_modelApiKey
}
}
`,
variables: { slug: params.slug },
})
return {
props: { post: data.blogPost },
revalidate: 3600, // ISR: Rebuild every hour
}
}
export async function getStaticPaths() {
const data = await request({
query: `{ allBlogPosts { slug } }`,
})
return {
paths: data.allBlogPosts.map(post => ({ params: { slug: post.slug } })),
fallback: 'blocking',
}
}
Method 3: Gatsby
Perfect for static DatoCMS sites with excellent build-time optimization.
1. Install Plugin
npm install gatsby-plugin-google-gtag gatsby-source-datocms
2. Configure Plugin
Update gatsby-config.js:
module.exports = {
plugins: [
{
resolve: 'gatsby-source-datocms',
options: {
apiToken: process.env.DATOCMS_API_TOKEN,
preview: false,
disableLiveReload: false,
},
},
{
resolve: `gatsby-plugin-google-gtag`,
options: {
trackingIds: [
process.env.GA_MEASUREMENT_ID, // Google Analytics
],
gtagConfig: {
anonymize_ip: true,
cookie_expires: 0,
},
pluginConfig: {
head: false,
respectDNT: true,
},
},
},
],
}
3. Set Environment Variables
Create .env.production:
GA_MEASUREMENT_ID=G-XXXXXXXXXX
DATOCMS_API_TOKEN=your-datocms-token
4. Track DatoCMS Content
In your DatoCMS page template:
// src/templates/blog-post.js
import React, { useEffect } from 'react'
import { graphql } from 'gatsby'
const BlogPostTemplate = ({ data }) => {
const post = data.datoCmsBlogPost
useEffect(() => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'datocms_content_view', {
content_type: 'blog_post',
content_id: post.originalId,
title: post.title,
category: post.category?.title,
author: post.author?.name,
})
}
}, [post])
return (
<article>
<h1>{post.title}</h1>
{/* Content */}
</article>
)
}
export const query = graphql`
query BlogPost($slug: String!) {
datoCmsBlogPost(slug: { eq: $slug }) {
originalId
title
slug
category {
title
}
author {
name
}
}
}
`
export default BlogPostTemplate
Method 4: Nuxt.js
Ideal for Vue developers building DatoCMS sites.
1. Install Module
npm install @nuxtjs/google-analytics
2. Configure in nuxt.config.js
export default {
modules: [
[
'@nuxtjs/google-analytics',
{
id: process.env.GA_MEASUREMENT_ID,
},
],
],
// Or for Nuxt 3
buildModules: [
['@nuxtjs/google-analytics', {
id: process.env.GA_MEASUREMENT_ID,
debug: {
enabled: process.env.NODE_ENV !== 'production',
sendHitTask: process.env.NODE_ENV === 'production',
},
}],
],
publicRuntimeConfig: {
gaMeasurementId: process.env.GA_MEASUREMENT_ID,
datocmsToken: process.env.DATOCMS_API_TOKEN,
},
}
3. Track DatoCMS Content
<template>
<article>
<h1>{{ post.title }}</h1>
<!-- Content -->
</article>
</template>
<script>
export default {
async asyncData({ $config }) {
const response = await fetch('https://graphql.datocms.com/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${$config.datocmsToken}`,
},
body: JSON.stringify({
query: `
query BlogPost($slug: String!) {
blogPost(filter: { slug: { eq: $slug } }) {
id
title
slug
}
}
`,
variables: { slug: params.slug }
}),
})
const { data } = await response.json()
return { post: data.blogPost }
},
mounted() {
if (this.$ga) {
this.$ga.event('datocms_content_view', {
content_type: 'blog_post',
content_id: this.post.id,
title: this.post.title,
})
}
},
}
</script>
Method 5: React SPA (Vite, Create React App)
For single-page applications consuming DatoCMS.
1. Add to index.html
Update public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
2. Track Route Changes with React Router
// App.tsx
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
function App() {
const location = useLocation()
useEffect(() => {
if (window.gtag) {
window.gtag('config', 'G-XXXXXXXXXX', {
page_path: location.pathname + location.search,
})
}
}, [location])
return <Router>{/* Routes */}</Router>
}
3. Use Environment Variables
Create .env:
VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX
VITE_DATOCMS_API_TOKEN=your-datocms-token
# or for CRA
REACT_APP_GA_MEASUREMENT_ID=G-XXXXXXXXXX
REACT_APP_DATOCMS_API_TOKEN=your-datocms-token
Advanced Configuration
Content-Specific Tracking
Track which DatoCMS content types perform best:
export function trackDatoCMSContent(record: any) {
if (!window.gtag) return
window.gtag('event', 'datocms_content_view', {
content_type: record._modelApiKey,
content_id: record.id,
title: record.title,
slug: record.slug,
published_at: record._publishedAt,
updated_at: record._updatedAt,
locale: record._locales?.[0] || 'en',
})
}
Preview Mode Exclusion
Don't track DatoCMS preview sessions:
const isPreview = searchParams.get('preview') === 'true' ||
searchParams.get('datocms_preview') === 'true'
if (!isPreview && GA_MEASUREMENT_ID) {
// Initialize GA4
}
Multi-Environment Setup
Different analytics properties for each environment:
const GA_MEASUREMENT_ID =
process.env.VERCEL_ENV === 'production'
? process.env.NEXT_PUBLIC_GA_PRODUCTION_ID
: process.env.VERCEL_ENV === 'preview'
? process.env.NEXT_PUBLIC_GA_STAGING_ID
: process.env.NEXT_PUBLIC_GA_DEV_ID
// Track environment in events
gtag('set', {
'custom_map': {
'dimension1': 'environment',
'dimension2': 'datocms_env'
}
})
gtag('event', 'page_view', {
'environment': process.env.VERCEL_ENV,
'datocms_env': process.env.NEXT_PUBLIC_DATOCMS_ENVIRONMENT
})
Track Modular Content Blocks
Track DatoCMS structured text blocks:
// Track structured text engagement
function trackStructuredTextBlocks(content: any, postId: string) {
content.blocks?.forEach((block: any, index: number) => {
if (window.gtag) {
window.gtag('event', 'content_block_view', {
block_type: block._modelApiKey,
block_id: block.id,
block_position: index,
parent_content_id: postId,
})
}
})
}
Consent Mode Implementation
Implement Google Consent Mode for GDPR/CCPA compliance:
// Initialize with denied consent
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'wait_for_update': 500
})
// Update after user consent
function handleConsentUpdate(analyticsConsent: boolean) {
gtag('consent', 'update', {
'analytics_storage': analyticsConsent ? 'granted' : 'denied'
})
}
TypeScript Support
Add type definitions for gtag:
// types/gtag.d.ts
declare global {
interface Window {
gtag: (
command: 'config' | 'event' | 'set' | 'consent',
targetId: string,
config?: Record<string, any>
) => void
dataLayer: any[]
}
}
export {}
Testing & Verification
1. Check Real-Time Reports
- Open GA4 → Reports → Realtime
- Navigate your DatoCMS-powered site
- Verify page views appear within 30 seconds
2. Use GA4 DebugView
Enable debug mode:
gtag('config', GA_MEASUREMENT_ID, {
'debug_mode': true
})
View in GA4:
- Go to Admin → DebugView
- See events with full parameters in real-time
3. Browser Console Testing
// Check if gtag is loaded
console.log(typeof window.gtag) // should be 'function'
// View data layer
console.table(window.dataLayer)
// Test event
window.gtag('event', 'test_event', { test: 'value' })
4. Network Tab Verification
Open Chrome DevTools → Network:
- Filter by
google-analytics.comoranalytics.google.com - Verify
collectrequests are sent - Check request payload for correct parameters
Troubleshooting
For common issues and solutions, see:
- Events Not Firing - Debug tracking issues
- Performance Issues - Fix slow loading
Next Steps
- Configure GA4 Events - Track custom events
- Set up GTM - For easier tag management
- Optimize Performance - Improve Core Web Vitals
For general GA4 concepts, see Google Analytics 4 Guide.