PayloadCMS Data Layer for GTM | OpsBlu Docs

PayloadCMS Data Layer for GTM

Build a comprehensive data layer for Google Tag Manager using PayloadCMS content and user data for advanced tracking.

A properly structured data layer enables advanced tracking, consistent data across marketing platforms, and easier tag management without code changes.

What is a Data Layer?

A data layer is a JavaScript object containing structured information about the page, content, and user. GTM reads this data to:

  • Fire tags conditionally based on page/content type
  • Pass dynamic values to tags (content IDs, user data, etc.)
  • Track events with rich context
  • Ensure consistent data across all marketing pixels

Base Data Layer Implementation

Initialize Data Layer

File: pages/_app.js or app/layout.js

import { useEffect } from 'react';

function MyApp({ Component, pageProps }) {
  useEffect(() => {
    // Initialize data layer
    window.dataLayer = window.dataLayer || [];

    // Push initial page data
    window.dataLayer.push({
      event: 'dataLayerReady',
      site: {
        name: 'Your Site Name',
        environment: process.env.NODE_ENV,
        version: '1.0.0',
      },
    });
  }, []);

  return <Component {...pageProps} />;
}

Content-Specific Data Layers

Blog Post Data Layer

// pages/blog/[slug].js
import { useEffect } from 'react';

export default function BlogPost({ post }) {
  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'contentView',
      content: {
        id: post.id,
        title: post.title,
        type: 'blog_post',
        category: post.category?.name,
        author: post.author?.name,
        publishDate: post.publishedDate,
        tags: post.tags?.map(tag => tag.name) || [],
      },
      page: {
        type: 'blog_post',
        url: window.location.href,
        path: window.location.pathname,
      },
    });
  }, [post]);

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

export async function getStaticProps({ params }) {
  const res = await fetch(
    `${process.env.PAYLOAD_URL}/api/posts?where[slug][equals]=${params.slug}`
  );
  const data = await res.json();

  return {
    props: {
      post: data.docs[0],
    },
  };
}

Category/Listing Pages

// pages/blog/index.js
import { useEffect } from 'react';

export default function BlogIndex({ posts, category }) {
  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'listingView',
      listing: {
        type: 'blog',
        category: category?.name || 'all',
        itemCount: posts.length,
        items: posts.map(post => ({
          id: post.id,
          title: post.title,
          category: post.category?.name,
        })),
      },
    });
  }, [posts, category]);

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{/* Post preview */}</article>
      ))}
    </div>
  );
}

User Data Layer

Track Logged-In Users

// hooks/useDataLayer.js
import { useEffect } from 'react';
import { useAuth } from './useAuth';

export function useUserDataLayer() {
  const { user } = useAuth();

  useEffect(() => {
    window.dataLayer = window.dataLayer || [];

    if (user) {
      window.dataLayer.push({
        event: 'userDataReady',
        user: {
          id: user.id,
          email: user.email, // Hash this if needed for privacy
          role: user.role,
          memberSince: user.createdAt,
          isAuthenticated: true,
        },
      });
    } else {
      window.dataLayer.push({
        event: 'userDataReady',
        user: {
          isAuthenticated: false,
        },
      });
    }
  }, [user]);
}

// Use in _app.js
function MyApp({ Component, pageProps }) {
  useUserDataLayer();

  return <Component {...pageProps} />;
}

Event-Based Data Layer Pushes

Form Submission Events

import { useState } from 'react';

