
Google Analytics is one of those things that’s easy to set up poorly and annoying to get right. Especially in a Next.js app using the App Router where you’re juggling server components, layouts, and different environments like dev, staging, and production.
This post is about doing it properly, no extra pings from localhost, no junk data from staging, and no manually pasting scripts in random places.
First, What Are We Adding and Where?
Let’s say you’re using Google Analytics 4 (GA4). The basic flow is:
- Load the GA script tag in
<head>
- Initialize it with your tracking ID (starts with
G-
) - Send page view events as users navigate
With the App Router in Next.js 15, we’ll use a client-side component and load it via layout.tsx
, so we can keep logic clean and isolated.
Step-by-Step Setup (With No Messy Data From Dev)
1. Add your GA Tracking ID to environment variables
Start by adding your GA ID to .env.local
(but only if you’re in production):
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
Only commit this in .env.production
, and keep .env.local
GA-free during local dev.
2. Create a small GA utils file
Let’s centralize the config and helper:
// lib/gtag.ts
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID || '';
export const pageview = (url: string) => {
if (!GA_TRACKING_ID || process.env.NODE_ENV !== 'production') return;
window.gtag('config', GA_TRACKING_ID, {
page_path: url,
});
};
3. Create the GoogleAnalytics component
We’ll load this from layout.tsx
, and it’ll handle the script injection + route tracking:
// components/GoogleAnalytics.tsx
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import Script from 'next/script';
import * as gtag from '../lib/gtag';
export function GoogleAnalytics() {
const pathname = usePathname();
useEffect(() => {
if (process.env.NODE_ENV !== 'production') return;
gtag.pageview(pathname);
}, [pathname]);
if (process.env.NODE_ENV !== 'production') return null;
return (
<>
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
/>
<Script
id="gtag-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${gtag.GA_TRACKING_ID}', {
page_path: window.location.pathname,
});
`,
}}
/>
</>
);
}
4. Import in Your App Router Layout
Now just import the component in app/layout.tsx
:
// app/layout.tsx
import { GoogleAnalytics } from '../components/GoogleAnalytics';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<GoogleAnalytics />
{children}
</body>
</html>
);
}
Why This Works:
- Analytics only fires in production
- No accidental tracking from localhost or staging
- Pageviews are tracked on client route changes via App Router
Common Pitfalls to Avoid
- Don’t hardcode the GA ID in code. Use environment variables.
- Don’t forget staging mirrors prod. Always double-check if you’re using
.env.production
in staging.
Final Thought
Tracking your traffic is important—but tracking it wrong is worse than not tracking at all. I’ve shipped too many projects where someone forgot to block staging or included dev data, and it always comes back to haunt you.
Keep it clean. Keep it conditional. Keep it client-side.