July 12, 2026Analytics

Next JS Google Tag Manager: Load It Once, Fire It Right

Why Next JS Google Tag Manager Installs Break on Navigation

Last month I audited a B2B SaaS company running EUR 6,200 a month on Google Ads. Their Next.js marketing site looked sharp. GA4 showed sessions. The Google Ads conversion tag was published inside GTM. But when I opened Preview mode and clicked from the homepage to the pricing page, nothing happened. No Container Loaded event. No virtual pageview. The container had fired on the initial hard load and then gone silent for every subsequent client-side route change.

Their campaign manager had been scaling spend against a conversion count that only captured visitors who landed directly on the thank-you page from an external referral or a hard refresh. Every visitor who navigated through the site and reached the thank-you page via Next.js routing was invisible to Google Ads. Six months of Smart Bidding trained on roughly 40 percent of actual conversions.

The root cause was a misunderstanding of how Next.js handles navigation. Unlike traditional sites, Next.js uses client-side routing by default. After the first load, clicking internal links does not trigger a full page reload. The browser URL updates, the DOM mutates, but the <script> tags in <head> do not re-execute. If your Google Tag Manager setup relies on a fresh script evaluation for every page, it sees one pageview and nothing after.

This is the single most common failure pattern when you add Google Tag Manager to Next.js. The container loads. Tags fire once. Then the app takes over routing and your tracking goes dark. If you have searched "Next JS Google Tag Manager not working" and landed here, this is almost certainly your problem.

How Next.js Routing Differs from Traditional Sites

On a traditional multi-page website, every link click triggers a full HTTP request. The browser fetches new HTML and re-executes every <script> tag. GTM reloads. All Page View triggers evaluate. Tags fire.

Next.js uses the App Router (or the older Pages Router) with client-side navigation. After the initial server-rendered load, route transitions happen via JavaScript. The shell stays mounted. Only the changed route segment re-renders. This is what makes Next.js fast, but it is also what makes Google Tag Manager in Next.js tricky.

The consequence: GTM's built-in Page View trigger fires exactly once, on the first load. Subsequent navigations produce no gtm.js event, no Page View event in Preview mode, and no tag firing unless you explicitly tell the data layer about each route change.

How to Add Google Tag Manager in Next.js the Right Way

There are two clean approaches depending on your Next.js version. Both follow the same principle: load the Next.js Google Tag Manager snippet once and push a data layer event on every route change.

App Router (Next.js 13.4+)

Since Next.js 14, the framework ships a dedicated <GoogleTagManager> component in @next/third-parties. This is the simplest path.

Install the package:

npm install @next/third-parties

Then add it to your root layout:

// app/layout.tsx
import { GoogleTagManager } from '@next/third-parties/google'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <GoogleTagManager gtmId="GTM-XXXXXXX" />
        {children}
      </body>
    </html>
  )
}

This component renders the GTM script once at the layout level. Because the root layout persists across route changes in the App Router, the container stays loaded without re-injecting on navigation.

However, it does not automatically push a virtual pageview on client-side route transitions. You need to handle that yourself.

Pages Router (Legacy)

If your project uses the Pages Router, add the GTM script to _app.js or _document.js. Place the inline script in a <Script> component with the afterInteractive strategy:

// pages/_app.js
import Script from 'next/script'

export default function MyApp({ Component, pageProps }) {
  return (
    <>
      <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-XXXXXXX');`,
        }}
      />
      <Component {...pageProps} />
    </>
  )
}

The afterInteractive strategy loads the script after the page becomes interactive, matching GTM's expected async behavior. Placing it in _app.js rather than individual pages ensures the container loads exactly once across the entire application.

Tracking Route Changes with dataLayer.push

This is where most Next.js GTM installs stop working. The container is loaded, but nobody told GTM that the user navigated.

The fix is a component that listens to Next.js route changes and pushes a custom event into the data layer on each transition.

For the App Router, use usePathname from next/navigation:

// app/components/GTMRouteTracker.tsx
'use client'

import { usePathname } from 'next/navigation'
import { useEffect, useRef } from 'react'

export function GTMRouteTracker() {
  const pathname = usePathname()
  const isFirstRender = useRef(true)

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false
      return
    }

    window.dataLayer = window.dataLayer || []
    window.dataLayer.push({
      event: 'next_route_change',
      page_path: pathname,
    })
  }, [pathname])

  return null
}

Add this component inside your root layout, alongside the GTM snippet. The isFirstRender guard prevents a duplicate push on the initial page load, since GTM already fires its built-in Page View event on the first render.

Inside GTM, create a Custom Event trigger for next_route_change. Attach your GA4 event tag (or a Google Tag that sends a page_view event) to this trigger. Now every client-side navigation pushes a structured event into the data layer, and GTM fires exactly as it would on a full page load.

If the concept of pushing structured events into a data layer is unfamiliar, my post on what a data layer is and why tracking breaks without one covers the mechanics in detail.

Common Reasons Google Tag Manager Is Not Working in Next.js

I see the same failures across audits. Here is a diagnostic table.

SymptomLikely causeFix
Container loads on first page onlyNo route-change listener pushing to dataLayerAdd the GTMRouteTracker component described above
Container never loadsScript placed inside a component that unmounts, or wrong Script strategyMove to root layout (App Router) or _app.js (Pages Router)
Tags fire twice on initial loadGTM snippet injected in both _document.js and _app.jsRemove the duplicate; keep it in one place only
Page path shows stale URL in GA4Route-change push happens before the URL updatesUse usePathname() inside useEffect, which fires after the navigation completes
Preview mode connects but shows no events after navigationNo custom event trigger configured in GTMCreate a Custom Event trigger matching your push event name

When your Next.js Google Tag Manager script loads but tags stop firing after the first page, it is almost always a missing data layer push. The container is alive. It is listening. Nobody is talking to it.

Handling the Noscript Fallback

Google's installation instructions specify a <noscript> iframe immediately after the opening <body> tag. In the App Router, you can add this directly in your root layout:

// Inside the <body> of app/layout.tsx
<noscript>
  <iframe
    src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
    height="0"
    width="0"
    style={{ display: 'none', visibility: 'hidden' }}
  />
</noscript>

If you are using the @next/third-parties <GoogleTagManager> component, it does not render the noscript block. Add it manually. The noscript iframe only matters for the small percentage of users with JavaScript disabled (under 1 percent of web traffic according to usage studies), but including it keeps your install compliant with Google's spec.

Consent Mode and Next.js

If your site serves visitors in the EU or UK, your GTM container needs Google Consent Mode v2 defaults set before any tags fire. In a Next.js context, this means the consent defaults must be pushed to the data layer before the GTM container loads.

In the App Router, add a script block above the GTM component in your root layout:

<script
  dangerouslySetInnerHTML={{
    __html: `
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('consent', 'default', {
        'ad_storage': 'denied',
        'analytics_storage': 'denied',
        'ad_user_data': 'denied',
        'ad_personalization': 'denied'
      });
    `,
  }}
/>
<GoogleTagManager gtmId="GTM-XXXXXXX" />

This ensures the consent state is available when GTM initializes. Your CMP then updates these values when the user makes a choice. Skipping this does not just create legal risk. It means Google Ads loses modeling data, and your conversion reporting degrades. I covered the full implementation in Google Consent Mode v2: Implementation Guide.

Beyond Pageviews: Tracking Conversions in Next.js

Getting GTM loaded and pageviews firing is the foundation. The next layer is conversion tracking: form submissions, demo bookings, sign-ups, purchases.

In a Next.js app, these events happen without page reloads. A form submits via fetch, the UI updates in place, and the URL may not change at all. GTM's built-in Form Submission trigger is unreliable here because it was designed for traditional HTML form POSTs.

