Google Tag Manager

Integrate Google Tag Manager with Strapi to manage tracking codes across your CMS-powered sites. Deploy marketing tags, conversion pixels, and analytics scripts without code changes, giving marketers control over measurement while developers focus on content infrastructure

Google Tag Manager logo

What Is Google Tag Manager?

Google Tag Manager is a container-based tag management system that lets you deploy and manage tracking scripts through a centralized web interface. Rather than hardcoding multiple analytics platforms, advertising pixels, or A/B testing tools directly into your application, GTM provides a single JavaScript snippet that dynamically loads all your tracking implementations.

The architecture consists of four core components: containers hold your tag configurations, tags are the actual tracking scripts that execute, triggers define when tags should fire, and variables store dynamic data used by tags and triggers. The data layer—a JavaScript object that acts as an intermediary between your application and GTM—represents the most critical piece for headless CMS integrations.

For developers working with Strapi's headless CMS architecture, GTM's container approach eliminates the need for CMS-level analytics plugins. You implement tracking logic entirely in your presentation layer while Strapi handles content delivery through its REST and GraphQL APIs.

Why Integrate Google Tag Manager with Strapi

The architecture of GTM integration with Strapi stems from the fundamental nature of headless CMS design rather than tight coupling considerations. Unlike traditional monolithic CMS platforms where analytics plugins can live within the admin panel, headless CMS architectures like Strapi separate content delivery from presentation, which means analytics implementation necessarily occurs at the frontend layer rather than within the CMS itself.

Clean Separation of Concerns

Strapi handles content modeling and API delivery while GTM manages analytics deployment at the frontend layer. This architectural separation means content model changes in Strapi don't impact analytics configuration, and deploying new tracking tags through GTM doesn't require touching your application code.

Framework-Level Performance Optimization

Modern JavaScript frameworks provide optimized GTM loading mechanisms that defer script execution until after page hydration. According to the Next.js Third-Party Integrations documentation, the @next/third-parties package optimizes GTM loading by deferring script execution until after page hydration, reducing impact on Core Web Vitals metrics. These framework-specific optimizations preserve the performance benefits of choosing a JAMstack architecture while enabling comprehensive analytics tracking.

Flexible Data Layer Architecture

The data layer provides a CMS-agnostic interface between Strapi's content API responses and analytics platforms. This pattern works across Strapi versions without modification, future-proofing your analytics implementation against CMS upgrades.

Developer Workflow Efficiency

Marketing teams can manage analytics implementations through GTM's interface without requiring code changes, while content teams can create new content types through Strapi's admin panel. Developers focus on maintaining consistent data layer patterns between the frontend and Strapi's API responses.

How to Integrate Google Tag Manager with Strapi

GTM integration follows a frontend-centric pattern where Strapi serves content via API and your chosen framework handles analytics implementation. The specific approach varies by framework, but the architectural principle remains constant: initialize GTM in your frontend application, then push Strapi content metadata to the data layer.

Get Your GTM Container ID

First, create a GTM account and container at tagmanager.google.com. Select "Web" as the container type. You'll receive a container ID in the format GTM-XXXXXXX. Keep this ID available—you'll need it for all framework implementations.

Next.js Implementation

Next.js offers the most streamlined integration through the official @next/third-parties package, which handles script loading optimization automatically. The @next/third-parties package provides a GoogleTagManager component specifically designed for Next.js applications, optimizing GTM loading by deferring script execution until after page hydration.

Install the package:

npm install @next/third-parties

For App Router applications (Next.js 13+), add the GoogleTagManager component to your root layout:

// app/layout.tsx
import { GoogleTagManager } from '@next/third-parties/google'

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

Configure the data layer to pass Strapi content metadata when users view articles:

// app/blog/[slug]/page.tsx
'use client'
import { useEffect } from 'react'

