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.
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.
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.
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.
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);
}
document.referrer
if available, or window.location.href
if document.referrer
is empty. This ensures we always have a meaningful referrer value.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:
lastLinkClicked
variable to be used later.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:
previousUrl
.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
}
}
};
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;
After implementing the custom hook, we observed the following improvements in GTM:
target="_blank"
, the destination URL is correctly captured, even if it's a relative path.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.