GTM Data Layer for Strapi Sites | OpsBlu Docs

GTM Data Layer for Strapi Sites

Structure and implement a data layer for Strapi-powered sites using Google Tag Manager.

Learn how to structure and implement a robust data layer for Strapi-powered sites that works seamlessly with Google Tag Manager.

What is a Data Layer?

The data layer is a JavaScript object that stores information about your page and user interactions, making it available to GTM tags. For Strapi sites, the data layer bridges your headless CMS content with your analytics tracking.

Data Layer Structure for Strapi

Basic Page Load Data Layer

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'page_view',
  page: {
    url: window.location.href,
    path: window.location.pathname,
    title: document.title,
    type: 'article', // Strapi content type
    locale: 'en', // From Strapi i18n
  },
  strapi: {
    contentType: 'article',
    contentId: '123',
    apiVersion: 'v4',
  },
});

Content-Specific Data Layer

For Strapi collection types (articles, products, etc.):

window.dataLayer.push({
  event: 'view_content',
  content: {
    id: article.id,
    type: article.__component || 'article',
    title: article.attributes.title,
    slug: article.attributes.slug,
    locale: article.attributes.locale,
    publishedAt: article.attributes.publishedAt,
    updatedAt: article.attributes.updatedAt,
  },
  author: {
    id: article.attributes.author?.data?.id,
    name: article.attributes.author?.data?.attributes?.name,
  },
  category: {
    id: article.attributes.category?.data?.id,
    name: article.attributes.category?.data?.attributes?.name,
    slug: article.attributes.category?.data?.attributes?.slug,
  },
  tags: article.attributes.tags?.data?.map(tag => tag.attributes.name) || [],
});

Framework-Specific Implementations

Next.js (App Router)

// lib/dataLayer.ts
export interface StrapiContent {
  id: number;
  attributes: {
    title: string;
    slug: string;
    locale?: string;
    publishedAt?: string;
    category?: any;
    author?: any;
    tags?: any;
  };
}

export const pushContentToDataLayer = (content: StrapiContent, eventName = 'view_content') => {
  if (typeof window === 'undefined') return;

  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    event: eventName,
    content: {
      id: content.id,
      type: 'article',
      title: content.attributes.title,
      slug: content.attributes.slug,
      locale: content.attributes.locale || 'en',
      publishedAt: content.attributes.publishedAt,
    },
    author: {
      id: content.attributes.author?.data?.id,
      name: content.attributes.author?.data?.attributes?.name,
    },
    category: {
      id: content.attributes.category?.data?.id,
      name: content.attributes.category?.data?.attributes?.name,
    },
    tags: content.attributes.tags?.data?.map((tag: any) => tag.attributes.name) || [],
  });
};

Usage in component:

// app/articles/[slug]/page.tsx
'use client';

import { useEffect } from 'react';
import { pushContentToDataLayer } from '@/lib/dataLayer';

export default function ArticlePage({ article }: { article: StrapiContent }) {
  useEffect(() => {
    pushContentToDataLayer(article);
  }, [article]);

  return (
    <article>
      <h1>{article.attributes.title}</h1>
      {/* Article content */}
    </article>
  );
}

Next.js (Pages Router)

// lib/dataLayer.js
export const initDataLayer = () => {
  window.dataLayer = window.dataLayer || [];
};

export const pushPageView = (pageData) => {
  window.dataLayer.push({
    event: 'page_view',
    page: {
      url: window.location.href,
      path: window.location.pathname,
      title: document.title,
      ...pageData,
    },
  });
};

export const pushContentView = (content) => {
  window.dataLayer.push({
    event: 'view_content',
    content: {
      id: content.id,
      type: content.__component || 'article',
      title: content.attributes.title,
      slug: content.attributes.slug,
    },
    author: content.attributes.author?.data?.attributes?.name,
    category: content.attributes.category?.data?.attributes?.name,
  });
};
// pages/articles/[slug].js
import { useEffect } from 'react';
import { pushContentView } from '@/lib/dataLayer';

export default function Article({ article }) {
  useEffect(() => {
    pushContentView(article);
  }, [article]);

  return <article>{/* Content */}</article>;
}

Gatsby

// src/utils/dataLayer.js
export const pushToDataLayer = (data) => {
  if (typeof window === 'undefined') return;

  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push(data);
};

export const trackContentView = (article) => {
  pushToDataLayer({
    event: 'view_content',
    content: {
      id: article.strapiId,
      type: 'article',
      title: article.title,
      slug: article.slug,
    },
    category: article.category?.name,
    author: article.author?.name,
    publishedAt: article.publishedAt,
  });
};
// src/templates/article.js
import React, { useEffect } from 'react';
import { trackContentView } from '../utils/dataLayer';

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

  useEffect(() => {
    trackContentView(article);
  }, [article]);

  return (
    <article>
      <h1>{article.title}</h1>
      {/* Article content */}
    </article>
  );
};

Nuxt.js