export default function ArticlePage({ article }) {
  useEffect(() => {
    if (typeof window !== 'undefined') {
      window.dataLayer = window.dataLayer || []
      window.dataLayer.push({
        event: 'article_view',
        article: {
          id: article.id,
          title: article.attributes.title,
          category: article.attributes.category?.data?.attributes?.name,
          author: article.attributes.author?.data?.attributes?.name,
          tags: article.attributes.tags?.data?.map(tag => tag.attributes.name),
          publishedAt: article.attributes.publishedAt
        }
      })
    }
  }, [article])
  
  return (
    <article>
      <h1>{article.attributes.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.attributes.content }} />
    </article>
  )
}

The useEffect hook ensures the data layer push happens client-side after the component mounts, preventing server-side execution issues and hydration mismatches in SSR applications.

Nuxt.js Implementation

Nuxt requires a manual GTM implementation through a client-side plugin using Nuxt's plugin system. Start by storing your GTM container ID in environment variables:

# .env
NUXT_PUBLIC_GTM_ID=GTM-XXXXXXX

Create a client-side plugin to inject the GTM scripts:

// plugins/gtm.client.js
export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig()
  const gtmId = config.public.gtmId

  if (!gtmId) return

  window.dataLayer = window.dataLayer || []

  const script = document.createElement('script')
  script.innerHTML = `
    (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','${gtmId}');
  `
  document.head.appendChild(script)

  // Add noscript iframe for non-JavaScript browsers
  const noscript = document.createElement('noscript')
  const iframe = document.createElement('iframe')
  iframe.src = `https://www.googletagmanager.com/ns.html?id=${gtmId}`
  iframe.height = '0'
  iframe.width = '0'
  iframe.style.display = 'none'
  iframe.style.visibility = 'hidden'
  noscript.appendChild(iframe)
  document.body.insertBefore(noscript, document.body.firstChild)
})

Integrate Strapi content data in your page components:

<!-- pages/articles/[slug].vue -->
<script setup>
const route = useRoute()
const { find } = useStrapi()

const { data: article } = await useAsyncData(
  'article',
  () => find('articles', {
    filters: { slug: route.params.slug },
    populate: '*'
  })
)

onMounted(() => {
  if (article.value?.data?.[0]) {
    const articleData = article.value.data[0]
    window.dataLayer = window.dataLayer || []
    window.dataLayer.push({
      event: 'article_loaded',
      content: {
        id: articleData.id,
        title: articleData.attributes.title,
        category: articleData.attributes.category?.data?.attributes?.name,
        author: articleData.attributes.author?.data?.attributes?.username,
        publishedAt: articleData.attributes.publishedAt
      }
    })
  }
})
</script>

React Implementation

React applications have two primary approaches for GTM integration: using the react-gtm-module package or implementing manual script injection through the index.html file.

For the package-based approach:

npm install react-gtm-module

Initialize GTM in your application entry point:

// src/index.js or src/App.js
import TagManager from 'react-gtm-module'

const tagManagerArgs = {
  gtmId: 'GTM-XXXXXXX'
}

TagManager.initialize(tagManagerArgs)

Push Strapi content to the data layer when components mount:

// src/components/Article.jsx
import { useEffect, useState } from 'react'

function Article({ slug }) {
  const [article, setArticle] = useState(null)

  useEffect(() => {
    fetch(`${process.env.REACT_APP_STRAPI_URL}/api/articles?filters[slug][$eq]=${slug}&populate=*`)
      .then(res => res.json())
      .then(data => {
        const articleData = data.data[0]
        setArticle(articleData)
        
        window.dataLayer = window.dataLayer || []
        window.dataLayer.push({
          event: 'article_view',
          article: {
            id: articleData.id,
            title: articleData.attributes.title,
            category: articleData.attributes.category?.data?.attributes?.name
          }
        })
      })
  }, [slug])

  return article ? <h1>{article.attributes.title}</h1> : null
}

Gatsby Implementation

Gatsby provides the most integrated GTM implementation through its plugin ecosystem, specifically through the official gatsby-plugin-google-tagmanager plugin.

