DEV Community

Marco Valsecchi
Marco Valsecchi

Posted on • Updated on

How to setup Google Tag Manager in a Next 13 App Router website

Recently I finished a brand new Next.js 13 website using the latest App Router solution (I know that is not production ready but I love to learn new things by real projects) and before to go live I setup a new Google Tag Manager with all the needed tags... but how to add it to the new app directory?

Inspired by the new @vercel/analytics react component, I've added one in my layout.tsx root component called Analytics:

// layout.tsx
<html lang="it">
<body>
    <Suspense>
        <Analytics />
    </Suspense>
    ...
</body>
Enter fullscreen mode Exit fullscreen mode

I wrapped it in a Suspense boundary to avoid the "deopted into client-side rendering" error for my static pages.

And this is its content:

// Analytics.tsx
"use client"

import { GTM_ID, pageview } from "lib/gtm"
import { usePathname, useSearchParams } from "next/navigation"
import Script from "next/script"
import { useEffect } from "react"

export default function Analytics() {
  const pathname = usePathname()
  const searchParams = useSearchParams()

  useEffect(() => {
    if (pathname) {
      pageview(pathname)
    }
  }, [pathname, searchParams])

  if (process.env.NEXT_PUBLIC_VERCEL_ENV !== "production") {
    return null
  }

  return (
    <>
      <noscript>
        <iframe
          src={`https://www.googletagmanager.com/ns.html?id=${GTM_ID}`}
          height="0"
          width="0"
          style={{ display: "none", visibility: "hidden" }}
        />
      </noscript>
      <Script
        id="gtm-script"
        strategy="afterInteractive"
        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}');
  `,
        }}
      />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

The idea is similar to the with-google-tag-manager example with the pages solution.

Instead of using both the custom _document.tsx and the _app.tsx files I added all the configuration in our client component and thanks to the native Script tag, GTM is visible everywhere but loaded once after the required scripts.
The next views are triggered on page change event and in the new approach this is achieved monitoring the current pathname (and the searchParams too if needed).

This is my TypeScript version of the original lib/gtm library:

// lib/gtm.ts
type WindowWithDataLayer = Window & {
  dataLayer: Record<string, any>[]
}

declare const window: WindowWithDataLayer

export const GTM_ID = process.env.NEXT_PUBLIC_GTM

export const pageview = (url: string) => {
  if (typeof window.dataLayer !== "undefined") {
    window.dataLayer.push({
      event: "pageview",
      page: url,
    })
  } else {
    console.log({
      event: "pageview",
      page: url,
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

I just added a check for the dataLayer variable so you can use it during the development too, logging the event in the console for example.

And that's all... now you're ready to test and publish your GTM configuration and collect all your user views ;)

Top comments (27)

Collapse
 
malikgaurav626 profile image
malikgaurav626

you are such a life saver man, thank you.

I appreciate it

Collapse
 
ux_bob profile image
Bobby Smith

I continue to get an error at window.dataLayer.push (cannot read properties of undefined). For some reason, with this setup, dataLayer is not a property of window by the time the useEffect runs. You run into this problem?

Collapse
 
ux_bob profile image
Bobby Smith

ah! only getting the error in development because of process.env.NEXT_PUBLIC_VERCEL_ENV !== 'production' not set so returns null, but useEffect still runs and tries to fire pageview(pathname)

any way, besides checking for window.dataLayer in lib/gtm, to get around that?

Collapse
 
diazsasak profile image
Diaz Guntur Febrian

you can remove the checking on Analytics component and add the checking on pageView() method instead. so it not break the rule of hook.

export const GTM_ID = process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID;

export const pageview = (url: string) => {
if (process.env.NEXT_PUBLIC_VERCEL_ENV !== "production") {
return;
}

// @ts-ignore
window.dataLayer.push({
event: "pageview",
page: url,
});
};

Thread Thread
 
valse profile image
Marco Valsecchi

Hi, returning null is full legit because you don't break the useEffect hook. I prefer to don't add the GTM script during the development and thanks to your suggestion I added my gtm lib version that check the dataLayer variable that is missing in the original version. Thanks 👍🏻

Thread Thread
 
diazsasak profile image
Diaz Guntur Febrian

That's great 👍

Collapse
 
valse profile image
Marco Valsecchi

Thanks for your interest; I added the gtm lib too on the post because in the original version is missing a check to the dataLayer variable, needed during the development 🙏🏻

Collapse
 
volterra profile image
Steve Doig • Edited

FTR, using this lib/gtm.js meant I received an error in VS Code / analytics.tsx: "lib/gtm"' has no exported member named 'gtmId'. Did you mean 'GTM_ID'? ; replacing gtmId with GTM_ID resolved that error in analytics.tsx and GTM now loads nicely. Cheers.

Collapse
 
valse profile image
Marco Valsecchi

Hi, thanks I updated the post using the original GTM_ID variable.

Collapse
 
jakubfiglak profile image
jakubfiglak • Edited

I'm getting VM22299:2 Uncaught TypeError: Cannot read properties of null (reading 'parentNode') error with this approach, have any of you encountered that?

Collapse
 
valse profile image
Marco Valsecchi

Hi, I added my version of gtm lib file; try to use it instead of the original one because I added a missing check for the dataLayer variable. Let me know if it helps you!

Collapse
 
jakubfiglak profile image
jakubfiglak

Thanks but I'm not even using this file, the error I get is throw during script initialisation. The issue is probably there - f.parentNode.insertBefore(j,f), it seems like the f variable is null (var f=d.getElementsByTagName(s)[0] - looks like it's a first script tag). Interestingly, the GTM works even with that error. Maybe it's being called more than once and the error occurs only for the first time?

Collapse
 
djj0s3 profile image
Jose Gonzalez

This was super helpful! Just want to add some help for the next explorer. The offical GTM JS location has changed. So in Analytics.tsx you'll want to update to: https://www.googletagmanager.com/gtag/js?

Collapse
 
bryanjhickey profile image
Bryan Hickey

Thanks for the article. Very clear and well-written.

You've referenced lib/gtm in Analytics.tsx. How do you deal with pageview in that file? Would be great to get some insight into that part of your solution.

Cheers!

Collapse
 
valse profile image
Marco Valsecchi • Edited

Hi, thank you!
You can find it on the current with-google-tag-manager example 😉

I just added it to the post too; it's in TypeScript and with a missing check for the dataLayar variable.

Collapse
 
phanthanh07 profile image
PhanThanh07

I think there is a problem here when you wrap the Analytics component inside Suspense, the noscript tag will never be executed because when javascript is disabled, the Analytic component also does not execute

Collapse
 
rschneider94 profile image
Rodrigo Schneider

Really awesome Marcelo! Thanks for sharing this!

Collapse
 
lsgelzer profile image
lsgelzer

This does not work and is not picked up with GTM Tag assistant as the code needs to be in the head of the site, has anyone found a solution yet?

Collapse
 
valse profile image
Marco Valsecchi

Hi, I don't know why this isn't working for you but the GTM Tag is placed correctly on the head of the page and also the Tag Assistant recognize it; this is because using the Script tag by Next.js put the script in the right place.

Collapse
 
wessvz profile image
Wessel

Hi, I have the same problem. After debugging I found out that it has to do with "use client". Without "use client" google tag manager has no problem finding the tag, unfortunately this is not the case when "use client" is added to make the useEffect work. Does anyone have a solution for this?

Collapse
 
nandumoura profile image
Fernando Moura

What is this pageview function?

Collapse
 
valse profile image
Marco Valsecchi • Edited

Hi, it's the function exported by the original lib/gtm.js to set a view with GTM.

I added the code for this library too using TypeScript and adding some check more then in the original one.

Collapse
 
kyantikov profile image
Kirill Yantikov

Thanks so much for the article!

Wrapping <Analytics /> with the <Suspense /> boundary got SSR working for my site.

By any chance, do you know what the reason for this is?

Collapse
 
kgoedecke profile image
Kevin Goedecke

We've just published a package that does exactly that with a few lines of code, check it out here: github.com/XD2Sketch/next-google-t...

Collapse
 
markmackaycat profile image
markmackaycat

Hi @kgoedecke - i'm having issues on my next.js project...getting the following error message:

ReferenceError: $ is not defined

Followed by Uncaught TypeError: Cannot read properties of undefined (reading 'childNodes')

Any ideas on how to resolve this when using your library?

Image description

Collapse
 
spispeas profile image
Joakim Bording

Having a problem where the pageview method is fired twice on each reload of the page. Anyone else experiencing the same? Might be something with my setup though 🤔

Collapse
 
spispeas profile image
Joakim Bording

Ah.. Seems like that was caused by React Strict mode on localhost. This suggestion fixed the issue locally:

const nextConfig = {
  reactStrictMode: false
}
Enter fullscreen mode Exit fullscreen mode

Thanks for a great post!