// composables/useDataLayer.ts
export const useDataLayer = () => {
  const pushToDataLayer = (data: any) => {
    if (process.client && window.dataLayer) {
      window.dataLayer.push(data);
    }
  };

  const trackContentView = (content: any) => {
    pushToDataLayer({
      event: 'view_content',
      content: {
        id: content.id,
        type: content.attributes.__component || 'article',
        title: content.attributes.title,
        slug: content.attributes.slug,
        locale: content.attributes.locale,
      },
      author: content.attributes.author?.data?.attributes?.name,
      category: content.attributes.category?.data?.attributes?.name,
    });
  };

  return {
    pushToDataLayer,
    trackContentView,
  };
};
<!-- pages/articles/[slug].vue -->
<script setup>
const { trackContentView } = useDataLayer();
const { data: article } = await useFetch('/api/articles/...');

onMounted(() => {
  if (article.value) {
    trackContentView(article.value.data);
  }
});
</script>

Strapi Content Type Patterns

Collection Types

Articles/Blog Posts

window.dataLayer.push({
  event: 'view_content',
  content: {
    id: article.id,
    type: 'article',
    title: article.attributes.title,
    slug: article.attributes.slug,
    publishedAt: article.attributes.publishedAt,
    wordCount: article.attributes.content?.length || 0,
  },
  author: {
    id: article.attributes.author?.data?.id,
    name: article.attributes.author?.data?.attributes?.name,
    slug: article.attributes.author?.data?.attributes?.slug,
  },
  category: {
    id: article.attributes.category?.data?.id,
    name: article.attributes.category?.data?.attributes?.name,
  },
  tags: article.attributes.tags?.data?.map(tag => tag.attributes.name),
  seo: {
    metaTitle: article.attributes.seo?.metaTitle,
    metaDescription: article.attributes.seo?.metaDescription,
  },
});

Products (if using Strapi for e-commerce)

window.dataLayer.push({
  event: 'view_item',
  ecommerce: {
    currency: 'USD',
    value: product.attributes.price,
    items: [{
      item_id: product.id,
      item_name: product.attributes.name,
      item_brand: product.attributes.brand?.data?.attributes?.name,
      item_category: product.attributes.category?.data?.attributes?.name,
      price: product.attributes.price,
      quantity: 1,
    }],
  },
  product: {
    id: product.id,
    sku: product.attributes.sku,
    stock: product.attributes.stock,
    featured: product.attributes.featured,
  },
});

Events (e.g., conferences, webinars)

window.dataLayer.push({
  event: 'view_event',
  strapiEvent: {
    id: event.id,
    title: event.attributes.title,
    type: event.attributes.eventType,
    date: event.attributes.date,
    location: event.attributes.location,
    capacity: event.attributes.capacity,
    registrations: event.attributes.registrations,
  },
});

Single Types

Homepage

window.dataLayer.push({
  event: 'page_view',
  page: {
    type: 'homepage',
    template: 'single_type',
  },
  homepage: {
    heroTitle: homepageData.attributes.hero?.title,
    featuredArticles: homepageData.attributes.featuredArticles?.data?.length || 0,
  },
});

About Page

window.dataLayer.push({
  event: 'page_view',
  page: {
    type: 'about',
    template: 'single_type',
  },
});

Dynamic Zones Tracking

Track interactions with Strapi Dynamic Zone components:

// Track which components are visible
const trackDynamicZone = (components) => {
  window.dataLayer.push({
    event: 'dynamic_zone_view',
    components: components.map(comp => ({
      type: comp.__component,
      id: comp.id,
    })),
  });
};

// Track individual component interaction
const trackComponentClick = (component) => {
  window.dataLayer.push({
    event: 'component_interaction',
    component: {
      type: component.__component,
      id: component.id,
      action: 'click',
    },
  });
};

User Interactions

window.dataLayer.push({
  event: 'search',
  search: {
    term: searchQuery,
    resultsCount: results.length,
    contentTypes: ['article', 'product'], // Which Strapi types were searched
    filters: {
      category: selectedCategory,
      dateRange: dateRange,
    },
  },
});

Filters

window.dataLayer.push({
  event: 'filter_apply',
  filter: {
    type: 'category',
    value: selectedCategory.attributes.name,
    resultsCount: filteredResults.length,
  },
});

Pagination

window.dataLayer.push({
  event: 'pagination',
  pagination: {
    currentPage: page,
    totalPages: totalPages,
    itemsPerPage: pageSize,
    contentType: 'article',
  },
});

Multi-Language Sites (Strapi i18n)

Track language and locale information:

window.dataLayer.push({
  event: 'page_view',
  page: {
    locale: article.attributes.locale,
    defaultLocale: 'en',
    availableLocales: ['en', 'fr', 'es'],
  },
  content: {
    id: article.id,
    locale: article.attributes.locale,
    localizations: article.attributes.localizations?.data?.map(loc => ({
      locale: loc.attributes.locale,
      id: loc.id,
    })),
  },
});

// Track language switch
window.dataLayer.push({
  event: 'language_change',
  language: {
    from: previousLocale,
    to: newLocale,
  },
});

User Data (Authenticated Users)

If you have user authentication:

// On login/authentication
window.dataLayer.push({
  event: 'user_authenticated',
  user: {
    id: user.id,
    role: user.role?.name,
    // DO NOT include PII like email or name
  },
});

