Last week we released a brand new website (https://uwork.no) for our amazing client (uWork AS) and had to get Google Tag Manager (GTM) up and running, easy, we thought! Well, not quite!
For those interested, the stack is:
- Next.js
- TailwindCSS
- Laravel (API)
- Kubernetes (K8S) hosted on DigitalOcean
- Bitbucket with Bitbucket Pipelines for deployment
The problem
Since we had GTM already setup, we thought all we needed to do is to inject the GTM script. And so we did!
We checked if the script was loaded β , we also checked if GTM was loaded correctly using Google Tag Manager extension for Chrome β , yep everything looked OKEY!
Then after completed the checklist before release we pushed to PROD and run DEPLOY ππ
Next day we where monitoring Google Analytics and found out that all the data where from the index page (path: /
), so we started to investigate and found out:
- Moving from page to page did not fire our GA tag!
The solution
After using the whole day researching and reading on internet and found different suggestions on how to solve this, below we will show you our implementation using the classic Google Analytics tag.
Inject GMT script in pages/_document.tsx
:
<>
{/* Google Tag Manager */}
<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_ID}');`,
}}
/>
{/* End Google Tag Manager */}
{/* Google Tag Manager (noscript) */}
<noscript
dangerouslySetInnerHTML={{
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=${GTM_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
}}
/>
{/* End Google Tag Manager (noscript) */}
</>
Remember to replace / define GTM_ID
!
Then create an page view util function, utils/gtm.ts
:
export const GTMPageView = (url: string) => {
interface PageEventProps {
event: string;
page: string;
}
const pageEvent: PageEventProps = {
event: 'pageview',
page: url,
};
//@ts-ignore
window && window.dataLayer && window.dataLayer.push(pageEvent);
return pageEvent;
};
After that we need to run "pageview" event on every page, this can be done by setting up an listener (routeChangeComplete) on our Router using useEffect, pages/_app.tsx
:
import { AppProps } from 'next/app';
import Router from 'next/router';
import React, {useEffect } from 'react';
import { GTMPageView } from '../utils/gtm';
function MyApp({ Component, pageProps }: AppProps) {
// ...
// Initiate GTM
useEffect(() => {
const handleRouteChange = (url: string) => GTMPageView(url);
Router.events.on('routeChangeComplete', handleRouteChange);
return () => {
Router.events.off('routeChangeComplete', handleRouteChange);
};
}, []);
// ...
}
export default MyApp;
Almost done, now we need to add a new Custom Event to fire our GA tag.
In GTM console:
- Go to Triggers and click "New"
- Name it as desired, in our case I'm naming it "PageViewCustom"
- Under "Trigger configuration" select "Custom event", and under "Event name" write "pageview" <- this should the same as defined in your code/function
GTMPageView
. - Then go to Tags find you GA tag and configure to use the event we just created above.
- Test the changes and publish it :)
UPDATE: Another alternative
You can as well use "History Change" trigger to achieve the same result.
That's all! Happy tracking π
Top comments (25)
Is there some significance to the name of the
page
parameter you're including in the event object? I don't see that parameter documented anywhere and you don't specifically reference it when creating your trigger.I tested without including the
page
parameter and it works the same. It seems likepage
isn't necessary.Well the property it self is not required but I think its important to include it. The reason for it is to know what pages the user visits. Without it you are just (unless GTM can figure out this automatically) you only get the counts.
More info regarding the page you can find here:
developers.google.com/analytics/de...
Oh, nice find. I couldn't find documentation on that parameter; I assumed it wasn't an actual GTM param. Yeah, if you leave it empty GTM figures it out. I was thinking that might be a better option, since it reduces the chance that you set it incorrectly.
Hi. Great tip!
But I see a problem on my end using this method. The GTM script renders multiple times (for each Page Change). Does not matter if I use the Custom Event trigger or the History Change trigger.
Anybody else seeing this?
Sorry for late reply, no, haven't encounter this issue. Check if the component is rerendering and therefor firing multiple GTM page view events!
Every time I have delved into this issue, it turned out that pageProps fires after the router events, so this implementation (even though its recommended by Next.js examples) only does one job: tracking a page view.
If you want to start pushing data into the dataLayer via pageProps it is limited.
I wrote about this here: morganfeeney.com/how-to/integrate-..., you're welcome.
Awesome article, very helpful to learn how to properly setup GTM with nextjs.
My only recommendation would be to setup the new Google Analytics G4 instead of Google Analytics Universal, support.google.com/analytics/answe...
I have to take a closer look at it! But quite satisfied with the GA Universal. I use custom events to handle all cases! I even implemented virtuell page view (handling submit forms on react/next.js)
I did this awhile back and forgot to make the event "non-interactive" and our bounce rate dropped a TON.
Oops π
our service, too π’
Oopsi indeed!
Hi I have done everything in this post but there is a problem.
When a user logs-in on my website it gets redirected to another page (using router.replace()). The problem is that the pageview event is triggering two times when a user logs-in and thus registering two pageviews instead of one.
I attach a photo of my google tag manager
Hope anybody can help me as I do not want to have "fake" pageviews on my analytics
Havent had this scenario, so not sure what you can do here! If you have found a solution please do share it with us.
All the best.
How should the
_document
component look like as aFunctionComponent
?Also is there any difference in injecting the tags in
_document
orNextHead
?I've tried this and it seems to work in
next/head
. I have a Layout component which has aHead
, and this is used in every page. Placing thescript
in there should also work.But I guess placing it in
_document
is best since this ensures the script is always on all pages.Yeah I did the same but forgot to add the general
Page
component to every page so I was missing the script in some of my pages.Nice article Flamur!
In case you or anyone else here wants an example of how to implement GTM and Consent Mode in NextJS 15 (App Router) feel free to check out the following article π
Happy developing π§βπ»β‘
I have been having this same issue for a while now, thanks a lot for this
You are welcome!
Hi Flamur, thanks for your blog. I'm currently building a next.js application and would like to load GTM only AFTER the entire page is loaded. The goal behind this is to improve the website performance. Have you figured out a way how I can run third-party JS code only after my application has loaded?