npm install gatsby-plugin-google-tagmanager

Configure the plugin in your Gatsby config:

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: 'gatsby-plugin-google-tagmanager',
      options: {
        id: 'GTM-XXXXXXX',
        includeInDevelopment: false,
        defaultDataLayer: { platform: 'gatsby' },
      },
    },
    {
      resolve: 'gatsby-source-strapi',
      options: {
        apiURL: process.env.STRAPI_URL || 'http://localhost:1337',
        collectionTypes: ['article', 'category', 'author'],
        queryLimit: 1000,
      },
    },
  ],
}

Push Strapi content data in your templates:

// src/templates/article.js
import React, { useEffect } from 'react'
import { graphql } from 'gatsby'

const ArticleTemplate = ({ data }) => {
  const article = data.strapiArticle

  useEffect(() => {
    if (typeof window !== 'undefined') {
      window.dataLayer = window.dataLayer || []
      window.dataLayer.push({
        event: 'article_loaded',
        article: {
          id: article.strapiId,
          title: article.title,
          category: article.category?.name,
          author: article.author?.name,
          publishedAt: article.publishedAt,
          tags: article.tags?.map(tag => tag.name)
        }
      })
    }
  }, [article])

  return (
    <article>
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.content }} />
    </article>
  )
}

export const query = graphql`
  query ArticleQuery($slug: String!) {
    strapiArticle(slug: { eq: $slug }) {
      strapiId
      title
      content
      publishedAt
      category {
        name
      }
      author {
        name
      }
      tags {
        name
      }
    }
  }
`

export default ArticleTemplate

Data Layer Timing Requirements

The most common implementation mistake is pushing data layer events before GTM initializes or after GTM attempts to read them. Initialize the data layer before the GTM container snippet loads for static data, and use the dataLayer.push() method for dynamic events that occur after page load.

This timing issue represents a common cause of data layer errors in production environments, as GTM attempts to read variables that don't yet exist when events are pushed too late.

<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    'pageCategory': 'homepage',
    'userType': 'visitor'
  });
</script>
<!-- GTM container snippet loads after initialization -->

For dynamic events that occur after page load—like Strapi content loading via API—use dataLayer.push() in response to those events. The data layer persists across single-page application route changes, so you don't need to reinitialize it on navigation.

Configure GTM Triggers and Tags

After implementing the container code and data layer pushes in your frontend, you need to configure GTM to respond to those events:

  1. Open your GTM container at tagmanager.google.com
  2. Create a new trigger with type "Custom Event"
  3. Set the event name to match your frontend data layer events (e.g., article_view, article_loaded) that you've configured in your Strapi-consuming application (Next.js, React, Gatsby, or Nuxt)
  4. Create tags that fire on these custom event triggers
  5. Configure variables to capture data from the data layer using variables that match your Strapi content structure (e.g., {{article.title}}, {{article.category}}), ensuring exact case-sensitive matching between your dataLayer.push() calls and GTM variable names

Test your implementation using GTM's Preview mode before publishing. The preview console shows which tags fire, when they fire, and what data they receive from the data layer.

Project Example: Content Engagement Analytics Dashboard

Consider building a content engagement analytics system that tracks how users interact with blog posts powered by Strapi CMS. This project demonstrates how Strapi's structured content types, GTM's event tracking, and Google Analytics 4 work together to measure content performance across multiple dimensions.

Project Architecture

The system architecture separates concerns across distinct domains: Strapi serves as the headless CMS backend, providing content APIs for blog posts with custom content types for articles, categories, and authors. A Next.js frontend consumes this Strapi content and implements Google Tag Manager (GTM) through the @next/third-parties package, which handles asynchronous script loading.

The frontend application pushes Strapi content metadata to GTM's data layer (including article IDs, titles, categories, and authors), enabling GTM to capture these events. GTM then forwards tracking events to Google Analytics 4 to build engagement reports. This frontend-centric architecture maintains a clear separation between content management (Strapi's domain) and analytics implementation (frontend + GTM domain).

