Data Layer Configuration for Netlify CMS / Decap CMS | OpsBlu Docs

Data Layer Configuration for Netlify CMS / Decap CMS

Complete data layer implementation for Google Tag Manager on Netlifycms. Covers page metadata, user properties, ecommerce data, and custom event variables.

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):

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

  1. Open GTM → Click Preview
  2. Enter site URL
  3. GTM Debug panel opens
  4. Click any event
  5. View Data Layer tab
  6. 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