← Back to blog

Privacy Analytics for Next.js Apps: A Complete Setup Guide

How to add GDPR-compliant, cookie-free analytics to your Next.js app — covering both the Pages Router and App Router, CSP headers, and dynamic route tracking.

Why Next.js Developers Need to Think Carefully About Analytics

Next.js powers millions of production applications, from indie SaaS products to Fortune 500 websites. Every one of them eventually adds analytics — and most reach for Google Analytics by default. It's free, it's familiar, and there are thousands of tutorials for it.

But Google Analytics has a surprisingly poor fit with the Next.js architecture, and it comes with legal and performance costs that aren't always obvious upfront. This guide covers why privacy-first analytics like Beam are a better default for Next.js apps, and exactly how to integrate them with both the Pages Router and the App Router.

The Problems with Google Analytics in Next.js

1. Hydration and the Single-Page App Problem

Next.js apps are SPAs after the initial load. The browser doesn't trigger a full page reload on client-side navigation — it just swaps content. Traditional GA snippets track pageviews by listening for the browser's load event, which only fires on the first visit. Every subsequent navigation in your Next.js app is invisible to a naive GA setup.

The fix involves hooking into Next.js's router events (routeChangeComplete in the Pages Router, or intercepting the native History API in the App Router). This works, but it's boilerplate you need to maintain, and it breaks whenever Next.js changes its routing internals. Beam's script handles single-page app navigation automatically — no custom router integration required.

2. Content Security Policy Conflicts

If your Next.js app sets strict Content Security Policy headers — which you should, especially if you're handling user data — Google Analytics is a problem. GA loads scripts from www.google-analytics.com, www.googletagmanager.com, and several other domains, all of which need to be whitelisted in your script-src and connect-src directives. This meaningfully weakens your CSP.

Beam sends data to a single first-party endpoint (beam.keylightdigital.dev), making your CSP configuration simpler and more restrictive.

3. GDPR Consent Requirements

Google Analytics sets third-party cookies and transfers personal data (IP addresses) to Google's servers. Under GDPR, this requires explicit user consent before the script loads. You need a consent banner. Users reject it. Your analytics miss 30–60% of visitors. The data you do collect is skewed toward users who actively click "accept" — not a random sample of your audience.

Cookie-free analytics like Beam require no consent mechanism. There are no cookies, no cross-site tracking, and no personal data transferred to third parties. You get accurate, complete data from every visitor — without legal risk.

Setting Up Beam in the Pages Router

The Pages Router uses a custom _app.tsx (or _app.js) file as the root component that wraps every page. This is the right place to add the Beam script so it loads on every page load and client-side navigation.

Using the Next.js Script Component

Next.js ships a built-in Script component that handles loading strategy, deduplication, and performance optimisation. Use it with strategy="afterInteractive" for analytics:

// pages/_app.tsx
import type { AppProps } from 'next/app'
import Script from 'next/script'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Script
        src="https://beam.keylightdigital.dev/js/beam.js"
        data-site-id="YOUR_SITE_ID"
        strategy="afterInteractive"
      />
      <Component {...pageProps} />
    </>
  )
}

Replace YOUR_SITE_ID with the site ID from your Beam dashboard. The afterInteractive strategy loads the script after the page becomes interactive — ideal for analytics that shouldn't block rendering.

Tracking Dynamic Route Changes

Beam's script automatically listens for pushState and replaceState calls to detect client-side navigation. This means dynamic route changes in /pages/[slug].tsx, /pages/products/[id].tsx, and similar patterns are tracked automatically without any extra configuration.

If you want to verify tracking is working, open your browser's DevTools Network tab and navigate between pages. You should see POST /api/collect requests to Beam on each navigation.

Setting Up Beam in the App Router

The App Router introduced in Next.js 13 uses a different file structure. Analytics belong in the root layout file, which wraps the entire application.

Adding to app/layout.tsx

// app/layout.tsx
import Script from 'next/script'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://beam.keylightdigital.dev/js/beam.js"
          data-site-id="YOUR_SITE_ID"
          strategy="afterInteractive"
        />
      </body>
    </html>
  )
}

Placing the Script component inside the root layout ensures it loads on every route, including nested layouts and pages. The App Router's React Server Components architecture means the Script component from next/script handles deduplication automatically — you won't get duplicate script loads even if multiple layouts are nested.

Environment Variable for the Site ID

Rather than hardcoding the site ID, use a Next.js public environment variable:

// .env.local
NEXT_PUBLIC_BEAM_SITE_ID=your-site-id-here
// app/layout.tsx
<Script
  src="https://beam.keylightdigital.dev/js/beam.js"
  data-site-id={process.env.NEXT_PUBLIC_BEAM_SITE_ID}
  strategy="afterInteractive"
/>

Variables prefixed with NEXT_PUBLIC_ are inlined into the client bundle at build time, so they're available in both Server Components and Client Components.

Configuring CSP Headers in next.config.js

If you use Content Security Policy headers, you'll need to allow the Beam script and its collection endpoint. Add these to your next.config.js:

// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      // Allow Beam tracking script and collection endpoint
      "script-src 'self' 'unsafe-inline' https://beam.keylightdigital.dev",
      "connect-src 'self' https://beam.keylightdigital.dev",
      // Add other sources your app needs...
    ].join('; '),
  },
]

module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ]
  },
}

This is significantly cleaner than the equivalent configuration for Google Analytics, which requires allowlisting Google's analytics, tag manager, and ad domains.

Verifying Your Setup

Once you've added the script, verify that data is flowing:

  1. Open your Next.js app in a browser.
  2. Navigate to a few different pages (including dynamic routes like /blog/my-post).
  3. Open your Beam dashboard — you should see pageviews appearing in real time, with the correct paths shown for each route.
  4. Check the Top Pages breakdown to confirm dynamic routes (e.g., /blog/my-post, /products/42) are tracked individually.

Comparing Your Options

If you're evaluating Beam against other analytics tools for your Next.js project:

  • Beam vs Google Analytics — the full comparison on privacy, GDPR compliance, and Next.js integration complexity.
  • Beam vs Plausible — both are cookie-free; Beam costs $5/mo vs Plausible's $9/mo starting price.
  • For a general overview of all options, see our 5-minute setup guide covering HTML, React, WordPress, and static site generators.

Add privacy-first analytics to your Next.js app

Free for up to 50,000 pageviews/month. Works with Pages Router and App Router. No cookies, no consent banner required.