How to Integrate Google Tag Manager with Next.js
Recently I worked on establishing a framework for the implementation of Google Tag Manager on an enterprise level project built with Next.js. The project replaces a legacy J2EE application.
A key difference between the new and current platform, which came as a complete surprise to the team that initiated the transition, was client-side navigation. When tracking page views the existing triggers/tag events didn’t work as expected. Clicking a link to navigate to another page would only fire a click event, not the event used for tracking a page view, window.onLoad.
Next.js is a hybrid of a Single Page Application (SPA) and a website which has a backend running on a server, it has the concept of routes and pages like a typical React site, however routes are handled automatically by adding pages and directories, there’s no need to install React Router.
The <Link />
component enables client-side navigation between pages. The behaviour at this point is that of an SPA, it’s a seamless way to navigate, and does not require a page load, but because window.onLoad does not fire the problem with tracking events occurs.
So how do you let GTM know you navigated to another page?
The key here is making use of next/router & React's useEffect hook, in order to let GTM know about a page view, I’ll cover how we do this later on, you can skip to it here.
Set up Google Tag Manager (GTM)
To get up and running you’ll need to add the tag manager script to the <head>
of each page, Google recommend adding it as high up in the DOM as possible.
- Create a GTM account: https://tagmanager.google.com/
- GTM Quick Start: https://developers.google.com/tag-manager/quickstart

Prior to Next.js v11.0.0 the simplest way to do this was by using the custom _document
.
- Custom
_document
: https://nextjs.org/docs/advanced-features/custom-document
You’ll be given 2 code snippets, plus instructions, but essentially one goes in the <head>
, the other goes after the opening <body>
tag.
<Head>
<script
dangerouslySetInnerHTML={{
__html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXX');`,
}}
/>
</Head>
<body>
<Main />
<NextScript />
<noscript
dangerouslySetInnerHTML={{
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXX" height="0" width="0" style="display: none; visibility: hidden;" />`,
}}
/>
</body>
Since Next.js v11.0.0 there is a new <Script/>
component, which is the recommended way to implement 3rd party scripts that are only needed client side after a page becomes interactive. In the following case you would add the following code to _app.tsx
and also add the code from fig. 3 to _document.tsx
.
return (
<>
<Head>
<title>Your title</title>
<meta name="description" content="Your description" />
<link rel="icon" href="/favicon.ico" />
</Head>
<Script id="google-tag-manager" strategy="afterInteractive">
{`
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${GTM_ID}');
`}
</Script>
<Component {...pageProps} />
</>
);
Once the scripts have been implemented you can start to add customisations.
I previously mentioned that window.onLoad can’t be solely relied on for tracking page views, there are several ways this issue can be dealt with, here are 2 I've used:
Either way will work, which you choose depends upon how granular you want to be.
Create a history event change trigger
After creating an account, navigate to the dashboard for the website—you'll have to specify a domain, I made up a domain for working locally with localhost—click on Triggers, then configure a trigger.


Choose a Trigger type.
Don't forget to hit save.

To see the history changes pushed into the dataLayer
in the preview, submit the latest version, otherwise your changes won't be visible.


This configuration will enable you to go on to create tags if you'd like to share data with Google Analytics, this article provides more detail.
Fire a custom event when a page changes
All the GTM examples kindly provided by Next.js focus solely on one thing, a url changing. The url is available to next/router
, however when you want to push anything from pageProps
into the dataLayer
, that approach won't help.
I tested this out using console.log
to see the order of events being fired, pageProps
fires after router.events
, so I added pageProps
into the dependency array of the effect (Fig. 12).
What I want to do is watch for the pageProps
changing, and then push the information into the dataLayer
for tracking purposes, things like product titles, product SKU information, and more.
As I fetch data on a page-by-page basis, pageProps
provides a global way to handle passing that data.
I created a function called gtmVirtualPageView
(catchy I know) with a single purpose, to push data as part of an event type called VirtualPageView.
export const gtmVirtualPageView = (rest) => {
window.dataLayer?.push({
event: "VirtualPageView",
...rest,
});
};
export default function Page2() {
return <h1>Hello world!</h1>;
}
export async function getStaticProps() {
return {
props: { page: "page 2" }, // is passed up to the custom app as pageProps
};
}
import "../styles/globals.css";
import { useEffect } from "react";
import { useRouter } from "next/router";
import { gtmVirtualPageView } from "../lib/gtm";
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const mainDataLayer = {
pageTypeName: pageProps.page || null,
url: router.pathname,
};
gtmVirtualPageView(mainDataLayer);
}, [pageProps]);
return <Component {...pageProps} />;
}
export default MyApp;
The only caveat with this approach is that it won't work unless pageProps
is used. If you don't want to make use of pageProps
, there are other ways, for instance using Redux or Context, which I'm not overing this time around.


Conclusion
There's not a lot of code to write to create an initial setup; the biggest challenge is understanding how it all fits together in a practical sense, and getting your head around GTM and Analytics, which requires investigation.
If you have any suggestions as to how the examples can be made clearer, easier to understand, or how I can add any missing use-cases you feel would help others, I'd love nothing more than to hear from you! Reach out to me on Twitter, or shoot me an email.
Written by Morgan Feeney
I’ve been designing and developing for almost 2 decades.
Read about me, and my work.
For juicy tips and exclusive content, subscribe to my FREE newsletter.
