The data layer is the foundation of effective Google Tag Manager implementation on static sites. This guide covers implementing data layers for content built with Netlify CMS (now Decap CMS) across different static site generators.
Data Layer Fundamentals for Static Sites
Build-Time vs Runtime Data
Build-Time Data (template injection):
- Content metadata (author, category, publish date)
- Page type (blog post, landing page, documentation)
- Static properties (word count, reading time, tags)
- Content structure (section, collection)
Runtime Data (JavaScript):
- User behavior (scroll depth, time on page)
- Session data (returning visitor, session count)
- Dynamic interactions (clicks, form submissions)
- Browser/device information
Data Layer Structure
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
// Page Information (build-time)
'pageType': 'blog_post',
'contentCategory': 'tutorials',
'author': 'john_doe',
'publishDate': '2024-01-15',
'wordCount': 1500,
'tags': ['gatsby', 'javascript', 'jamstack'],
// User Information (runtime)
'userType': 'returning',
'sessionCount': 5,
// Event Information
'event': 'page_view'
});
Hugo Data Layer Implementation
Basic Content Metadata
Create layouts/partials/data-layer.html:
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'pageType': '{{ .Type }}',
'contentSection': '{{ .Section }}',
'contentCategory': '{{ .Section }}',
{{ with .Params.author }}
'author': '{{ . }}',
{{ end }}
{{ with .Date }}
'publishDate': '{{ .Format "2006-01-02" }}',
{{ end }}
{{ with .Lastmod }}
'lastModified': '{{ .Format "2006-01-02" }}',
{{ end }}
'wordCount': {{ .WordCount }},
'readingTime': {{ .ReadingTime }},
{{ if .Params.tags }}
'tags': {{ .Params.tags | jsonify }},
{{ end }}
{{ if .Params.categories }}
'categories': {{ .Params.categories | jsonify }},
{{ end }}
'pageUrl': '{{ .Permalink }}',
'pageTitle': '{{ .Title }}',
'environment': '{{ getenv "CONTEXT" }}'
});
</script>
Advanced Hugo Data Layer with Conditionals
Create layouts/partials/data-layer-advanced.html:
<script>
window.dataLayer = window.dataLayer || [];
{{ $dataLayer := dict }}
// Page metadata
{{ $dataLayer = merge $dataLayer (dict "pageType" .Type) }}
{{ $dataLayer = merge $dataLayer (dict "pageUrl" .Permalink) }}
{{ $dataLayer = merge $dataLayer (dict "pageTitle" .Title) }}
// Content metadata
{{ if .Section }}
{{ $dataLayer = merge $dataLayer (dict "contentSection" .Section) }}
{{ end }}
{{ with .Params.author }}
{{ $dataLayer = merge $dataLayer (dict "author" .) }}
{{ end }}
{{ with .Date }}
{{ $dataLayer = merge $dataLayer (dict "publishDate" (.Format "2006-01-02")) }}
{{ end }}
{{ $dataLayer = merge $dataLayer (dict "wordCount" .WordCount) }}
{{ $dataLayer = merge $dataLayer (dict "readingTime" .ReadingTime) }}
{{ if .Params.tags }}
{{ $dataLayer = merge $dataLayer (dict "tags" .Params.tags) }}
{{ end }}
// Environment information
{{ $dataLayer = merge $dataLayer (dict "environment" (getenv "CONTEXT")) }}
// Content status
{{ if .Draft }}
{{ $dataLayer = merge $dataLayer (dict "contentStatus" "draft") }}
{{ else }}
{{ $dataLayer = merge $dataLayer (dict "contentStatus" "published") }}
{{ end }}
// Language
{{ if .Site.IsMultiLingual }}
{{ $dataLayer = merge $dataLayer (dict "language" .Language.Lang) }}
{{ end }}
window.dataLayer.push({{ $dataLayer | jsonify }});
</script>
Blog Post-Specific Data Layer
Create layouts/posts/single.html:
{{ define "main" }}
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'pageType': 'blog_post',
'contentType': 'article',
'author': '{{ .Params.author }}',
'publishDate': '{{ .Date.Format "2006-01-02" }}',
'category': '{{ .Section }}',
{{ if .Params.featured }}
'featured': true,
{{ end }}
{{ if .Params.series }}
'series': '{{ .Params.series }}',
{{ end }}
'wordCount': {{ .WordCount }},
'readingTime': {{ .ReadingTime }},
'tags': {{ .Params.tags | jsonify }},
// Article schema
'articleId': '{{ .File.UniqueID }}',
'articleUrl': '{{ .Permalink }}'
});
</script>
{{ .Content }}
{{ end }}
Jekyll Data Layer Implementation
Basic Data Layer Include
Create _includes/data-layer.html:
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'pageType': '{{ page.layout }}',
{% if page.author %}
'author': '{{ page.author }}',
{% endif %}
{% if page.date %}
'publishDate': '{{ page.date | date: "%Y-%m-%d" }}',
{% endif %}
{% if page.categories %}
'contentCategory': '{{ page.categories | first }}',
'categories': {{ page.categories | jsonify }},
{% endif %}
{% if page.tags %}
'tags': {{ page.tags | jsonify }},
{% endif %}
'pageUrl': '{{ page.url | absolute_url }}',
'pageTitle': '{{ page.title }}',
'wordCount': {{ page.content | number_of_words }},
'environment': '{{ jekyll.environment }}'
});
</script>
Jekyll with Liquid Conditionals
Create _includes/data-layer-advanced.html:
<script>
window.dataLayer = window.dataLayer || [];
{% capture dataLayer %}
{
"pageType": "{{ page.layout }}",
"pageUrl": "{{ page.url | absolute_url }}",
"pageTitle": "{{ page.title | escape }}",
"wordCount": {{ page.content | number_of_words }},
{% if page.author %}
"author": "{{ page.author | escape }}",
{% endif %}
{% if page.date %}
"publishDate": "{{ page.date | date: "%Y-%m-%d" }}",
{% endif %}
{% if page.categories %}
"categories": {{ page.categories | jsonify }},
"primaryCategory": "{{ page.categories | first }}",
{% endif %}
{% if page.tags %}
"tags": {{ page.tags | jsonify }},
{% endif %}
{% if page.featured %}
"featured": {{ page.featured }},
{% endif %}
{% if page.series %}
"series": "{{ page.series }}",
{% endif %}
"environment": "{{ jekyll.environment }}"
}
{% endcapture %}
window.dataLayer.push({{ dataLayer }});
</script>
Gatsby Data Layer Implementation
Plugin Configuration
Add to gatsby-config.js:
{
resolve: "gatsby-plugin-google-tagmanager",
options: {
id: process.env.GATSBY_GTM_ID,
// Default data layer pushed on every page
defaultDataLayer: {
platform: "gatsby",
siteTitle: "My Site",
environment: process.env.GATSBY_ENV || "production"
},
// Include in development (usually false)
includeInDevelopment: false,
// Event name for route changes
routeChangeEventName: "gatsby-route-change"
}
}
Page-Level Data Layer
In page templates (e.g., src/templates/blog-post.js):
import React, { useEffect } from 'react';
const BlogPost = ({ data, pageContext }) => {
const post = data.markdownRemark;
useEffect(() => {
if (typeof window !== 'undefined' && window.dataLayer) {
window.dataLayer.push({
pageType: 'blog_post',
contentCategory: post.frontmatter.category,
author: post.frontmatter.author,
publishDate: post.frontmatter.date,
tags: post.frontmatter.tags,
wordCount: post.wordCount.words,
readingTime: Math.ceil(post.wordCount.words / 200),
featured: post.frontmatter.featured || false
});
}
}, [post]);
return (
<article>
<h1>{post.frontmatter.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
);
};
export const query = graphql`
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
date(formatString: "YYYY-MM-DD")
author
category
tags
featured
}
wordCount {
words
}
}
}
`;
export default BlogPost;
Gatsby Browser API for Route Changes
In gatsby-browser.js:
export const location, prevLocation }) => {
if (typeof window !== 'undefined' && window.dataLayer) {
// Only push if not the initial page load
if (prevLocation) {
window.dataLayer.push({
event: 'gatsby-route-change',
pagePath: location.pathname,
pageUrl: window.location.href,
pageTitle: document.title,
previousPath: prevLocation.pathname
});
}
}
};
Next.js Data Layer Implementation
App-Level Data Layer
In pages/_app.js:
import { useEffect } from 'react';
import { useRouter } from 'next/router';
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
// Initial page load data layer
if (typeof window !== 'undefined' && window.dataLayer) {
window.dataLayer.push({
platform: 'nextjs',
environment: process.env.NODE_ENV,
pageType: pageProps.pageType || 'page',
pageUrl: window.location.href,
pagePath: router.pathname
});
}
}, [router.pathname, pageProps]);
useEffect(() => {
const handleRouteChange = (url) => {
if (typeof window !== 'undefined' && window.dataLayer) {
window.dataLayer.push({
event: 'pageview',
pagePath: url,
pageUrl: window.location.href,
pageTitle: document.title
});
}
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return <Component {...pageProps} />;
}
export default MyApp;
Page-Level Data Layer with getStaticProps
In blog post page (e.g., pages/blog/[slug].js):
import { useEffect } from 'react';
export default function BlogPost({ post }) {
useEffect(() => {
if (typeof window !== 'undefined' && window.dataLayer) {
window.dataLayer.push({
pageType: 'blog_post',
contentCategory: post.category,
author: post.author,
publishDate: post.date,
tags: post.tags,
wordCount: post.wordCount,
featured: post.featured || false
});
}
}, [post]);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
export async function getStaticProps({ params }) {
// Fetch post data
const post = await getPostBySlug(params.slug);
return {
props: {
post,
pageType: 'blog_post' // Pass to _app.js
}
};
}
11ty Data Layer Implementation
Nunjucks Template
Create _includes/data-layer.njk:
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'pageType': '{{ layout }}',
'pageUrl': '{{ page.url }}',
'pageTitle': '{{ title }}',
{% if author %}
'author': '{{ author }}',
{% endif %}
{% if date %}
'publishDate': '{{ date | dateToFormat("yyyy-MM-dd") }}',
{% endif %}
{% if category %}
'contentCategory': '{{ category }}',
{% endif %}
{% if tags %}
'tags': {{ tags | dump | safe }},
{% endif %}
'environment': '{{ env.environment or "production" }}'
});
</script>
JavaScript Data File
Create _data/dataLayer.js:
module.exports = {
platform: "eleventy",
environment: process.env.ELEVENTY_ENV || "production",
buildDate: new Date().toISOString()
};
Use in templates:
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
platform: '{{ dataLayer.platform }}',
environment: '{{ dataLayer.environment }}',
buildDate: '{{ dataLayer.buildDate }}',
pageUrl: '{{ page.url }}',
pageTitle: '{{ title }}'
});
</script>
Common Data Layer Patterns
User Engagement Tracking
Add to any static site:
// Track user engagement metrics
(function() {
let startTime = Date.now();
let maxScroll = 0;
// Track time on page
setInterval(function() {
let timeOnPage = Math.floor((Date.now() - startTime) / 1000);
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'event': 'engagement',
'timeOnPage': timeOnPage,
'maxScrollDepth': maxScroll
});
}, 30000); // Every 30 seconds
// Track scroll depth
window.addEventListener('scroll', function() {
let scrollPercent = Math.floor((window.pageYOffset / (document.documentElement.scrollHeight - window.innerHeight)) * 100);
if (scrollPercent > maxScroll) {
maxScroll = scrollPercent;
}
}, { passive: true });
})();
Returning Visitor Detection
// Detect returning visitors
(function() {
let sessionCount = parseInt(localStorage.getItem('sessionCount') || '0') + 1;
localStorage.setItem('sessionCount', sessionCount);
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'userType': sessionCount > 1 ? 'returning' : 'new',
'sessionCount': sessionCount,
'lastVisit': localStorage.getItem('lastVisit') || 'never'
});
localStorage.setItem('lastVisit', new Date().toISOString());
})();
Content Performance Metrics
// Track content engagement
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
'contentMetrics': {
'hasVideo': document.querySelectorAll('video, iframe[src*="youtube"]').length > 0,
'hasImages': document.querySelectorAll('article img').length,
'hasCode': document.querySelectorAll('pre code').length > 0,
'linkCount': document.querySelectorAll('article a').length,
'headingCount': document.querySelectorAll('h2, h3, h4').length
}
});
GTM Variables from Data Layer
In GTM, create Data Layer Variables to access values:
Variable Name: Author
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
author
Variable Name: Content Category
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
contentCategory
Variable Name: Tags
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
tags
Use these variables in tags and triggers.
Testing Data Layer
Browser Console
// View entire data layer
console.log(window.dataLayer);
// View specific push
window.dataLayer.forEach((item, index) => {
console.log(`Data Layer[${index}]:`, item);
});
// Check for specific value
console.log('Author:', window.dataLayer.find(item => item.author)?.author);
GTM Preview Mode
- Open GTM → Click Preview
- Enter site URL
- GTM Debug panel opens
- Click any event
- View Data Layer tab
- Verify all values present
Data Layer Debugger
Install dataLayer Checker Chrome Extension
Best Practices
1. Initialize Before GTM
<!-- Data layer BEFORE GTM -->
<script>
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({...});
</script>
<!-- Then load GTM -->
<script>
(function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-XXXXXX');
</script>
2. Don't Include PII
// BAD - Contains PII
window.dataLayer.push({
'userEmail': 'user@example.com', // Don't do this
'userName': 'John Doe' // Don't do this
});
// GOOD - No PII
window.dataLayer.push({
'userType': 'returning',
'userRole': 'subscriber'
});
3. Consistent Naming
Use camelCase for consistency:
// Good
window.dataLayer.push({
'contentCategory': 'tutorials',
'publishDate': '2024-01-15',
'pageType': 'blog_post'
});
// Avoid
window.dataLayer.push({
'content_category': 'tutorials', // snake_case
'PublishDate': '2024-01-15', // PascalCase
'page-type': 'blog_post' // kebab-case
});
4. Validate Data Types
window.dataLayer.push({
'wordCount': parseInt('1500'), // Number, not string
'featured': Boolean(featured), // Boolean, not truthy value
'tags': Array.isArray(tags) ? tags : [], // Always array
'publishDate': formatDate(date) // Consistent date format
});
Next Steps
- Set Up Event Tracking - Use data layer in events
- Configure GTM Tags - Leverage data layer variables
- Debug Issues - Troubleshoot data layer problems
Related Resources
- GTM Data Layer Fundamentals - Universal concepts
- GA4 Custom Dimensions - Use data layer values in GA4
- Privacy Compliance - PII handling in data layer