How to Correctly Track Clicks and Page Referrers on Sitecore XM Cloud With Next.js With GTM

Accurate click and referrer tracking in Sitecore XM Cloud with Next.js and Google Tag Manager: Implementing a custom hook solution

May 22, 2024

By Sohrab Saboori

Integrating GTM, Next.js & XM Cloud

In this blog post, we'll discuss the challenges of tracking clicks and page referrers in a Sitecore XM Cloud and Next.js environment using Google Tag Manager (GTM). We will explore the issues that arise due to the nature of single-page applications (SPAs) like Next.js, where GTM often fails to track route changes and click events accurately. Specifically, we'll address problems where the referrer URL remains static and click events capture the destination URL instead of the current page URL. Finally, we'll provide a detailed solution using a custom hook in Next.js to accurately push data to GTM.

Problem Overview

When using GTM with a Next.js application, several issues can arise:

  • GTM Click Tracking: GTM's native click tracking often captures the destination page URL instead of the current page URL, leading to inaccurate data.

    Screenshot showing data on user clicks for internal navigation, including source and destination pages.

    Screenshot showing URL and HTTP referrer fields in Google Tag Assistant.

  • Route Changes: In Next.js, route changes happen client-side, which GTM cannot always follow accurately. As a result, the HTTP referrer often shows the initial referrer and does not update with each route change.

  • Internal Navigation: For internal page navigations, the referrer URL captured by GTM remains static and doesn't reflect the actual navigation flow within the application.

Implementing the Solution

To address these issues, we'll implement a custom solution using a data layer push in GTM. This approach involves creating a custom hook in Next.js that accurately captures click events and route changes and pushes the correct data to the GTM data layer.

For a detailed guide on implementing a data layer, refer to this blog post.

Step 1: Create a Custom Hook

We'll start by creating a custom hook in Next.js to handle click events and route changes. This hook will capture necessary data such as the current page URL, destination URL, link text, and referrer URL.

Custom Hook: useDataLayerClick.js

import { useEffect } from 'react';
import { useRouter } from 'next/router';

const useDataLayerClick = () => {
  const router = useRouter();

  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    let previousUrl = '';
    let lastLinkClicked = null;

    // Initialize custom referrer if not set
    if (!sessionStorage.getItem('customReferrer')) {
      sessionStorage.setItem('customReferrer', document.referrer || window.location.href);
    }

    const handleClick = (event) => {
      const target = event.target.closest('a');
      if (target) {
        lastLinkClicked = {
          text: target.textContent || target.innerText,
          classes: Array.from(target.classList).join(' '),
          href: target.getAttribute('href') || '', // Ensure href is a string
          target: target.getAttribute('target'),
        };
      }
    };

    const handleRouteChangeStart = (url) => {
      previousUrl = window.location.href;
    };

    const handleRouteChangeComplete = (url) => {
      // Update custom referrer with the previous URL
      const currentReferrer = sessionStorage.getItem('customReferrer') || '';
      sessionStorage.setItem('customReferrer', previousUrl);
      if (lastLinkClicked && (!lastLinkClicked.target || lastLinkClicked.target === '_self')) {
        // Ensure internal link
        window.dataLayer.push({
          event: 'userclick',
          eventType: 'internalNavigation',
          sourcePage: previousUrl,
          destinationPage: window.location.href,
          referrer: currentReferrer, // Use custom referrer
          clickTitle: lastLinkClicked.text,
          clickClass: lastLinkClicked.classes,
          clickTarget: lastLinkClicked.target || '',
          linkHref: lastLinkClicked.href,
          gtm: {
            uniqueEventId: Math.floor(Math.random() * 10000),
          },
        });
        lastLinkClicked = null; // Reset after handling
      }
    };

    const handleDocumentClick = (event) => {
      handleClick(event);
      if (lastLinkClicked) {
        // Check if the href is a relative path and prepend the current domain if necessary
        let destinationPage = lastLinkClicked.href;
        if (!destinationPage.startsWith('http')) {
          destinationPage = window.location.origin + destinationPage;
        }

        if (
          lastLinkClicked.target === '_blank' ||
          lastLinkClicked.href.includes('-/media/') ||
          (lastLinkClicked.href.includes('http') &&
            !lastLinkClicked.href.includes(window.location.hostname))
        ) {
          window.dataLayer.push({
            event: 'userclick',
            eventType: 'externalOrNewTab',
            sourcePage: window.location.href,
            destinationPage: destinationPage,
            referrer: sessionStorage.getItem('customReferrer'), // Use custom referrer
            clickTitle: lastLinkClicked.text,
            clickClass: lastLinkClicked.classes,
            clickTarget: lastLinkClicked.target || '',
            linkHref: lastLinkClicked.href,

            gtm: {
              uniqueEventId: Math.floor(Math.random() * 10000),
            },
          });
          lastLinkClicked = null; // Reset after handling
        }
      }
    };

    document.addEventListener('click', handleDocumentClick);
    router.events.on('routeChangeStart', handleRouteChangeStart);
    router.events.on('routeChangeComplete', handleRouteChangeComplete);

    return () => {
      document.removeEventListener('click', handleDocumentClick);
      router.events.off('routeChangeStart', handleRouteChangeStart);
      router.events.off('routeChangeComplete', handleRouteChangeComplete);
    };
  }, []); // Empty dependency array ensures setup only runs once

  return null;
};

