Since Strapi is a headless CMS, Google Tag Manager is installed on your frontend framework (Next.js, Gatsby, Nuxt.js, etc.), not in Strapi itself. GTM is the recommended approach for managing all tracking tags on Strapi-powered sites.
Why Use GTM with Strapi?
Benefits for Headless Sites:
- Framework-agnostic: Works with any frontend framework
- Centralized management: Update tags without deploying code
- Team collaboration: Marketers can manage tags without developers
- Better performance: Single container load for multiple tags
- Testing: Preview and debug before publishing
- Version control: Built-in versioning for tag configurations
Before You Begin
Create a GTM Account
- Go to Google Tag Manager
- Create a new account and container
- Note your Container ID (format:
GTM-XXXXXX)
Choose Your Implementation Method
- Direct implementation in your frontend framework
- Third-party libraries/packages
- Custom wrapper components
Method 1: Next.js + Strapi
App Router (Next.js 13+)
1. Create GTM Component
// components/GoogleTagManager.tsx
'use client';
import Script from 'next/script';
interface GTMProps {
gtmId: string;
}
export default function GoogleTagManager({ gtmId }: GTMProps) {
return (
<>
{/* Google Tag Manager 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','${gtmId}');
`,
}}
/>
</>
);
}
export function GTMNoScript({ gtmId }: GTMProps) {
return (
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`}
height="0"
width="0"
style={{ display: 'none', visibility: 'hidden' }}
/>
</noscript>
);
}
2. Add to Root Layout
// app/layout.tsx
import GoogleTagManager, { GTMNoScript } from '@/components/GoogleTagManager';
export default function RootLayout({ children }: { children: React.ReactNode }) {
const gtmId = process.env.NEXT_PUBLIC_GTM_ID!;
return (
<html lang="en">
<head>
<GoogleTagManager gtmId={gtmId} />
</head>
<body>
<GTMNoScript gtmId={gtmId} />
{children}
</body>
</html>
);
}
3. Create Data Layer Utility
// lib/gtm.ts
type DataLayerEvent = {
event: string;
[key: string]: any;
};
export const pushToDataLayer = (data: DataLayerEvent) => {
if (typeof window !== 'undefined') {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(data);
}
};
// Example usage functions
export const trackPageView = (url: string) => {
pushToDataLayer({
event: 'page_view',
page: {
url,
title: document.title,
},
});
};
export const trackContentView = (content: any) => {
pushToDataLayer({
event: 'view_content',
contentType: content.__component || 'article',
contentId: content.id,
contentTitle: content.attributes?.title,
});
};
4. Track Route Changes
// components/RouteChangeTracker.tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { pushToDataLayer } from '@/lib/gtm';
export default function RouteChangeTracker() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
const url = pathname + (searchParams?.toString() ? `?${searchParams.toString()}` : '');
pushToDataLayer({
event: 'page_view',
page: {
url,
path: pathname,
},
});
}, [pathname, searchParams]);
return null;
}
Add to layout:
// app/layout.tsx
import { Suspense } from 'react';
import RouteChangeTracker from '@/components/RouteChangeTracker';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Suspense fallback={null}>
<RouteChangeTracker />
</Suspense>
</body>
</html>
);
}
Pages Router (Next.js 12 and earlier)
1. Install Package (Optional)
npm install @next/third-parties
# or
npm install react-gtm-module
2. Add to _app.js
// pages/_app.js
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import Script from 'next/script';
function MyApp({ Component, pageProps }) {
const router = useRouter();
const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID;
useEffect(() => {
const handleRouteChange = (url) => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'page_view',
page: url,
});
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return (
<>
{/* Google Tag Manager */}
<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}');
`,
}}
/>
<Component {...pageProps} />
</>
);
}
export default MyApp;
3. Add to _document.js
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
const GTM_ID = process.env.NEXT_PUBLIC_GTM_ID;
return (
<Html lang="en">
<Head />
<body>
{/* Google Tag Manager (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>
);
}
Method 2: Gatsby + Strapi
Installation
npm install gatsby-plugin-google-tagmanager
Configuration
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: 'gatsby-plugin-google-tagmanager',
options: {
id: process.env.GTM_ID,
includeInDevelopment: false,
defaultDataLayer: { platform: 'gatsby' },
routeChangeEventName: 'gatsby-route-change',
enableWebVitalsTracking: true,
},
},
{
resolve: 'gatsby-source-strapi',
options: {
apiURL: process.env.STRAPI_API_URL || 'http://localhost:1337',
accessToken: process.env.STRAPI_TOKEN,
collectionTypes: ['article', 'category'],
},
},
],
};
Push to Data Layer
// src/templates/article.js
import React, { useEffect } from 'react';
const ArticleTemplate = ({ data }) => {
const article = data.strapiArticle;
useEffect(() => {
if (typeof window !== 'undefined' && window.dataLayer) {
window.dataLayer.push({
event: 'view_content',
contentType: 'article',
contentId: article.strapiId,
contentTitle: article.title,
});
}
}, [article]);
return (
<article>
<h1>{article.title}</h1>
{/* Article content */}
</article>
);
};
export default ArticleTemplate;
Method 3: Nuxt.js + Strapi
Installation (Nuxt 3)
npm install @gtm-support/vue-gtm
Configuration
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/gtm'],
gtm: {
id: process.env.GTM_ID || '',
enabled: true,
debug: process.env.NODE_ENV === 'development',
loadScript: true,
enableRouterSync: true,
devtools: true,
},
runtimeConfig: {
public: {
strapiUrl: process.env.STRAPI_URL || 'http://localhost:1337',
},
},
});
Push to Data Layer
<!-- pages/articles/[slug].vue -->
<script setup>
const { $gtm } = useNuxtApp();
const route = useRoute();
const { data: article } = await useFetch(
`${useRuntimeConfig().public.strapiUrl}/api/articles/${route.params.slug}?populate=*`
);
onMounted(() => {
if (article.value) {
$gtm.push({
event: 'view_content',
contentType: 'article',
contentId: article.value.data.id,
contentTitle: article.value.data.attributes.title,
});
}
});
</script>
Method 4: React SPA + Strapi
Installation
npm install react-gtm-module
Implementation
// src/gtm.js
import TagManager from 'react-gtm-module';
const tagManagerArgs = {
gtmId: process.env.REACT_APP_GTM_ID,
dataLayer: {
platform: 'react-spa',
},
};
export const initGTM = () => {
TagManager.initialize(tagManagerArgs);
};
export const pushToDataLayer = (data) => {
TagManager.dataLayer({
dataLayer: data,
});
};
// src/App.js
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { initGTM, pushToDataLayer } from './gtm';
function App() {
const location = useLocation();
useEffect(() => {
initGTM();
}, []);
useEffect(() => {
pushToDataLayer({
event: 'page_view',
page: location.pathname,
});
}, [location]);
return (
<div className="App">
{/* Your app content */}
</div>
);
}
export default App;
Method 5: Vue.js + Strapi
Installation
npm install @gtm-support/vue-gtm
Configuration
// src/main.js
import { createApp } from 'vue';
import { createGtm } from '@gtm-support/vue-gtm';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(router);
app.use(
createGtm({
id: import.meta.env.VITE_GTM_ID,
vueRouter: router,
enabled: true,
debug: import.meta.env.DEV,
loadScript: true,
})
);
app.mount('#app');
Environment Variables
For all frameworks, store GTM ID in environment variables:
# .env.local (Next.js)
NEXT_PUBLIC_GTM_ID=GTM-XXXXXX
# .env.production (Gatsby)
GTM_ID=GTM-XXXXXX
# .env (Nuxt)
GTM_ID=GTM-XXXXXX
# .env (React/Vue)
REACT_APP_GTM_ID=GTM-XXXXXX
VITE_GTM_ID=GTM-XXXXXX
Configure GTM Container
Once GTM is installed, configure tags in the GTM interface.
1. Create GA4 Configuration Tag
- Go to Tags → New
- Tag Type: Google Analytics: GA4 Configuration
- Measurement ID: Your GA4 ID (G-XXXXXXXXXX)
- Trigger: All Pages
2. Create Data Layer Variables
See GTM Data Layer Structure for detailed variable configuration.
3. Create Event Triggers
Create triggers for Strapi-specific events:
Trigger: View Content
- Type: Custom Event
- Event name:
view_content
Trigger: Page View
- Type: Custom Event
- Event name:
page_view
4. Create Event Tags
Tag: GA4 - View Content
- Type: Google Analytics: GA4 Event
- Configuration Tag: Select your GA4 Config tag
- Event Name:
view_content - Event Parameters: Map from data layer variables
- Trigger:
view_contenttrigger
Testing GTM Installation
1. Use GTM Preview Mode
2. Verify Container Loads
Check browser console:
// Should return GTM container object
console.log(google_tag_manager);
// Check data layer
console.log(window.dataLayer);
3. Test Data Layer Pushes
// Manually push test event
window.dataLayer.push({
event: 'test_event',
testParam: 'test_value',
});
Verify it appears in GTM Preview.
4. Check Network Requests
In browser DevTools Network tab:
- Filter by
google-analytics.comorgoogletagmanager.com - Verify GTM container loads
- Verify GA4 hits send
SSR/SSG Considerations
Server-Side Rendering
GTM script must only run client-side:
// Only initialize on client
if (typeof window !== 'undefined') {
// GTM code here
}
Static Site Generation
For SSG, GTM loads at build time but executes at runtime:
// Gatsby example - plugin handles this automatically
// Manual implementation:
useEffect(() => {
// Runs only in browser after hydration
window.dataLayer = window.dataLayer || [];
}, []);
Hybrid Rendering
For mixed SSR/SSG:
// Next.js - Use 'use client' directive
'use client';
export default function GTM() {
// Client-only code
}
Common Issues
GTM Not Loading
Cause: Script blocked by ad blockers or incorrect container ID.
Solution:
- Test in incognito mode
- Verify GTM_ID environment variable
- Check browser console for errors
Data Layer Not Defined
Cause: Data layer push before GTM initialization.
Solution:
// Initialize data layer
window.dataLayer = window.dataLayer || [];
// Then push
window.dataLayer.push({ event: 'myEvent' });
Events Not Firing on Route Changes
Cause: SPA navigation not tracked.
Solution: Implement route change listener (shown in framework examples above).
Duplicate Events
Cause: Multiple GTM implementations or duplicate route listeners.
Solution:
- Check for duplicate GTM scripts
- Ensure route listener cleanup in useEffect
Content Model Data Layer Setup
Configure your data layer to match Strapi's content structure for accurate tracking.
Strapi Content Structure Overview
Strapi returns data in a specific format. Understanding this structure is crucial for proper data layer configuration.
REST API Response:
{
"data": {
"id": 1,
"attributes": {
"title": "My Article",
"slug": "my-article",
"content": "Article content...",
"publishedAt": "2024-01-15T10:00:00.000Z",
"category": {
"data": {
"id": 5,
"attributes": {
"name": "Technology"
}
}
},
"author": {
"data": {
"id": 2,
"attributes": {
"name": "John Doe",
"email": "john@example.com"
}
}
},
"tags": {
"data": [
{
"id": 1,
"attributes": { "name": "JavaScript" }
},
{
"id": 2,
"attributes": { "name": "Web Development" }
}
]
}
}
}
}
GraphQL Response:
{
"data": {
"articles": {
"data": [
{
"id": "1",
"attributes": {
"title": "My Article",
"slug": "my-article",
"category": {
"data": {
"attributes": { "name": "Technology" }
}
}
}
}
]
}
}
}
Data Layer Structure for Strapi Content
Create a standardized data layer that maps Strapi content types:
// lib/strapi-data-layer.ts
/**
* Standard Strapi content data layer structure
*/
export interface StrapiDataLayer {
// Event metadata
event: string;
eventCategory?: string;
eventAction?: string;
eventLabel?: string;
// Content metadata
contentId: string | number;
contentType: string;
contentTitle: string;
contentSlug: string;
contentStatus: 'draft' | 'published';
// Content relationships
contentCategory?: string;
contentAuthor?: string;
contentTags?: string[];
// Timestamps
publishedAt?: string;
createdAt?: string;
updatedAt?: string;
// User context (if authenticated)
userId?: string;
userRole?: string;
// Page context
pageType: string;
pageUrl: string;
pageTitle: string;
}
/**
* Parse Strapi REST API response to data layer format
*/
export function parseRestApiToDataLayer(
strapiData: any,
eventName: string = 'view_content'
): StrapiDataLayer {
const attributes = strapiData.data?.attributes || strapiData.attributes || {};
return {
event: eventName,
eventCategory: 'Content',
eventAction: 'View',
eventLabel: attributes.title || attributes.name,
contentId: strapiData.data?.id || strapiData.id,
contentType: strapiData.__component || 'article',
contentTitle: attributes.title || attributes.name || 'Untitled',
contentSlug: attributes.slug || '',
contentStatus: attributes.publishedAt ? 'published' : 'draft',
contentCategory: attributes.category?.data?.attributes?.name,
contentAuthor: attributes.author?.data?.attributes?.name,
contentTags: attributes.tags?.data?.map((tag: any) => tag.attributes.name) || [],
publishedAt: attributes.publishedAt,
createdAt: attributes.createdAt,
updatedAt: attributes.updatedAt,
pageType: 'content',
pageUrl: window.location.href,
pageTitle: document.title,
};
}
/**
* Parse Strapi GraphQL response to data layer format
*/
export function parseGraphQLToDataLayer(
graphqlData: any,
eventName: string = 'view_content'
): StrapiDataLayer {
const article = graphqlData.data?.articles?.data?.[0] || graphqlData;
const attributes = article.attributes || {};
return {
event: eventName,
eventCategory: 'Content',
eventAction: 'View',
eventLabel: attributes.title,
contentId: article.id,
contentType: 'article',
contentTitle: attributes.title,
contentSlug: attributes.slug,
contentStatus: attributes.publishedAt ? 'published' : 'draft',
contentCategory: attributes.category?.data?.attributes?.name,
contentAuthor: attributes.author?.data?.attributes?.name,
contentTags: attributes.tags?.data?.map((tag: any) => tag.attributes.name) || [],
publishedAt: attributes.publishedAt,
createdAt: attributes.createdAt,
updatedAt: attributes.updatedAt,
pageType: 'content',
pageUrl: window.location.href,
pageTitle: document.title,
};
}
/**
* Push parsed data to GTM data layer
*/
export function pushStrapiDataToGTM(dataLayerObject: StrapiDataLayer) {
if (typeof window === 'undefined') return;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(dataLayerObject);
console.log('Strapi data pushed to GTM:', dataLayerObject);
}
Implementation Examples
Next.js with REST API:
// app/articles/[slug]/page.tsx
import { parseRestApiToDataLayer, pushStrapiDataToGTM } from '@/lib/strapi-data-layer';
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const response = await fetch(
`${process.env.STRAPI_API_URL}/api/articles?filters[slug][$eq]=${params.slug}&populate=*`
);
const data = await response.json();
const article = data.data[0];
return <ArticleContent article={article} />;
}
// app/articles/[slug]/ArticleContent.tsx
'use client';
import { useEffect } from 'react';
import { parseRestApiToDataLayer, pushStrapiDataToGTM } from '@/lib/strapi-data-layer';
export function ArticleContent({ article }: { article: any }) {
useEffect(() => {
const dataLayer = parseRestApiToDataLayer(article, 'view_article');
pushStrapiDataToGTM(dataLayer);
}, [article.id]);
return <article>{/* Content */}</article>;
}
Next.js with GraphQL:
// app/articles/[slug]/page.tsx
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
import { parseGraphQLToDataLayer, pushStrapiDataToGTM } from '@/lib/strapi-data-layer';
const GET_ARTICLE = gql`
query GetArticle($slug: String!) {
articles(filters: { slug: { eq: $slug } }) {
data {
id
attributes {
title
slug
publishedAt
category { data { attributes { name } } }
author { data { attributes { name } } }
tags { data { attributes { name } } }
}
}
}
}
`;
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const client = new ApolloClient({
uri: `${process.env.STRAPI_API_URL}/graphql`,
cache: new InMemoryCache(),
});
const { data } = await client.query({
query: GET_ARTICLE,
variables: { slug: params.slug },
});
return <ArticleContent data={data} />;
}
// Client component
'use client';
export function ArticleContent({ data }: { data: any }) {
useEffect(() => {
const dataLayer = parseGraphQLToDataLayer(data, 'view_article');
pushStrapiDataToGTM(dataLayer);
}, [data]);
return <article>{/* Content */}</article>;
}
Gatsby:
// src/templates/article.js
import React, { useEffect } from 'react';
import { graphql } from 'gatsby';
import { parseGraphQLToDataLayer, pushStrapiDataToGTM } from '../lib/strapi-data-layer';
const ArticleTemplate = ({ data }) => {
const article = data.strapiArticle;
useEffect(() => {
if (typeof window !== 'undefined') {
const dataLayer = {
event: 'view_article',
contentId: article.strapiId,
contentType: 'article',
contentTitle: article.title,
contentCategory: article.category?.name,
contentAuthor: article.author?.name,
contentTags: article.tags?.map((tag) => tag.name),
};
window.dataLayer = window.dataLayer || [];
window.dataLayer.push(dataLayer);
}
}, [article]);
return <article>{/* Content */}</article>;
};
export const query = graphql`
query($slug: String!) {
strapiArticle(slug: { eq: $slug }) {
strapiId
title
slug
category { name }
author { name }
tags { name }
}
}
`;
export default ArticleTemplate;
Configure GTM Variables for Strapi Data
In Google Tag Manager, create Data Layer Variables to capture Strapi content data:
1. Create Variables
Go to Variables → New → Data Layer Variable
| Variable Name | Data Layer Variable Name |
|---|---|
| DL - Content ID | contentId |
| DL - Content Type | contentType |
| DL - Content Title | contentTitle |
| DL - Content Category | contentCategory |
| DL - Content Author | contentAuthor |
| DL - Content Tags | contentTags |
| DL - Content Status | contentStatus |
| DL - Published Date | publishedAt |
2. Create Custom Event Trigger
Trigger Name: View Article
- Trigger Type: Custom Event
- Event name:
view_article - Use regex matching:
view_.*(to catch all view events)
3. Create GA4 Event Tag
Tag Name: GA4 - View Strapi Content
- Tag Type: Google Analytics: GA4 Event
- Configuration Tag: Select your GA4 Config tag
- Event Name:
view_content
Event Parameters:
| Parameter Name | Value |
|---|---|
| content_type | {{DL - Content Type}} |
| content_id | {{DL - Content ID}} |
| content_category | {{DL - Content Category}} |
| content_author | {{DL - Content Author}} |
| content_status | {{DL - Content Status}} |
- Trigger: View Article
E-commerce Data Layer (for Strapi-powered stores)
If using Strapi for e-commerce:
// lib/strapi-ecommerce-data-layer.ts
export interface StrapiProductDataLayer {
event: 'view_item' | 'add_to_cart' | 'purchase';
ecommerce: {
currency: string;
value: number;
items: Array<{
item_id: string;
item_name: string;
item_brand?: string;
item_category?: string;
item_variant?: string;
price: number;
quantity: number;
}>;
};
}
export function parseProductToDataLayer(product: any): StrapiProductDataLayer {
const attributes = product.attributes || product;
return {
event: 'view_item',
ecommerce: {
currency: 'USD',
value: attributes.price,
items: [
{
item_id: attributes.sku || product.id.toString(),
item_name: attributes.name || attributes.title,
item_brand: attributes.brand?.data?.attributes?.name,
item_category: attributes.category?.data?.attributes?.name,
item_variant: attributes.variant,
price: attributes.price,
quantity: 1,
},
],
},
};
}
// Usage
useEffect(() => {
const dataLayer = parseProductToDataLayer(product);
window.dataLayer.push(dataLayer);
}, [product.id]);
Collection/Category Data Layer
For category/collection pages:
export function parseCategoryToDataLayer(
category: any,
items: any[]
): StrapiDataLayer {
return {
event: 'view_item_list',
eventCategory: 'Category',
eventAction: 'View',
eventLabel: category.attributes.name,
contentId: category.id,
contentType: 'category',
contentTitle: category.attributes.name,
contentSlug: category.attributes.slug,
contentStatus: 'published',
// Add item list data
itemListId: category.id.toString(),
itemListName: category.attributes.name,
items: items.map((item, index) => ({
item_id: item.attributes.slug,
item_name: item.attributes.title,
index,
item_category: category.attributes.name,
})),
pageType: 'category',
pageUrl: window.location.href,
pageTitle: document.title,
};
}
Dynamic Zone Data Layer
For Strapi dynamic zones:
export function parseDynamicZoneToDataLayer(page: any) {
const components = page.attributes.content || []; // Dynamic zone field
return {
event: 'view_page',
contentId: page.id,
contentType: 'page',
contentTitle: page.attributes.title,
// Track which components are on the page
pageComponents: components.map((comp: any) => comp.__component),
componentCount: components.length,
// Track specific component types
hasHeroSection: components.some((c: any) => c.__component === 'sections.hero'),
hasFormSection: components.some((c: any) => c.__component === 'sections.form'),
hasCTASection: components.some((c: any) => c.__component === 'sections.cta'),
pageType: 'dynamic',
pageUrl: window.location.href,
pageTitle: document.title,
};
}
Admin Panel Exclusion
Prevent tracking on Strapi admin panel and ensure only frontend events are tracked.
Why Exclude Admin Panel?
- Avoid data pollution: Admin activity shouldn't count as user activity
- Privacy: Keep internal team actions private
- Accuracy: Ensure analytics reflect real user behavior
- GDPR compliance: Reduce tracking of employee data
Method 1: URL-Based Exclusion (Recommended)
If Strapi Admin on Subdomain/Path:
Most common setups:
- Admin:
https://admin.yoursite.com - Frontend:
https://yoursite.com
Or:
- Admin:
https://yoursite.com/admin - Frontend:
https://yoursite.com
Implementation:
// lib/should-track.ts
export function shouldInitializeTracking(): boolean {
if (typeof window === 'undefined') return false;
const hostname = window.location.hostname;
const pathname = window.location.pathname;
// Exclude admin subdomain
if (hostname.startsWith('admin.')) {
console.log('Admin subdomain detected - tracking disabled');
return false;
}
// Exclude admin path
if (pathname.startsWith('/admin')) {
console.log('Admin path detected - tracking disabled');
return false;
}
// Exclude Strapi backend
if (hostname.includes('strapi') || pathname.includes('/strapi')) {
console.log('Strapi backend detected - tracking disabled');
return false;
}
// Exclude localhost admin
if (hostname === 'localhost' && pathname.startsWith('/admin')) {
console.log('Localhost admin detected - tracking disabled');
return false;
}
return true;
}
// Usage
export function initializeGTM() {
if (!shouldInitializeTracking()) {
return;
}
// Initialize GTM
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'gtm.start': new Date().getTime(),
event: 'gtm.js',
});
}
Apply to GTM Component:
// components/GoogleTagManager.tsx
'use client';
import Script from 'next/script';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
export default function GoogleTagManager({ gtmId }: { gtmId: string }) {
const pathname = usePathname();
const [shouldTrack, setShouldTrack] = useState(false);
useEffect(() => {
// Check if we should track
const isAdminPanel = pathname?.startsWith('/admin');
const isStrapBackend = window.location.hostname.includes('strapi');
setShouldTrack(!isAdminPanel && !isStrapBackend);
}, [pathname]);
if (!shouldTrack) {
return null; // Don't render GTM
}
return (
<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','${gtmId}');
`,
}}
/>
);
}
Method 2: Environment Variable Exclusion
Set environment variable for Strapi admin:
# Admin environment
NEXT_PUBLIC_ENABLE_TRACKING=false
# Frontend environment
NEXT_PUBLIC_ENABLE_TRACKING=true
Implementation:
// components/GoogleTagManager.tsx
export default function GoogleTagManager({ gtmId }: { gtmId: string }) {
const enableTracking = process.env.NEXT_PUBLIC_ENABLE_TRACKING === 'true';
if (!enableTracking) {
console.log('Tracking disabled via environment variable');
return null;
}
return (
<Script id="gtm-script" /* GTM code */ />
);
}
Method 3: GTM-Side Exclusion (Defense in Depth)
Configure exclusions in GTM itself:
1. Create Hostname Variable
- Variable Name: Hostname
- Variable Type: URL
- Component Type: Hostname
2. Create Admin Detection Variable
- Variable Name: Is Admin Panel
- Variable Type: Custom JavaScript
function() {
var hostname = window.location.hostname;
var pathname = window.location.pathname;
// Check if admin subdomain
if (hostname.startsWith('admin.')) {
return true;
}
// Check if admin path
if (pathname.startsWith('/admin')) {
return true;
}
// Check if Strapi backend
if (hostname.includes('strapi')) {
return true;
}
return false;
}
3. Add Exception to All Tags
For each tag (GA4, Meta Pixel, etc.):
- Go to Triggering
- Add Exception:
{{Is Admin Panel}}equalstrue
Or create a Blocking Trigger:
- Trigger Name: Block Admin Panel
- Trigger Type: Custom Event
- This trigger fires on: All Custom Events
- Some Custom Events where
{{Is Admin Panel}}equalstrue
Then add this as an exception to all your tags.
Method 4: Separate Builds
Build separate versions for frontend and admin:
package.json:
{
"scripts": {
"build:frontend": "ENABLE_TRACKING=true next build",
"build:admin": "ENABLE_TRACKING=false next build",
"start:frontend": "ENABLE_TRACKING=true next start",
"start:admin": "ENABLE_TRACKING=false next start"
}
}
Method 5: Role-Based Exclusion
Exclude tracking for authenticated Strapi users:
// lib/tracking.ts
import Cookies from 'js-cookie';
export function isAdminUser(): boolean {
// Check for Strapi admin token
const strapiToken = Cookies.get('strapi-token');
if (strapiToken) {
try {
// Decode JWT to check role
const payload = JSON.parse(atob(strapiToken.split('.')[1]));
return payload.role === 'admin' || payload.role === 'editor';
} catch (e) {
return false;
}
}
return false;
}
export function shouldTrack(): boolean {
if (typeof window === 'undefined') return false;
// Don't track admin users
if (isAdminUser()) {
console.log('Admin user detected - tracking disabled');
return false;
}
// Don't track admin panel
if (window.location.pathname.startsWith('/admin')) {
return false;
}
return true;
}
Verification
Test admin panel exclusion:
// Browser console on admin panel
console.log('GTM loaded?', typeof window.google_tag_manager !== 'undefined');
console.log('Data layer exists?', typeof window.dataLayer !== 'undefined');
console.log('Should track?', shouldInitializeTracking());
// Should all be false on admin panel
Test frontend tracking:
// Browser console on frontend
console.log('GTM loaded?', typeof window.google_tag_manager !== 'undefined');
console.log('Data layer exists?', typeof window.dataLayer !== 'undefined');
console.log('Should track?', shouldInitializeTracking());
// Should all be true on frontend
Best Practices
1. Layer multiple methods:
export function shouldInitializeTracking(): boolean {
// Check 1: Server-side environment
if (process.env.NEXT_PUBLIC_ENABLE_TRACKING !== 'true') {
return false;
}
// Check 2: Client-side URL
if (typeof window !== 'undefined') {
if (window.location.pathname.startsWith('/admin')) {
return false;
}
}
// Check 3: User role
if (isAdminUser()) {
return false;
}
return true;
}
2. Add debug logging:
export function initializeGTM() {
const reasons = [];
if (process.env.NEXT_PUBLIC_ENABLE_TRACKING !== 'true') {
reasons.push('Disabled via env var');
}
if (window.location.pathname.startsWith('/admin')) {
reasons.push('Admin path detected');
}
if (isAdminUser()) {
reasons.push('Admin user detected');
}
if (reasons.length > 0) {
console.log('GTM not initialized:', reasons.join(', '));
return;
}
// Initialize GTM
console.log('GTM initialized');
}
3. Document exclusions:
Create a comment in your code:
/**
* GTM Exclusion Logic
*
* Tracking is disabled when:
* 1. NEXT_PUBLIC_ENABLE_TRACKING !== 'true'
* 2. URL path starts with /admin
* 3. Hostname includes 'admin.' or 'strapi'
* 4. User has admin/editor role (via strapi-token cookie)
*
* To test:
* - Frontend: window.dataLayer should exist
* - Admin: window.dataLayer should be undefined
*/
Next Steps
- Configure Data Layer - Deep dive into data layer structure
- Set up GA4 Events - Track custom events
- Troubleshoot Events - Debug tracking issues
- Install GA4 - Alternative to GTM
For general GTM concepts, see Google Tag Manager Guide.