Install Google Tag Manager with Sanity | Blue Frog Docs

Install Google Tag Manager with Sanity

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

Install Google Tag Manager with Sanity

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

Why Use GTM with Sanity?

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 Sanity 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: 'sanity',
      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 Sanity sites.

1. Install Plugin

npm install gatsby-plugin-google-tagmanager

2. Configure in gatsby-config.js

module.exports = {
  plugins: [
    {
      resolve: 'gatsby-source-sanity',
      options: {
        projectId: process.env.SANITY_PROJECT_ID,
        dataset: process.env.SANITY_DATASET,
      },
    },
    {
      resolve: 'gatsby-plugin-google-tagmanager',
      options: {
        id: process.env.GTM_ID,

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

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

        // 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 onRenderBody = ({ 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 onRouteUpdate = ({ location }) => {
  if (typeof window !== 'undefined' && window.dataLayer) {
    window.dataLayer.push({
      event: 'pageview',
      page: location.pathname,
    })
  }
}

Method 4: Nuxt.js

For Vue developers using Sanity.

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 Sanity Data

See GTM Data Layer for Sanity 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)
  • Content Author (Data Layer Variable: content.author)
  • Content Tags (Data Layer Variable: content.tags)
  • Sanity Dataset (Data Layer Variable: sanity.dataset)
  • Content Revision (Data Layer Variable: content.revision)

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: Sanity content engagement

Scroll Depth Trigger:

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

Data Layer Setup with Sanity Content

Basic Data Layer Structure

Push Sanity content metadata to GTM data layer:

'use client'

import { useEffect } from 'react'

export function SanityDataLayer({ document }: { document: any }) {
  useEffect(() => {
    if (typeof window === 'undefined') return

    window.dataLayer = window.dataLayer || []
    window.dataLayer.push({
      event: 'sanity_content_loaded',
      content: {
        type: document._type,
        id: document._id,
        title: document.title,
        slug: document.slug?.current,
        category: document.category?.title || null,
        author: document.author?.name || null,
        tags: document.tags?.map(t => t.title) || [],
        publishedAt: document.publishedAt || null,
        updatedAt: document._updatedAt,
        revision: document._rev,
      },
      sanity: {
        projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
        dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
        apiVersion: '2024-01-01',
      }
    })
  }, [document])

  return null
}

Portable Text Tracking

Track user engagement with Portable Text content blocks:

1. Track Portable Text Block Views:

import { PortableText, PortableTextComponents } from '@portabletext/react'
import { useEffect, useRef } from 'react'

const components: PortableTextComponents = {
  block: {
    h2: ({ children, value }) => {
      const ref = useRef<HTMLHeadingElement>(null)

      useEffect(() => {
        const observer = new IntersectionObserver(
          (entries) => {
            entries.forEach(entry => {
              if (entry.isIntersecting && window.dataLayer) {
                window.dataLayer.push({
                  event: 'portable_text_heading_view',
                  headingText: children,
                  blockKey: value._key,
                  blockType: value._type,
                })
              }
            })
          },
          { threshold: 0.5 }
        )

        if (ref.current) {
          observer.observe(ref.current)
        }

        return () => observer.disconnect()
      }, [])

      return <h2 ref={ref}>{children}</h2>
    },
  },

  types: {
    image: ({ value }) => {
      const ref = useRef<HTMLDivElement>(null)

      useEffect(() => {
        const observer = new IntersectionObserver(
          (entries) => {
            entries.forEach(entry => {
              if (entry.isIntersecting && window.dataLayer) {
                window.dataLayer.push({
                  event: 'portable_text_image_view',
                  imageAlt: value.alt,
                  blockKey: value._key,
                })
              }
            })
          },
          { threshold: 0.5 }
        )

        if (ref.current) {
          observer.observe(ref.current)
        }

        return () => observer.disconnect()
      }, [])

      return (
        <div ref={ref}>
          <img src={value.asset.url} alt={value.alt} />
        </div>
      )
    },

    videoEmbed: ({ value }) => {
      return (
        <div
          onClick={() => {
            window.dataLayer?.push({
              event: 'portable_text_video_click',
              videoUrl: value.url,
              videoProvider: value.provider,
              blockKey: value._key,
            })
          }}
        >
          {/* Video embed component */}
        </div>
      )
    },

    ctaBlock: ({ value }) => {
      return (
        <div
          onClick={() => {
            window.dataLayer?.push({
              event: 'portable_text_cta_click',
              ctaText: value.text,
              ctaLink: value.link,
              blockKey: value._key,
            })
          }}
        >
          {/* CTA component */}
        </div>
      )
    },
  },

  marks: {
    link: ({ value, children }) => {
      return (
        <a
          href={value.href}
          onClick={() => {
            window.dataLayer?.push({
              event: 'portable_text_link_click',
              linkUrl: value.href,
              linkText: children,
              linkType: value.href.startsWith('http') ? 'external' : 'internal',
            })
          }}
        >
          {children}
        </a>
      )
    },
  },
}

export function TrackedPortableText({ value }: { value: any }) {
  return <PortableText value={value} components={components} />
}

2. Create GTM Tags for Portable Text Events:

In GTM, create tags that fire on these custom events:

Heading View Tag:

  • Trigger: Custom Event = portable_text_heading_view
  • Tag Type: GA4 Event
  • Event Name: content_heading_view
  • Parameters:
    • heading_text: {{DLV - Heading Text}}
    • block_key: {{DLV - Block Key}}

Image View Tag:

  • Trigger: Custom Event = portable_text_image_view
  • Tag Type: GA4 Event
  • Event Name: content_image_view

Link Click Tag:

  • Trigger: Custom Event = portable_text_link_click
  • Tag Type: GA4 Event
  • Event Name: content_link_click
  • Parameters:
    • link_url: {{DLV - Link URL}}
    • link_type: {{DLV - Link Type}}

Image Asset Tracking

Track Sanity image asset performance:

import Image from 'next/image'
import { urlFor } from '@/lib/sanity.image'

export function TrackedSanityImage({ image, alt, priority = false }) {
  const imageUrl = urlFor(image).width(1200).url()
  const assetId = image.asset._ref

  return (
    <Image
      src={imageUrl}
      alt={alt || image.alt}
      width={1200}
      height={675}
      priority={priority}
      onLoad={() => {
        // Track when image loads
        window.dataLayer?.push({
          event: 'sanity_image_loaded',
          imageAssetId: assetId,
          imageAlt: alt || image.alt,
          imageDimensions: image.asset.metadata?.dimensions,
        })
      }}
      onClick={() => {
        // Track image clicks
        window.dataLayer?.push({
          event: 'sanity_image_click',
          imageAssetId: assetId,
          imageAlt: alt || image.alt,
        })
      }}
    />
  )
}

Multi-Dataset Considerations

Track which Sanity dataset is being used:

1. Push Dataset to Data Layer:

// app/layout.tsx
'use client'

import { useEffect } from 'react'

export function DatasetTracker() {
  useEffect(() => {
    const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET

    window.dataLayer = window.dataLayer || []
    window.dataLayer.push({
      event: 'sanity_dataset_loaded',
      sanity: {
        dataset: dataset,
        projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
        environment: dataset === 'production' ? 'production' : 'staging',
      }
    })
  }, [])

  return null
}

2. Create GTM Variable for Dataset:

In GTM:

  • Variable Type: Data Layer Variable
  • Data Layer Variable Name: sanity.dataset
  • Default Value: unknown

3. Conditional Tags Based on Dataset:

Only fire production tags when dataset is production:

Production-Only Tag:

  • Trigger: All Pages
  • Exception: {{Sanity Dataset}} does not equal production

Staging Tag (Different GA4 Property):

  • Trigger: All Pages
  • Fires on: {{Sanity Dataset}} equals staging
  • GA4 Measurement ID: Your staging property ID

4. Multi-Dataset Event Tracking:

export function trackEvent(eventName: string, params: any) {
  const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET

  window.dataLayer?.push({
    event: eventName,
    ...params,
    sanity_dataset: dataset,
    environment: dataset === 'production' ? 'production' : 'non-production',
  })
}

// Usage
trackEvent('content_view', {
  content_type: 'post',
  content_id: '123',
})

GROQ-Enhanced Data Layer

Use GROQ queries to enrich the data layer:

import { client } from '@/lib/sanity.client'

export async function getEnrichedContent(slug: string) {
  const query = `
    *[_type == "post" && slug.current == $slug][0] {
      _id,
      _type,
      _rev,
      title,
      slug,
      publishedAt,

      // Author details
      "author": author-> {
        name,
        _id,
        "postCount": count(*[_type == "post" && references(^._id)])
      },

      // Categories
      "categories": categories[]-> {
        title,
        _id,
        "postCount": count(*[_type == "post" && references(^._id)])
      },

      // Related posts
      "relatedPosts": *[_type == "post" && count((categories[]._ref)[@ in ^.categories[]._ref]) > 0 && _id != ^._id][0...3] {
        title,
        slug
      },

      // Content metadata
      "metadata": {
        "wordCount": length(pt::text(body)),
        "estimatedReadTime": round(length(pt::text(body)) / 200),
        "imageCount": count(body[_type == "image"]),
        "videoCount": count(body[_type == "videoEmbed"]),
        "hasCallToAction": defined(body[_type == "ctaBlock"][0])
      }
    }
  `

  const content = await client.fetch(query, { slug })

  return content
}

// Push enriched data to data layer
export function pushEnrichedDataLayer(content: any) {
  window.dataLayer?.push({
    event: 'content_loaded',
    content: {
      id: content._id,
      type: content._type,
      title: content.title,
      slug: content.slug.current,
      author: content.author?.name,
      authorId: content.author?._id,
      authorPostCount: content.author?.postCount,
      categories: content.categories?.map(c => c.title) || [],
      categoryPostCounts: content.categories?.map(c => c.postCount) || [],
      relatedPostCount: content.relatedPosts?.length || 0,
      wordCount: content.metadata?.wordCount || 0,
      readTime: content.metadata?.estimatedReadTime || 0,
      hasImages: (content.metadata?.imageCount || 0) > 0,
      hasVideos: (content.metadata?.videoCount || 0) > 0,
      hasCTA: content.metadata?.hasCallToAction || false,
    }
  })
}

Advanced Configuration

Exclude Sanity Studio Sessions

Don't track Sanity Studio sessions:

// Check for Sanity Studio
const isSanityStudio = window.location.pathname.startsWith('/studio')

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

Or use GTM trigger exception:

  1. Create variable: URL starts with /studio
  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 Sanity-powered site
  4. Verify:
    • GTM container loads
    • Tags fire correctly
    • Variables populate with Sanity 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 Sanity

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

Custom Event Tag

Track Sanity-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: 'post',
  contentId: '123'
})

// Bad (too much data)
dataLayer.push({
  event: 'content_view',
  entireSanityDocument: {...}  // 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.

Next Steps

For general GTM concepts, see Google Tag Manager Guide.

// SYS.FOOTER