export default useDataLayerClick;

The first part of our custom hook ensures that we have a meaningful referrer value to work with, even if the user directly navigates to our site.

// Initialize custom referrer if not set or if it's an empty string
if (!sessionStorage.getItem('customReferrer')) {
  const initialReferrer = document.referrer && document.referrer !== '' ? document.referrer : window.location.href;
  sessionStorage.setItem('customReferrer', initialReferrer);
}
  • Purpose: This snippet initializes a custom referrer in session storage.
  • Fallback Mechanism: It uses document.referrer if available, or window.location.href if document.referrer is empty. This ensures we always have a meaningful referrer value.

Click Event Handling

Next, we handle all click events on anchor tags (<a>). This part of the hook captures essential details of the clicked link.

const handleClick = (event) => {
  const target = event.target.closest('a');
  if (target) {
    lastLinkClicked = {
      text: target.textContent || target.innerText,
      classes: Array.from(target.classList).join(' '),
      href: target.getAttribute('href') || '', // Ensure href is a string
      target: target.getAttribute('target'),
    };
  }
};

Explanation:

  • Capture Details: Captures the link text, href, classes, and target attributes.
  • Storage: Stores these details in the lastLinkClicked variable to be used later.

Route Change Handling

For internal navigations, we handle route changes using Next.js router events. This part ensures that our custom referrer is updated correctly and the data is pushed to GTM.

Route Change Start

const handleRouteChangeStart = (url) => {
  previousUrl = window.location.href;

};

Route Change Complete

   const handleRouteChangeComplete = (url) => {
      // Update custom referrer with the previous URL
      const currentReferrer = sessionStorage.getItem('customReferrer') || '';
      sessionStorage.setItem('customReferrer', previousUrl);
      if (lastLinkClicked && (!lastLinkClicked.target || lastLinkClicked.target === '_self')) {
        // Ensure internal link
        window.dataLayer.push({
          event: 'userclick',
          eventType: 'internalNavigation',
          sourcePage: previousUrl,
          destinationPage: window.location.href,
          referrer: currentReferrer, // Use custom referrer
          clickTitle: lastLinkClicked.text,
          clickClass: lastLinkClicked.classes,
          clickTarget: lastLinkClicked.target || '',
          linkHref: lastLinkClicked.href,
          gtm: {
            uniqueEventId: Math.floor(Math.random() * 10000),
          },
        });
        lastLinkClicked = null; // Reset after handling
      }
    };

Explanation:

  • Route Change Start: Captures the current URL before navigation starts, storing it as previousUrl.
  • Route Change Complete: Updates the custom referrer with the previous URL and pushes the click event data to the GTM data layer. This includes source and destination pages, link text, classes, href, target, and the custom referrer.

For external links, links with target="_blank", and media files, we immediately push the click data to the GTM data layer when the link is clicked.

const handleDocumentClick = (event) => {
  handleClick(event);
  if (lastLinkClicked) {
    // Check if the href is a relative path and prepend the current domain if necessary
    let destinationPage = lastLinkClicked.href;
    if (!destinationPage.startsWith('http')) {
      destinationPage = window.location.origin + destinationPage;
    }

    if (
      lastLinkClicked.target === '_blank' ||
      lastLinkClicked.href.includes('/media/') ||
      (lastLinkClicked.href.includes('http') &&
        !lastLinkClicked.href.includes(window.location.hostname))
    ) {
      window.dataLayer.push({
        event: 'userclick',
        eventType: 'externalOrNewTab',
        sourcePage: window.location.href,
        destinationPage: destinationPage,
        clickTitle: lastLinkClicked.text,
        clickClass: lastLinkClicked.classes,
        clickTarget: lastLinkClicked.target || '',
        referrer: sessionStorage.getItem('customReferrer'),
        gtm: {
          uniqueEventId: Math.floor(Math.random() * 10000),
        },
      });
      lastLinkClicked = null; // Reset after handling
    }
  }
};

Step 2: Integrate the Hook in app.tsx

Next, we'll integrate this custom hook into our app.tsx file to ensure it runs globally across the application.

import { useEffect } from 'react';
import { useRouter } from 'next/router';
import useDataLayerClick from '../hooks/useDataLayerClick';

const MyApp = ({ Component, pageProps }) => {
  useDataLayerClick();

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

export default MyApp;

Results of Implementing the Custom Hook

After implementing the custom hook, we observed the following improvements in GTM:

  1. Accurate Click Tracking: Click events are now tracked correctly, capturing the link text, classes, href, and target attributes accurately.
  2. Correct Page Referrer: The referrer URL now updates correctly on each route change, reflecting the actual navigation flow within the application.
  3. Improved Data Accuracy: For external links and links with target="_blank", the destination URL is correctly captured, even if it's a relative path.

Screenshot showing data on user clicks for internal navigation, including source and destination pages.

Screenshot of a user click event data in a web analytics interface showing an external navigation event.

Final Word on GTM & Next.js

By implementing this custom solution, you can ensure accurate tracking of clicks and page referrers in a Next.js application integrated with Sitecore XM Cloud. This approach overcomes the limitations of GTM in handling SPAs and provides reliable data for your analytics needs.

For more detailed insights, refer to the comprehensive guide on enhancing analytics in Sitecore Headless XM Cloud with Next.js.



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.