Install Google Tag Manager on Strapi Sites | OpsBlu Docs

Install Google Tag Manager on Strapi Sites

How to install GTM on Strapi-powered sites using Next.js, Gatsby, Nuxt, and other frontend frameworks.

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

  1. Create a GTM Account

    • Go to Google Tag Manager
    • Create a new account and container
    • Note your Container ID (format: GTM-XXXXXX)
  2. 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 TagsNew
  • 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_content trigger

Testing GTM Installation

1. Use GTM Preview Mode

  • In GTM, click Preview
  • Enter your site URL
  • GTM debugger opens in new tab

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.com or googletagmanager.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 VariablesNewData 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

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}} equals true

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}} equals true

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

For general GTM concepts, see Google Tag Manager Guide.