How to setup Google Tag Manager in a Next 13 App Router website
Tags: googletagmanager, nextjs
Published: 2023-03-21 21:37:56

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](https://vercel.com/docs/concepts/analytics/audiences/package) react component, I've added one in my `layout.tsx` root component called `Analytics`:

```react
// layout.tsx
<html lang="it">
<body>
	<Suspense>
		<Analytics />
	</Suspense>
	...
</body>
```

I wrapped it in a `Suspense` boundary to avoid the "[deopted into client-side rendering](https://nextjs.org/docs/messages/deopted-into-client-rendering)" error for my static pages.

And this is its content:

```react
// 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}');
  `,
        }}
      />
    </>
  )
}
```

The idea is similar to the [with-google-tag-manager](https://github.com/vercel/next.js/tree/canary/examples/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:

``` ts
// 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,
    })
  }
}
```

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 ;)