The reliable pattern is the same one that works everywhere: push a custom event into the data layer when the conversion happens.

// Inside your form's submit handler
const handleSubmit = async (formData) => {
  const response = await submitForm(formData)

  if (response.ok) {
    window.dataLayer = window.dataLayer || []
    window.dataLayer.push({
      event: 'generate_lead',
      form_name: 'demo_request',
      lead_value: 500,
    })
  }
}

Then in GTM, create a Custom Event trigger for generate_lead and attach your conversion tags. This approach works whether you are sending conversions to GA4, Google Ads, or Meta. It survives routing changes, React re-renders, and framework upgrades.

If your trigger setup is more complex, my post on 5 silent trigger misconfigurations covers the mistakes I find in almost every audit.

Server-Side Tracking with Next.js

A browser-side GTM install in Next.js faces the same headwinds as any client-side setup: Safari ITP caps JavaScript cookies at seven days, ad blockers strip tracking requests, and privacy regulations restrict what fires before consent.

For Next.js sites driving meaningful ad spend, a server-side GTM container closes these gaps. The web container sends events to your server container, which forwards them server-to-server. First-party cookies set by the server persist beyond ITP caps.

Next.js makes this slightly easier than traditional sites because you already have a Node.js server (or serverless functions) handling requests. Your server-side Google Tag Manager Next.js container can run on the same infrastructure, or on a dedicated subdomain via Stape or Google Cloud Run.

Quick Verification Checklist

After deploying your Google Tag Manager Next.js setup, run through this before you call it done:

  1. Hard refresh the site. Open GTM Preview mode. Confirm the Container Loaded event fires.
  2. Click an internal link. In Preview mode, confirm your next_route_change (or equivalent) custom event appears.
  3. Check for duplicates. In the browser Network tab, filter for gtm.js. You should see exactly one request. Two means the container is loading twice.
  4. Test a conversion. Submit a form or trigger a key event. Confirm the data layer push appears in Preview mode and the conversion tag fires with the correct parameters.
  5. Test a direct landing. Open a deep page URL directly (not via client-side navigation). Confirm the container loads and the initial Page View fires.

If any of these fail, your Next.js Google Tag Manager install is broken and everything downstream — GA4 reports, ad platform conversions, automated bidding — is working with incomplete data. If that sounds familiar, I can audit your setup and tell you exactly what needs fixing.

FAQ

Why does Google Tag Manager only fire on the first page in Next.js?

Next.js uses client-side routing for navigation after the initial page load. The browser does not re-execute script tags on route changes, so GTM only receives the first page view event. You need to push a custom data layer event on every route change so GTM knows a navigation happened.

Should I use the next/third-parties GoogleTagManager component?

Yes, if you are on Next.js 14 or later with the App Router. It handles script loading correctly and avoids duplicate injection. You still need a separate route-change listener component because the built-in component does not push virtual pageview events on client-side navigations.

Can I install Google Tag Manager in both _document.js and _app.js?

You should not. Placing the GTM snippet in both files loads the container twice on every page, doubling all your events and inflating every metric in GA4 and Google Ads. Choose one location, preferably _app.js for the Pages Router, and remove it from the other.

How do I track form submissions in Next.js with GTM?

Push a custom data layer event from your form submit handler after a successful submission. Create a Custom Event trigger in GTM that matches the event name and attach your conversion tags to it. Do not rely on GTM built-in form triggers because Next.js forms typically submit via fetch without a page reload.

Do I need server-side tracking for a Next.js site?

It depends on your ad spend and audience. If you run significant paid campaigns and a large share of your traffic uses Safari or ad blockers, server-side tracking recovers data that client-side GTM loses. For most marketing sites with moderate traffic, a well-configured client-side setup is a solid starting point.

Not sure your Next.js tracking is actually capturing conversions? Get in touch -- I will audit your setup and tell you exactly what is broken and how to fix it.

Ready to fix your marketing measurement?

Take assessment →