Strapi Content Model

Create a collection type for articles in Strapi's Content-Type Builder:

  • Title (Text): Article headline
  • Slug (UID): URL-friendly identifier
  • Content (Rich Text): Article body
  • ReadingTime (Number): Estimated reading minutes
  • Category (Relation): Many-to-one relationship with categories
  • Author (Relation): Many-to-one relationship with authors
  • Tags (Relation): Many-to-many relationship with tags
  • FeaturedImage (Media): Hero image

Configure API permissions to allow public access to the articles endpoint with population of related content.

Next.js Frontend with Enhanced Tracking

Extend the basic Next.js implementation to track article engagement metrics:

// app/blog/[slug]/page.tsx
'use client'
import { useEffect } from 'react'

export default function ArticleComponent({ article }) {
  // Track article view in GTM data layer
  useEffect(() => {
    if (article && typeof window !== 'undefined') {
      window.dataLayer = window.dataLayer || []
      window.dataLayer.push({
        event: 'article_view',
        article: {
          id: article.id,
          title: article.attributes.title,
          author: article.attributes.author?.data?.attributes?.name,
          category: article.attributes.category?.data?.attributes?.name,
          tags: article.attributes.tags?.data?.map(tag => tag.attributes.name),
          publishedAt: article.attributes.publishedAt,
          readingTime: article.attributes.readingTime
        }
      })
    }
  }, [article])

  return (
    <article>
      <h1>{article.attributes.title}</h1>
      <div className="metadata">
        <span>{article.attributes.author?.data?.attributes?.name}</span>
        <span>{article.attributes.readingTime} min read</span>
      </div>
      <div dangerouslySetInnerHTML={{ __html: article.attributes.content }} />
    </article>
  )
}

GTM Configuration for Content Analytics

In your GTM container, create these variables to capture article data:

Data Layer Variables for Strapi Content Tracking:

Based on Strapi's structured content model and GTM best practices, the data layer should include the following variables extracted from Strapi content attributes:

  • article.id → Variable name: Article ID
  • article.title → Variable name: Article Title
  • article.category → Variable name: Article Category
  • article.author → Variable name: Article Author
  • article.publishedAt → Variable name: Published Date
  • article.tags → Variable name: Content Tags

These variables represent the core metrics for content engagement analytics in Strapi-powered JAMstack applications, enabling developers to track both content metadata (sourced from Strapi API responses) and user interaction metrics (captured via GTM triggers and custom events).

Create triggers for each event type:

Article View Trigger:

  • Type: Custom Event
  • Event name: article_view

Create GA4 Event tags that fire on these triggers:

Article View Tag:

  • Tag type: Google Analytics: GA4 Event
  • Event name: view_item
  • Event parameters:
    • item_id: {{Article ID}}
    • item_name: {{Article Title}}
    • item_category: {{Article Category}}
    • item_brand: {{Article Author}}

Implementation Note: This event should be fired from your frontend application (Next.js, React, Gatsby, or Nuxt) when displaying content fetched from Strapi, not from within Strapi itself. GTM integration with Strapi occurs exclusively at the frontend layer. The data parameters should be populated from Strapi API responses using the dataLayer.push() method. Ensure the dataLayer is initialized before the GTM container loads to prevent undefined variable errors.

Strapi API Integration Pattern

Fetch populated Strapi content through the API for comprehensive tracking:

// lib/strapi.ts
export async function getArticleBySlug(slug: string) {
  const response = await fetch(
    `${process.env.NEXT_PUBLIC_STRAPI_URL}/api/articles?` +
    `filters[slug][$eq]=${slug}&` +
    `populate[category][fields][0]=name&` +
    `populate[author][fields][0]=name&` +
    `populate[author][fields][1]=email&` +
    `populate[tags][fields][0]=name&` +
    `populate[featuredImage][fields][0]=url`
  )
  
  const data = await response.json()
  return data.data[0]
}