const ContactForm = () => {
  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);

    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/contact-submissions`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: formData.get('name'),
          email: formData.get('email'),
          message: formData.get('message'),
        }),
      });

      if (response.ok) {
        window.dataLayer.push({
          event: 'formSubmission',
          form: {
            name: 'contact',
            id: 'contact_form',
            method: 'POST',
            fields: {
              name: formData.get('name'),
              email: formData.get('email'),
              // Don't include sensitive data like message content
            },
          },
        });
      }
    } catch (error) {
      window.dataLayer.push({
        event: 'formError',
        form: {
          name: 'contact',
          error: error.message,
        },
      });
    }
  };

  return (
    <form
      {/* Form fields */}
    </form>
  );
};

Search Events

const SearchBar = () => {
  const [query, setQuery] = useState('');

  const handleSearch = async (e) => {
    e.preventDefault();

    const response = await fetch(
      `${process.env.NEXT_PUBLIC_PAYLOAD_URL}/api/posts?where[title][like]=${query}`
    );
    const data = await response.json();

    window.dataLayer.push({
      event: 'search',
      search: {
        query: query,
        resultsCount: data.totalDocs,
        hasResults: data.totalDocs > 0,
      },
    });
  };

  return (
    <form
      <input
        type="search"
        value={query} => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <button type="submit">Search</button>
    </form>
  );
};

Download Tracking

const DownloadButton = ({ file }) => {
  const handleDownload = () => {
    window.dataLayer.push({
      event: 'fileDownload',
      file: {
        name: file.filename,
        type: file.mimeType,
        size: file.filesize,
        url: file.url,
      },
    });

    window.open(file.url, '_blank');
  };

  return (
    <button
      Download {file.filename}
    </button>
  );
};

import Link from 'next/link';

const NavLink = ({ href, label, category }) => {
  const handleClick = () => {
    window.dataLayer.push({
      event: 'navigationClick',
      navigation: {
        linkText: label,
        linkUrl: href,
        linkCategory: category,
      },
    });
  };

  return (
    <Link href={href}
      {label}
    </Link>
  );
};
const ExternalLink = ({ href, children }) => {
  const handleClick = () => {
    window.dataLayer.push({
      event: 'outboundClick',
      link: {
        url: href,
        domain: new URL(href).hostname,
        text: typeof children === 'string' ? children : '',
      },
    });
  };

  return (
    <a href={href} target="_blank" rel="noopener noreferrer"
      {children}
    </a>
  );
};

Enhanced Ecommerce Data Layer

Product Views (if applicable)

export default function ProductPage({ product }) {
  useEffect(() => {
    window.dataLayer.push({
      event: 'productView',
      ecommerce: {
        detail: {
          products: [{
            id: product.id,
            name: product.name,
            price: product.price,
            brand: product.brand,
            category: product.category?.name,
          }],
        },
      },
    });
  }, [product]);

  return <div>{/* Product content */}</div>;
}

Using Data Layer Variables in GTM

Create Variables in GTM

  1. Variables > New
  2. Variable Type: Data Layer Variable
  3. Data Layer Variable Name: Example: content.id
  4. Save

Common variables to create:

Variable Name Data Layer Path Use Case
Content ID content.id Content tracking
Content Type content.type Conditional triggers
User ID user.id User identification
User Role user.role Access-based tracking
Form Name form.name Form tracking
Search Query search.query Search tracking

Use in Tag Configuration

Example: Fire tag only on blog posts

  1. Create Trigger:

    • Type: Custom Event
    • Event Name: contentView
    • Condition: content.type equals blog_post
  2. Apply to Tag:


Testing the Data Layer

Browser Console Inspection

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

// Find specific events
window.dataLayer.filter(item => item.event === 'contentView');

// Check current values
window.dataLayer[window.dataLayer.length - 1];

GTM Preview Mode

  1. Open GTM, click Preview
  2. Enter your site URL
  3. In debug panel, click Data Layer tab
  4. Inspect each data layer push
  5. Verify variables populate correctly

Best Practices

1. Initialize Early

// Always initialize before GTM
window.dataLayer = window.dataLayer || [];
// Then load GTM script

2. Use Consistent Naming

  • camelCase for property names
  • Descriptive names: contentId not id
  • Consistent structure across pages

3. Don't Store Sensitive Data

Never include:

  • Passwords
  • Credit card numbers
  • Unencrypted PII

Hash sensitive data:

import crypto from 'crypto';

const hashedEmail = crypto
  .createHash('sha256')
  .update(email.toLowerCase().trim())
  .digest('hex');

window.dataLayer.push({
  user: {
    hashedEmail: hashedEmail,
  },
});

4. Handle Missing Data

window.dataLayer.push({
  content: {
    category: post.category?.name || 'Uncategorized',
    author: post.author?.name || 'Unknown',
  },
});

Server-Side Data Layer

Generate Data Layer Server-Side

// pages/blog/[slug].js
export async function getStaticProps({ params }) {
  const res = await fetch(
    `${process.env.PAYLOAD_URL}/api/posts?where[slug][equals]=${params.slug}`
  );
  const data = await res.json();
  const post = data.docs[0];

  return {
    props: {
      post,
      dataLayer: {
        content: {
          id: post.id,
          title: post.title,
          type: 'blog_post',
          category: post.category?.name,
        },
      },
    },
  };
}

export default function BlogPost({ post, dataLayer }) {
  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'contentView',
      ...dataLayer,
    });
  }, [dataLayer]);

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

Troubleshooting

Data Layer Not Defined

Cause: GTM loading before data layer initialization

Solution: Ensure data layer initialized before GTM script

Variables Showing Undefined

Cause: Incorrect data layer path or timing

Solution:

  • Check data layer structure in console
  • Verify path matches exactly (case-sensitive)
  • Ensure data pushed before variable accessed

Next Steps


Additional Resources