DEV Community

Cover image for Add Google Analytics through GTM (Google Tag Manager) on Next.js
Flamur Mavraj for Team ORNIO

Posted on • Edited on

Add Google Analytics through GTM (Google Tag Manager) on Next.js

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) */}
</>
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Almost done, now we need to add a new Custom Event to fire our GA tag.

In GTM console:

  1. Go to Triggers and click "New"
  2. Name it as desired, in our case I'm naming it "PageViewCustom"
  3. Under "Trigger configuration" select "Custom event", and under "Event name" write "pageview" <- this should the same as defined in your code/function GTMPageView. alt text
  4. Then go to Tags find you GA tag and configure to use the event we just created above. alt text
  5. 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)

Collapse
 
cullylarson profile image
Cully Larson

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.

Collapse
 
cullylarson profile image
Cully Larson

I tested without including the page parameter and it works the same. It seems like page isn't necessary.

Collapse
 
oxodesign profile image
Flamur Mavraj

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...

Thread Thread
 
cullylarson profile image
Cully Larson

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.

Collapse
 
hukken profile image
Øystein Hukset

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?

Collapse
 
oxodesign profile image
Flamur Mavraj

Sorry for late reply, no, haven't encounter this issue. Check if the component is rerendering and therefor firing multiple GTM page view events!

Collapse
 
mog profile image
Morgan Feeney

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.

Collapse
 
mcasey8540 profile image
Mike Casey

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...

Collapse
 
oxodesign profile image
Flamur Mavraj

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)

Collapse
 
dbredvick profile image
drew.tech

I did this awhile back and forgot to make the event "non-interactive" and our bounce rate dropped a TON.

Oops πŸ˜…

Collapse
 
roseline124 profile image
Hyunji Song

our service, too 😒

Collapse
 
oxodesign profile image
Flamur Mavraj

Oopsi indeed!

Collapse
 
alex44lel profile image
Alex44lel

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

Collapse
 
oxodesign profile image
Flamur Mavraj

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.

Collapse
 
the_yamiteru profile image
Yamiteru

How should the _document component look like as a FunctionComponent?

Also is there any difference in injecting the tags in _document or NextHead?

Collapse
 
benbalderas profile image
Ben Balderas

I've tried this and it seems to work in next/head. I have a Layout component which has a Head, and this is used in every page. Placing the script in there should also work.

But I guess placing it in _document is best since this ensures the script is always on all pages.

Collapse
 
the_yamiteru profile image
Yamiteru

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.

Collapse
 
spencermarx profile image
Spencer Marx

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 πŸ§‘β€πŸ’»βš‘

Collapse
 
toyurc profile image
Adebayo-Ige Toyosi

I have been having this same issue for a while now, thanks a lot for this

Collapse
 
oxodesign profile image
Flamur Mavraj

You are welcome!

Collapse
 
dahrougomar profile image
Omar Dahroug

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?