// On page load for authenticated users
window.dataLayer.push({
  event: 'page_view',
  user: {
    id: user.id,
    authenticated: true,
    role: user.role?.name,
  },
});

Important: Never include Personally Identifiable Information (PII) in the data layer without proper consent and hashing.

E-commerce Events (Strapi + Stripe/Shop)

Add to Cart

window.dataLayer.push({
  event: 'add_to_cart',
  ecommerce: {
    currency: 'USD',
    value: product.attributes.price,
    items: [{
      item_id: product.id,
      item_name: product.attributes.name,
      item_category: product.attributes.category?.data?.attributes?.name,
      price: product.attributes.price,
      quantity: quantity,
    }],
  },
});

Purchase

window.dataLayer.push({
  event: 'purchase',
  ecommerce: {
    transaction_id: order.id,
    value: order.total,
    currency: 'USD',
    tax: order.tax,
    shipping: order.shipping,
    items: order.items.map(item => ({
      item_id: item.product.id,
      item_name: item.product.attributes.name,
      price: item.price,
      quantity: item.quantity,
    })),
  },
});

Creating GTM Variables

Once data is in the data layer, create GTM variables to access it.

Data Layer Variables

In GTM, go to VariablesNewData Layer Variable:

Variable: Content ID

  • Type: Data Layer Variable
  • Data Layer Variable Name: content.id
  • Name: DLV - Content ID

Variable: Content Type

  • Data Layer Variable Name: content.type
  • Name: DLV - Content Type

Variable: Content Title

  • Data Layer Variable Name: content.title
  • Name: DLV - Content Title

Variable: Author Name

  • Data Layer Variable Name: author.name
  • Name: DLV - Author Name

Variable: Category Name

  • Data Layer Variable Name: category.name
  • Name: DLV - Category Name

Variable: Content Locale

  • Data Layer Variable Name: content.locale
  • Name: DLV - Content Locale

Custom JavaScript Variables

For more complex data manipulation:

// Variable: Formatted Published Date
function() {
  var publishedAt = {{DLV - Published At}};
  if (publishedAt) {
    var date = new Date(publishedAt);
    return date.toLocaleDateString();
  }
  return undefined;
}
// Variable: Content Age in Days
function() {
  var publishedAt = {{DLV - Published At}};
  if (publishedAt) {
    var publishDate = new Date(publishedAt);
    var today = new Date();
    var diffTime = Math.abs(today - publishDate);
    var diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
    return diffDays;
  }
  return undefined;
}

Testing the Data Layer

1. Browser Console

// View entire data layer
console.table(window.dataLayer);

// View latest push
console.log(window.dataLayer[window.dataLayer.length - 1]);

// Listen for new pushes
const originalPush = window.dataLayer.push;
window.dataLayer.push = function() {
  console.log('Data Layer Push:', arguments[0]);
  return originalPush.apply(window.dataLayer, arguments);
};

2. GTM Preview Mode

  • Click Preview in GTM
  • Navigate your Strapi site
  • Check Data Layer tab in debugger
  • Verify all expected values populate

3. Data Layer Inspector Extension

Install browser extensions:

Best Practices

1. Consistent Naming

Use consistent naming across your data layer:

// Good - consistent structure
{
  event: 'view_content',
  content: { id, type, title },
  author: { id, name },
  category: { id, name }
}

// Bad - inconsistent
{
  event: 'view_content',
  contentId: 123,
  authorName: 'John',
  cat: 'Tech'
}

2. Avoid PII

Never include personally identifiable information:

// Bad - includes PII
{
  user: {
    email: 'user@example.com',
    name: 'John Doe',
    phone: '555-1234'
  }
}

// Good - uses hashed/anonymized IDs
{
  user: {
    id: 'abc123', // Hashed user ID
    role: 'subscriber'
  }
}

3. Clear Event Names

Use descriptive, consistent event names:

// Good
'view_content', 'select_category', 'apply_filter'

// Bad
'vc', 'cat_click', 'filter1'

4. Document Your Data Layer

Maintain documentation of your data layer structure:

// types/dataLayer.ts
export interface DataLayerContent {
  id: number;
  type: string;
  title: string;
  slug: string;
  locale?: string;
}

export interface DataLayerEvent {
  event: string;
  content?: DataLayerContent;
  author?: { id: number; name: string };
  category?: { id: number; name: string };
}

Troubleshooting

Data Layer Values Are Undefined

Cause: Strapi API response not populated or wrong path.

Solution: Use populate parameter in Strapi API calls:

fetch(`${STRAPI_URL}/api/articles/${slug}?populate=deep`)

GTM Variables Not Populating

Cause: Data pushed after GTM tag fires.

Solution: Push to data layer before event:

// Push data first
window.dataLayer.push({
  content: { /* data */ },
});

// Then push event
window.dataLayer.push({
  event: 'view_content',
});

Data Layer Cleared on Navigation

Cause: Full page reload clears data layer.

Solution: This is expected. Push new data on each navigation.

Next Steps

For general data layer concepts, see GTM Data Layer Guide.