# Google Analytics and Next.js

> Source: https://agilitycms.com/docs/nextjs/google-analytics-and-next-js

Adding Google Analytics to an App Router site is now mostly a matter of dropping one component into your root layout. The tricky part — tracking page views when the router navigates *without* a full page reload — is no longer something you write yourself, but it is something you have to **verify**.

> **Coming from the Pages Router?** The old recipe here used the `react-ga` package plus `router.events.on("routeChangeComplete", …)` from `next/router`. Neither works now: `next/router` does not exist in the App Router (there are no router events to subscribe to), and `react-ga` targets Universal Analytics, which Google shut down in July 2023 — a `UA-` property no longer collects anything. Replace both with `@next/third-parties` and a GA4 `G-` measurement ID.

## 1. Install `@next/third-parties`

Keep its major version in step with your Next.js version:

```bash
npm install @next/third-parties
```

## 2. Add the component to your root layout

```tsx
// app/[locale]/layout.tsx
import { GoogleAnalytics } from "@next/third-parties/google"

export default async function LocaleLayout({ children }) {
  return (
    <>
      {children}
      <GoogleAnalytics gaId="G-XXXXXXXXXX" />
    </>
  )
}
```

That is the whole integration. The component loads `gtag.js` through `next/script` with a sensible loading strategy, so it doesn't block your first paint.

## 3. Put the measurement ID in Agility, not in your code

A hard-coded ID means a deploy every time marketing changes properties. Add a `googleAnalyticsID` field to a global **Settings** content item and read it in the layout:

```tsx
import { GoogleAnalytics } from "@next/third-parties/google"
import { getSettings } from "@/lib/cms-content/getSettings"
import { getAgilityContext } from "@/lib/cms/getAgilityContext"

export default async function LocaleLayout({ children, params }) {
  const { locale } = await params
  const { isPreview } = await getAgilityContext(locale)
  const settings = await getSettings({ locale, preview: isPreview })
  const gaId = settings?.googleAnalyticsID || null

  return (
    <>
      {children}
      {gaId && <GoogleAnalytics gaId={gaId} />}
    </>
  )
}
```

Because that read is cached and tagged like any other Agility content, publishing a new ID invalidates the layout and the change goes live without a build. Rendering nothing when the field is empty also keeps analytics off your preview and local environments for free.

## 4. Client-side navigations: what actually tracks them

This is the part the old guide existed to solve, and the answer has moved.

`<GoogleAnalytics>` runs `gtag('config', …)` **once**, when it mounts. It does *not* subscribe to route changes — so if you read the source expecting to find navigation tracking, you won't.

It still works, because of something on the Google side: GA4's **Enhanced measurement** includes *"Page changes based on browser history events"*, and App Router navigation is `history.pushState`. GA4 sees the URL change and records a `page_view` by itself.

> ⚠️ **Verify this rather than assuming it.** In GA4, open **Admin → Data streams → your web stream → Enhanced measurement**, expand the settings, and confirm **"Page changes based on browser history events"** is on. It is on by default, but it is also the single switch that decides whether every navigation after the first one is counted. When someone reports "only the landing page is tracked," this is almost always why.

## 5. Sending a page view yourself

You only need this if you've turned enhanced measurement off, or you want extra parameters on the event. It's a small client component:

```tsx
"use client"

import { usePathname, useSearchParams } from "next/navigation"
import { useEffect } from "react"
import { sendGAEvent } from "@next/third-parties/google"

export function PageViewTracker() {
  const pathname = usePathname()
  const searchParams = useSearchParams()

  useEffect(() => {
    const query = searchParams.toString()
    sendGAEvent("event", "page_view", {
      page_path: query ? `${pathname}?${query}` : pathname,
    })
  }, [pathname, searchParams])

  return null
}
```

> ⚠️ **`useSearchParams()` must sit inside a `<Suspense>` boundary.** It is request-time data, so without one it opts the whole route out of static rendering — and under [Cache Components](/docs/nextjs/caching-with-next-js-and-agility) it fails the prerender outright.

```tsx
<Suspense fallback={null}>
  <PageViewTracker />
</Suspense>
```

Also make sure you aren't now counting each navigation twice: if you send these manually, turn the enhanced-measurement history setting **off**.

## 6. Custom events

`sendGAEvent` works from any client component:

```tsx
"use client"
import { sendGAEvent } from "@next/third-parties/google"

export function SignupButton() {
  return (
    <button onClick={() => sendGAEvent("event", "cta_clicked", { cta_name: "hero-signup" })}>
      Get started
    </button>
  )
}
```

It pushes onto the same `dataLayer` the component created. Two consequences worth knowing:

- Calling it **before** `<GoogleAnalytics>` has mounted logs `GA has not been initialized` and drops the event. Fire events from user interactions, not from module scope.
- It is a **client-only** function. To record something from a Server Component or a Route Handler, use the GA4 Measurement Protocol instead.

## 7. Google Tag Manager

Same package, same shape — use this instead of `GoogleAnalytics` if GTM owns your tags:

```tsx
import { GoogleTagManager } from "@next/third-parties/google"

<GoogleTagManager gtmId="GTM-XXXXXXX" />
```

```tsx
"use client"
import { sendGTMEvent } from "@next/third-parties/google"

sendGTMEvent({ event: "cta_clicked", cta_name: "hero-signup" })
```

Don't load both for the same property. If GTM is already firing a GA4 configuration tag, adding `<GoogleAnalytics>` as well gives you duplicated page views.

## 8. Consent

If you need Google Consent Mode, the default consent state has to be set **before** `gtag.js` loads:

```tsx
import Script from "next/script"

<Script id="consent-default" strategy="beforeInteractive">
  {`window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('consent', 'default', { ad_storage: 'denied', analytics_storage: 'denied' });`}
</Script>
<GoogleAnalytics gaId={gaId} />
```

Your consent banner then calls `gtag('consent', 'update', …)` once the visitor chooses.

## Verify it

1. Load the site and confirm a request to `googletagmanager.com/gtag/js?id=G-…` in the Network tab.
2. Open **GA4 → Reports → Realtime** and check the page view lands.
3. **Navigate with a `<Link>`** — not a refresh — and confirm a *second* page view appears. This is the step that catches a misconfigured enhanced-measurement setting, and it's the one people skip.

## Summary

1. `@next/third-parties` → `<GoogleAnalytics gaId="G-…" />` in the root layout. That's the integration.
2. Read the ID from Agility so editors can change properties without a deploy.
3. Route changes are tracked by GA4's enhanced measurement, not by your code — **confirm the setting**, then test with a `<Link>` navigation.
4. Send page views manually only if you must, in a client component inside `<Suspense>`, with enhanced measurement turned off.
5. `sendGAEvent` / `sendGTMEvent` for custom events, client-side only; use the Measurement Protocol on the server.
