Install Google Analytics 4 with DatoCMS | OpsBlu Docs

Install Google Analytics 4 with DatoCMS

How to install GA4 on websites and applications powered by DatoCMS headless CMS using various frontend frameworks.

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

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,
      })
    }
  })
}

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 → ReportsRealtime
  • 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 AdminDebugView
  • 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.com or analytics.google.com
  • Verify collect requests are sent
  • Check request payload for correct parameters

Troubleshooting

For common issues and solutions, see:

Next Steps

For general GA4 concepts, see Google Analytics 4 Guide.