Integrate Google Tag Manager with Strapi
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
These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
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-partiesFor 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-XXXXXXXCreate 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-moduleInitialize 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-tagmanagerConfigure 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 ArticleTemplateData 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:
- Open your GTM container at tagmanager.google.com
- Create a new trigger with type "Custom Event"
- 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) - Create tags that fire on these custom event triggers
- 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 IDarticle.title→ Variable name:Article Titlearticle.category→ Variable name:Article Categoryarticle.author→ Variable name:Article Authorarticle.publishedAt→ Variable name:Published Datearticle.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.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.