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
Search
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 Variables → New → Data 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
- GA4 Event Tracking - Use data layer with GA4
- Troubleshoot Events - Debug tracking issues
- GTM Setup - Install GTM first if not done
For general data layer concepts, see GTM Data Layer Guide.