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.
-
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;
<span class="hljs-comment">// Initialize custom referrer if not set</span>
<span class="hljs-keyword">if</span> (!sessionStorage.getItem(<span class="hljs-string">'customReferrer'</span>)) {
sessionStorage.setItem(<span class="hljs-string">'customReferrer'</span>, <span class="hljs-built_in">document</span>.referrer || <span class="hljs-built_in">window</span>.location.href);
}
<span class="hljs-keyword">const</span> handleClick = <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">event</span></span></span></span></span><span class="hljs-function">) =></span></span> {
<span class="hljs-keyword">const</span> target = event.target.closest(<span class="hljs-string">'a'</span>);
<span class="hljs-keyword">if</span> (target) {
lastLinkClicked = {
<span class="hljs-attr">text</span>: target.textContent || target.innerText,
<span class="hljs-attr">classes</span>: <span class="hljs-built_in">Array</span>.from(target.classList).join(<span class="hljs-string">' '</span>),
<span class="hljs-attr">href</span>: target.getAttribute(<span class="hljs-string">'href'</span>) || <span class="hljs-string">''</span>, <span class="hljs-comment">// Ensure href is a string</span>
<span class="hljs-attr">target</span>: target.getAttribute(<span class="hljs-string">'target'</span>),
};
}
};
<span class="hljs-keyword">const</span> handleRouteChangeStart = <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">url</span></span></span></span></span><span class="hljs-function">) =></span></span> {
previousUrl = <span class="hljs-built_in">window</span>.location.href;
};
<span class="hljs-keyword">const</span> handleRouteChangeComplete = <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">url</span></span></span></span></span><span class="hljs-function">) =></span></span> {
<span class="hljs-comment">// Update custom referrer with the previous URL</span>
<span class="hljs-keyword">const</span> currentReferrer = sessionStorage.getItem(<span class="hljs-string">'customReferrer'</span>) || <span class="hljs-string">''</span>;
sessionStorage.setItem(<span class="hljs-string">'customReferrer'</span>, previousUrl);
<span class="hljs-keyword">if</span> (lastLinkClicked && (!lastLinkClicked.target || lastLinkClicked.target === <span class="hljs-string">'_self'</span>)) {
<span class="hljs-comment">// Ensure internal link</span>
<span class="hljs-built_in">window</span>.dataLayer.push({
<span class="hljs-attr">event</span>: <span class="hljs-string">'userclick'</span>,
<span class="hljs-attr">eventType</span>: <span class="hljs-string">'internalNavigation'</span>,
<span class="hljs-attr">sourcePage</span>: previousUrl,
<span class="hljs-attr">destinationPage</span>: <span class="hljs-built_in">window</span>.location.href,
<span class="hljs-attr">referrer</span>: currentReferrer, <span class="hljs-comment">// Use custom referrer</span>
<span class="hljs-attr">clickTitle</span>: lastLinkClicked.text,
<span class="hljs-attr">clickClass</span>: lastLinkClicked.classes,
<span class="hljs-attr">clickTarget</span>: lastLinkClicked.target || <span class="hljs-string">''</span>,
<span class="hljs-attr">linkHref</span>: lastLinkClicked.href,
<span class="hljs-attr">gtm</span>: {
<span class="hljs-attr">uniqueEventId</span>: <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">10000</span>),
},
});
lastLinkClicked = <span class="hljs-literal">null</span>; <span class="hljs-comment">// Reset after handling</span>
}
};
<span class="hljs-keyword">const</span> handleDocumentClick = <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">event</span></span></span></span></span><span class="hljs-function">) =></span></span> {
handleClick(event);
<span class="hljs-keyword">if</span> (lastLinkClicked) {
<span class="hljs-comment">// Check if the href is a relative path and prepend the current domain if necessary</span>
<span class="hljs-keyword">let</span> destinationPage = lastLinkClicked.href;
<span class="hljs-keyword">if</span> (!destinationPage.startsWith(<span class="hljs-string">'http'</span>)) {
destinationPage = <span class="hljs-built_in">window</span>.location.origin + destinationPage;
}
<span class="hljs-keyword">if</span> (
lastLinkClicked.target === <span class="hljs-string">'_blank'</span> ||
lastLinkClicked.href.includes(<span class="hljs-string">'-/media/'</span>) ||
(lastLinkClicked.href.includes(<span class="hljs-string">'http'</span>) &&
!lastLinkClicked.href.includes(<span class="hljs-built_in">window</span>.location.hostname))
) {
<span class="hljs-built_in">window</span>.dataLayer.push({
<span class="hljs-attr">event</span>: <span class="hljs-string">'userclick'</span>,
<span class="hljs-attr">eventType</span>: <span class="hljs-string">'externalOrNewTab'</span>,
<span class="hljs-attr">sourcePage</span>: <span class="hljs-built_in">window</span>.location.href,
<span class="hljs-attr">destinationPage</span>: destinationPage,
<span class="hljs-attr">referrer</span>: sessionStorage.getItem(<span class="hljs-string">'customReferrer'</span>), <span class="hljs-comment">// Use custom referrer</span>
<span class="hljs-attr">clickTitle</span>: lastLinkClicked.text,
<span class="hljs-attr">clickClass</span>: lastLinkClicked.classes,
<span class="hljs-attr">clickTarget</span>: lastLinkClicked.target || <span class="hljs-string">''</span>,
<span class="hljs-attr">linkHref</span>: lastLinkClicked.href,
<span class="hljs-attr">gtm</span>: {
<span class="hljs-attr">uniqueEventId</span>: <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">10000</span>),
},
});
lastLinkClicked = <span class="hljs-literal">null</span>; <span class="hljs-comment">// Reset after handling</span>
}
}
};
<span class="hljs-built_in">document</span>.addEventListener(<span class="hljs-string">'click'</span>, handleDocumentClick);
router.events.on(<span class="hljs-string">'routeChangeStart'</span>, handleRouteChangeStart);
router.events.on(<span class="hljs-string">'routeChangeComplete'</span>, handleRouteChangeComplete);
<span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params"><span class="hljs-function"><span class="hljs-params">()</span></span></span></span></span><span class="hljs-function"> =></span></span> {
<span class="hljs-built_in">document</span>.removeEventListener(<span class="hljs-string">'click'</span>, handleDocumentClick);
router.events.off(<span class="hljs-string">'routeChangeStart'</span>, handleRouteChangeStart);
router.events.off(<span class="hljs-string">'routeChangeComplete'</span>, 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, orwindow.location.href
ifdocument.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.
External Link Handling
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;
}
<span class="hljs-keyword">if</span> (
lastLinkClicked.target === <span class="hljs-string">'_blank'</span> ||
lastLinkClicked.href.includes(<span class="hljs-string">'/media/'</span>) ||
(lastLinkClicked.href.includes(<span class="hljs-string">'http'</span>) &&
!lastLinkClicked.href.includes(<span class="hljs-built_in">window</span>.location.hostname))
) {
<span class="hljs-built_in">window</span>.dataLayer.push({
<span class="hljs-attr">event</span>: <span class="hljs-string">'userclick'</span>,
<span class="hljs-attr">eventType</span>: <span class="hljs-string">'externalOrNewTab'</span>,
<span class="hljs-attr">sourcePage</span>: <span class="hljs-built_in">window</span>.location.href,
<span class="hljs-attr">destinationPage</span>: destinationPage,
<span class="hljs-attr">clickTitle</span>: lastLinkClicked.text,
<span class="hljs-attr">clickClass</span>: lastLinkClicked.classes,
<span class="hljs-attr">clickTarget</span>: lastLinkClicked.target || <span class="hljs-string">''</span>,
<span class="hljs-attr">referrer</span>: sessionStorage.getItem(<span class="hljs-string">'customReferrer'</span>),
<span class="hljs-attr">gtm</span>: {
<span class="hljs-attr">uniqueEventId</span>: <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">10000</span>),
},
});
lastLinkClicked = <span class="hljs-literal">null</span>; <span class="hljs-comment">// Reset after handling</span>
}
}
};
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:
- Accurate Click Tracking: Click events are now tracked correctly, capturing the link text, classes, href, and target attributes accurately.
- Correct Page Referrer: The referrer URL now updates correctly on each route change, reflecting the actual navigation flow within the application.
- Improved Data Accuracy: For external links and links with
target="_blank"
, the destination URL is correctly captured, even if it's a relative path.
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.