This implementation demonstrates how frontend applications consuming Strapi's content API integrate with GTM's data layer to create rich analytics events that capture content structure and user behavior simultaneously.

Analytics Insights from Implementation

This architecture enables you to answer questions like:

  • Which blog categories generate the most engagement?
  • Which authors drive the longest read times?
  • What content types perform best with specific user segments?
  • How do different publication times affect content performance?

The separation between Strapi's content management (backend API layer) and GTM's analytics collection (frontend layer) means content teams can create new articles, categories, and tags through Strapi's admin panel without requiring code changes to the Strapi CMS itself.

However, tracking accuracy is maintained at the frontend application layer through the data layer, which maps Strapi content metadata to GTM events—this frontend-centric architecture represents a fundamental separation where Strapi handles content delivery while GTM tracking is configured entirely outside the CMS.

Strapi Open Office Hours

If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.

For more details, visit the Strapi documentation and Google Tag Manager documentation.

Frequently Answered Questions

Single-page applications don't trigger traditional page reloads when users navigate between routes, which means GTM's default page view triggers won't capture these route changes. You need to implement custom event tracking that fires on route changes.

According to Google's official single-page application measurement documentation, you should trigger virtual page views on each screen interaction to measure user journeys correctly in SPAs. For Next.js applications, listen to router.events to detect navigation, then push a custom pageview event to the data layer.

For React Router, add event listeners to route change events. Configure a History Change trigger in GTM to capture these custom events. The critical implementation detail is ensuring your data layer push happens after GTM container initialization and after route changes occur, so that GTM can properly detect and process the custom pageview events you're sending.

Initialize the data layer before the GTM container snippet loads if you're pushing static configuration data like user type or page category. For dynamic content from Strapi's API, use dataLayer.push() after the content loads in your component lifecycle. The most common timing error is pushing events after GTM tries to read them, resulting in undefined variables. In React or Next.js, implement data layer pushes inside useEffect hooks that depend on your Strapi content data. For server-side rendered applications, ensure GTM only initializes client-side to avoid hydration mismatches—use typeof window !== 'undefined' checks or framework-specific client-only rendering directives.

For Next.js App Router, import the GoogleTagManager component from @next/third-parties/google and add it to your root layout file (app/layout.tsx). Place it in the <html> element alongside your children to ensure the container loads once and persists across client-side navigation. This approach optimizes GTM loading by deferring script execution until after page hydration, reducing impact on Core Web Vitals metrics. For other frameworks or custom implementations requiring manual GTM setup, follow Google's official GTM installation documentation to add the container script in the appropriate location for your application architecture.

Start with GTM's Preview mode by clicking the Preview button in your container workspace. This opens a debug console showing every tag, trigger, and variable in real-time. Check the Variables tab to confirm that your data layer variables match exactly (case-sensitive) the keys you're pushing from your Strapi frontend code. Use your browser's developer tools to inspect window.dataLayer directly—type dataLayer in the console to see all pushed events. Verify that your custom event names in GTM triggers match the event names you're pushing (article_view, content_loaded, etc.). Common issues include: pushing events before GTM initializes, typos in variable names, triggers configured for the wrong event type, or containers that haven't been published (saved drafts don't deploy to your site).

Yes, this is a common pattern for headless e-commerce implementations using Strapi. Create a product content type in Strapi with fields for name, price, SKU, category, and brand. When users view products on your frontend, fetch the product data from Strapi's API and push it to the data layer using Google's e-commerce event structure. The data layer push should use the ecommerce object with a nested items array containing product details. Create a variable in GTM to capture the e-commerce object, then configure GA4 Event tags for view_item, add_to_cart, and purchase events. The key integration point is transforming Strapi's response format (which includes data.attributes nesting) into the flat structure Google Analytics expects for e-commerce tracking.