Enhancing Analytics in Sitecore Headless/XM Cloud With Next.js

A guide to implementing a data layer

March 20, 2024

By Sohrab Saboori

The web development world is constantly evolving, and keeping track of how visitors interact with your site is crucial for success. Enter the data layer: a behind-the-scenes hero that acts like an invisible tracker. It sheds light on user behavior, revealing valuable insights that can guide your development decisions.

This guide is tailored for developers working with Sitecore Headless and Next.js, aiming to equip you with the skills to integrate a data layer effectively. We'll cover the essentials, from setting up the data layer in your Next.js app and managing configurations in Sitecore, to fetching data with GraphQL and dynamically applying settings. By the end of this guide, you'll have a comprehensive analytics setup that provides valuable insights into user navigation, empowering you to enhance your website's user experience.

Adding a Data Layer in Next.js

When integrating a data layer in a Next.js application, developers have two primary approaches: a manual method using a custom hook and the useEffect hook, and a more automated method utilizing the @next/third-parties/google package.

1. Manual Method With Custom Hooks

The manual approach involves directly manipulating the data layer within your Next.js application. This method gives you full control over the data layer's initialization and updates. Here's a brief overview:

  • Custom Hook: Create a custom hook, useDataLayer, to manage the data layer. This hook uses the useEffect hook to initialize the data layer and can include functions to update it based on user interactions or other events.

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

    const useDataLayer = () => { useEffect(() => { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'eventName', page: { title: document.title, url: window.location.href, }, }); }, []); };

    export default useDataLayer;

  • Implementation in _app.js: To ensure the data layer is initialized on every page load, we use the useDataLayer hook within the _app.js file, which acts as the root for all pages in your Next.js application. This setup allows us to maintain a global data layer that is consistent across the entire site.

      // pages/_app.js
    import useDataLayer from '../hooks/useDataLayer';

    function MyApp({ Component, pageProps }) { useDataLayer(); return <Component {...pageProps} />; }

    export default MyApp;

    In addition to initializing the data layer, this approach provides an opportunity to integrate SitecorePageProps into the data layer. For example, if you're using Sitecore XM Cloud for your headless CMS, you might have access to properties like locale and id for each page. These properties are part of the page-specific data that Sitecore XM Cloud provides to your Next.js application. You can include these properties in the data layer to enhance your analytics with context-specific information, allowing for more granular analysis of user behavior across different regions and pages of your site.

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

    const useDataLayer = (sitecorePageProps) => { useEffect(() => { // Initialize the data layer with global data window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: 'pageview', page: { title: pageProps?.layoutData?.sitecore?.route.displayName, url: window.location.href, id: sitecorePageProps.layoutData?.sitecore?.route.itemId, locale: sitecorePageProps.locale, }, }); }, [sitecorePageProps.layoutData?.sitecore?.route.itemId, sitecorePageProps.locale]); };

    export default useDataLayer;

    Then, in _app.js, you can pass the SitecorePageProps to the useDataLayer hook

      // pages/_app.js
    import useDataLayer from '../hooks/useDataLayer';

    function App({ Component, pageProps }: AppProps<SitecorePageProps>): JSX.Element { const { dictionary, ...rest } = pageProps; useDataLayer(pageProps); return (

      <span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">&lt;</span></span></span></span><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name">I18nProvider</span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"> </span></span></span></span><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr">lngDict</span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">=</span></span></span></span><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string">{dictionary}</span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"> </span></span></span></span><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr">locale</span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">=</span></span></span></span><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string"><span class="xml"><span class="hljs-tag"><span class="hljs-string">{pageProps.locale}</span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">&gt;</span></span></span></span></span><span class="xml">
          </span><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">&lt;</span></span></span></span><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name">Component</span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag"> {</span></span></span></span><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr"><span class="xml"><span class="hljs-tag"><span class="hljs-attr">...rest</span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">} /&gt;</span></span></span></span></span><span class="xml">
      </span><span class="hljs-tag"><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">&lt;/</span></span></span></span><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name"><span class="xml"><span class="hljs-tag"><span class="hljs-name">I18nProvider</span></span></span></span></span></span></span><span class="xml"><span class="hljs-tag"><span class="xml"><span class="hljs-tag">&gt;</span></span></span></span></span>
    );
    

    }

    By integrating SitecorePageProps into the data layer, you can capture valuable context about each page, such as its unique ID and locale, which can be instrumental in segmenting and analyzing user behavior across different regions and pages of your site.

  • Dynamic Updates: You can further extend the data layer by dynamically updating it based on user interactions or other events. For example, you might add a function to your custom hook to track button clicks:

      // Add to hooks/useDataLayer.js
    const trackButtonClick = (buttonName) => {
    window.dataLayer.push({
    event: 'button_click',
    button: {
    name: buttonName,
    },
    });
    };
    

    Then, use this function in your components to track specific button clicks:

      // In a component file
    import { trackButtonClick } from '../hooks/useDataLayer';

    const MyButton = () => ( <button onClick={() => trackButtonClick('my_button')}>Click Me</button> );

    Note for TypeScript Users:

    If you're using TypeScript and encounter the error Property 'dataLayer' does not exist on type 'Window & typeof globalThis', you'll need to extend the Window interface to include the dataLayer property. This ensures that TypeScript recognizes dataLayer as a valid property on the global Window object. Create a new file named global.d.ts or window.d.ts in your project's root directory or a directory included in your TypeScript configuration.

      // global.d.ts 

    // Extend the Window interface declare global { interface Window { dataLayer: any[]; } }

    export {};

    Make sure your TypeScript configuration file (tsconfig.json) includes the declaration file in its include array.

    
    {
    "include": ["next-env.d.ts", "/*.ts", "/*.tsx", "global.d.ts"]
    }
    

    By following these steps, you'll resolve the TypeScript error and ensure that your data layer implementation is compatible with TypeScript's type checking.

2. Automated Method With @next/third-parties/google

For developers looking for a more streamlined integration with Google Analytics and Google Tag Manager, the @next/third-parties/google package offers a convenient solution:

  • Installation: Install the @next/third-parties/google package in your Next.js project.
  • Sending Events: Use the sendGAEvent or sendGTMEvent function to send events to Google Analytics or Google Tag Manager, respectively. These functions abstract away the direct manipulation of the data layer, simplifying the event tracking process.

      import { sendGTMEvent } from '@next/third-parties/google';
    

    export function EventButton() { return ( <button onClick={() => sendGTMEvent({ event: 'buttonClicked', value: 'xyz' })}> Send Event </button> ); }

    This automated method is ideal for projects that require a straightforward integration with Google's analytics tools and do not need extensive customization of the data layer.

Both manual and automated methods have their place in Next.js development. The manual approach offers more control and customization, while the automated method provides ease of use and quick integration with Google's analytics platforms. When deciding between these two approaches, consider your project's specific requirements and the level of customization needed.

Manage Configurations in Sitecore

Sitecore offers a flexible and robust content management system that can be leveraged to manage your tracking configurations centrally. By creating a dedicated template for your tracking needs, you can streamline the process of configuring and maintaining your analytics setup.

Creating a Tracking Configuration Template

  1. Template Creation: In Sitecore, create a new template named Campaign (or any name that suits your needs) to store your tracking configurations. This template will act as a blueprint for your tracking items.
  2. Custom Fields: Add custom fields to your Campaign template to represent different aspects of your tracking configuration. For example, you might include fields for DataLayerName and DataLayerValue, along with any other relevant properties such as StartDate, EndDate, or CampaignType.

    Screenshot of a data entry form titled "Test Campaign" with fields for campaign link and type, and an option to enroll in an engagement plan.

    Screenshot of a user interface for selecting a start and end date with "Now" and "Clear" options.

  3. Creating Items: Use this template to create items that represent individual tracking configurations. Populate the fields with the appropriate values for each campaign or tracking scenario.

Fetching Configurations With GraphQL

Once you have your tracking configurations set up in Sitecore, the next step is to fetch this data for use in your Next.js application. GraphQL provides a powerful and efficient way to query your Sitecore instance for the specific tracking data you need.

To learn more about how to use GraphQL to fetch data from Sitecore in a Next.js context, check out my previous blog post: Custom Hooks for Next.js in Sitecore XM Cloud. In this post, I cover the basics of setting up GraphQL queries in Next.js and how you can create custom hooks to streamline fetching and using Sitecore data in your Next.js components.

By leveraging GraphQL and the techniques outlined in the blog, you can efficiently retrieve your tracking configurations from Sitecore and prepare them for integration into your data layer.

Integrating the Data Layer With a Custom Hook

After fetching the tracking configuration data from Sitecore, you can utilize a custom hook to dynamically push the relevant data to the data layer in your Next.js application. This approach allows you to apply the data layer settings either globally in app.tsx or selectively on specific pages.

Here's an example of a custom hook, useFetchDataLayerItems, that fetches data and updates the data layer:

// useFetchItemFields.ts
import { useEffect, useState } from 'react';
import { useSitecoreContext } from '@sitecore-jss/sitecore-jss-react';

type JsonValue = { value: string | boolean | number | null; id?: string; url?: string; name?: string; displayName?: string; fields?: { [key: string]: { value: string | {} | null; }; }; };

type Field = { name: string; jsonValue: JsonValue | JsonValue[] | null; };

type Result = { name: string; fields: Field[]; };

type Children = { total: number; results: Result[]; pageInfo: { endCursor: string; hasNext: boolean; }; };

type Item = { id: string; name: string; children: Children; };

type Data = { item: Item; };

export type ItemProps = { data: Data; };

export const useFetchDataLayerItems = () => { const [itemFields, setItemFields] = useState<Result[]>([]); const { sitecoreContext } = useSitecoreContext();

useEffect(() => { const fetchData = async () => { try { const apiUrl ='/api/graphQL/GetDatalayerItems' // Adjusted endpoint name

    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(apiUrl, {
      <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
      <span class="hljs-attr">headers</span>: {
        <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
      },
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
        <span class="hljs-attr">sitecoreLanguage</span>: sitecoreContext?.language ?? <span class="hljs-string">'en'</span>,
      }),
    });

    <span class="hljs-keyword">if</span> (!response.ok) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string"><span class="hljs-string">`HTTP error! Status: </span><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst"><span class="hljs-string"><span class="hljs-subst">${response.status}</span></span></span></span></span><span class="hljs-string">`</span></span>);
    }

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> response.json();

    <span class="hljs-comment">// Assuming the API returns the result directly without needing JSON.parse</span>
    <span class="hljs-comment">// Adjust according to your actual API response structure</span>
    setItemFields(result);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching item fields:'</span>, err);
  }
};

fetchData();

}, [sitecoreContext?.language]); // Separate useEffect for dataLayer logic useEffect(() => { if (typeof window !== 'undefined' && itemFields?.length > 0) { // Ensure dataLayer is initialized window.dataLayer = window.dataLayer || [];

  itemFields.forEach(<span class="hljs-function"><span class="hljs-function">(</span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">item, index</span></span></span></span></span><span class="hljs-function">) =&gt;</span></span> {
    <span class="hljs-keyword">const</span> endDateField = item.fields.find(<span class="hljs-function"><span class="hljs-function">(</span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">field</span></span></span></span></span><span class="hljs-function">) =&gt;</span></span> field.name === <span class="hljs-string">'EndDate'</span>)
      ?.jsonValue <span class="hljs-keyword">as</span> JsonValue;

    <span class="hljs-comment">// Check if endDateField exists and is not the placeholder date</span>
    <span class="hljs-keyword">if</span> (
      endDateField == <span class="hljs-literal">null</span> ||
      (endDateField &amp;&amp;
        endDateField.value &amp;&amp;
        (<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(endDateField.value.toString()) &gt; <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>() ||
          endDateField.value.toString() === <span class="hljs-string">'0001-01-01T00:00:00Z'</span>))
    ) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(endDateField?.value?.toString()) &lt; <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>());
      <span class="hljs-keyword">const</span> channelField = item.fields.find(<span class="hljs-function"><span class="hljs-function">(</span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">field</span></span></span></span></span><span class="hljs-function">) =&gt;</span></span> field.name === <span class="hljs-string">'Channel'</span>)
        ?.jsonValue <span class="hljs-keyword">as</span> JsonValue;
      <span class="hljs-keyword">const</span> campaignGroupField = item.fields.find(<span class="hljs-function"><span class="hljs-function">(</span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">field</span></span></span></span></span><span class="hljs-function">) =&gt;</span></span> field.name === <span class="hljs-string">'Campaign group'</span>)
        ?.jsonValue <span class="hljs-keyword">as</span> JsonValue;
      <span class="hljs-keyword">const</span> campaignFacetField = item.fields.find(<span class="hljs-function"><span class="hljs-function">(</span><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">field</span></span></span></span></span><span class="hljs-function">) =&gt;</span></span> field.name === <span class="hljs-string">'Campaign facet 1'</span>)
        ?.jsonValue <span class="hljs-keyword">as</span> JsonValue;

      <span class="hljs-built_in">window</span>.dataLayer.push({
        <span class="hljs-attr">Source</span>: channelField?.name || <span class="hljs-string">'Unknown Source'</span>,
        <span class="hljs-attr">Medium</span>: <span class="hljs-string">'Deprecated - Display'</span>,
        <span class="hljs-attr">Campaign</span>: campaignGroupField?.name || <span class="hljs-string">'Unknown Campaign'</span>,
        <span class="hljs-attr">Content</span>: campaignFacetField?.name || <span class="hljs-string">'Unknown Content'</span>,
      });
    }
  });
}

}, [itemFields]);

return itemFields; };

To apply the data layer settings globally, you can use the useFetchDataLayerItems hook in your app.tsx file:

// pages/_app.tsx
import { useFetchDataLayerItems } from '../hooks/useFetchDataLayerItems';

function MyApp({ Component, pageProps }) { useFetchDataLayerItems();

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

export default MyApp;

Alternatively, if you want to apply the data layer settings only on specific pages, you can use the hook directly within those page components:

// pages/somePage.tsx
import { useFetchDataLayerItems } from '../hooks/useFetchDataLayerItems';

export default function SomePage() { useFetchDataLayerItems();

return ( // Page content ); }

By using this custom hook, you can ensure that your data layer is dynamically updated with the relevant tracking configurations from Sitecore, providing a flexible and scalable solution for your analytics needs.

Enhancing Analytics in Sitecore Headless With Next.js

In this guide, we've demonstrated how to integrate a dynamic data layer into a Sitecore Headless and Next.js application, enhancing its analytics capabilities. We explored both manual and automated methods for implementing the data layer, managing tracking configurations in Sitecore, and applying them dynamically in Next.js.

By effectively utilizing a data layer, you can gain valuable insights into user behavior and site performance, enabling data-driven decisions that improve user experience and marketing strategies. Whether you opt for the manual or automated approach, the key is to ensure a seamless integration with your analytics tools.



Photo of Fishtank employee Sohrab Saboori

Sohrab Saboori

Senior Full-Stack Developer

Sohrab is a Senior Front-End Developer with extensive experience in React, Next.js, JavaScript, and TypeScript. Sohrab is committed to delivering outstanding digital solutions that not only meet but exceed clients' expectations. His expertise in building scalable and efficient web applications, responsive websites, and e-commerce platforms is unparalleled. Sohrab has a keen eye for detail and a passion for creating seamless user experiences. He is a problem-solver at heart and enjoys working with clients to find innovative solutions to their digital needs. When he's not coding, you can find him lifting weights at the gym, pounding the pavement on the run, exploring the great outdoors, or trying new restaurants and cuisines. Sohrab believes in a healthy and balanced lifestyle and finds that these activities help fuel his creativity and problem-solving skills.

Second CTA Ogilvy's Legacy

Today, David Ogilvy's influence can still be felt in the world of advertising.

Ogilvy's Influence Example
Emphasis on research Market research is a crucial part of any successful advertising campaign
Focus on headlines A strong headline can make the difference between an ad that is noticed and one that is ignored
Use of visuals Compelling images and graphics are essential for capturing audience attention