Skip to content

Next.js

Next.js ships its own <Script> component that controls when third-party scripts load relative to hydration. Use it for Statable in both the App Router (app/, Next.js 13+) and the Pages Router (pages/). The component goes into a single shared layout, never into individual pages, so it loads once per session and survives client-side route transitions.

Statable hooks the History API, so route changes from next/link and useRouter are tracked automatically. No manual pageview wiring required.

Compatible with Next.js 13, 14, 15, and 16. The next/script API is stable across all of these versions.

Install

App Router (Next.js 13+)

Add <Script> to the root app/layout.tsx. The component is a Server Component by default. Place it as a sibling of <body> so Next.js can manage injection. The default strategy is afterInteractive, which is the recommended choice for analytics: the script loads early, but after first-party hydration starts.

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

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
      <Script
        src={`https://statable.com/js/${process.env.NEXT_PUBLIC_STATABLE_ID}/s.js`}
        strategy="afterInteractive"
      />
    </html>
  )
}

Set NEXT_PUBLIC_STATABLE_ID in .env.local:

NEXT_PUBLIC_STATABLE_ID=your_site_id

The NEXT_PUBLIC_ prefix is required. Without it, Next.js keeps the value server-side and src resolves to https://statable.com/js/undefined/s.js in the browser bundle.

Pages Router

For the Pages Router, put <Script> in pages/_app.tsx. This is the recommended placement for afterInteractive scripts that load on every route. Do not use pages/_document.tsx for this. The Pages Router only allows <Script> in _document.tsx when the strategy is beforeInteractive.

// 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://statable.com/js/${process.env.NEXT_PUBLIC_STATABLE_ID}/s.js`}
        strategy="afterInteractive"
      />
      <Component {...pageProps} />
    </>
  )
}

Strategy choice

next/script exposes four strategies. For Statable, pick afterInteractive:

StrategyWhen it loadsUse for
beforeInteractiveBefore any Next.js code, in the initial HTMLBot detection, consent managers
afterInteractiveDefault. After hydration startsAnalytics, tag managers (Statable)
lazyOnloadBrowser idle time, after every other resourceChat widgets, social embeds
workerWeb Worker via Partytown. Experimental, Pages onlyAdvanced offload, not needed for Statable

afterInteractive is the default, so the strategy prop is technically optional, but stating it explicitly documents intent.

Tracking custom events

Fire events from any Client Component using window.statable.t(). Add a TypeScript declaration so the global is typed:

// types/global.d.ts
declare global {
  interface Window {
    statable: {
      t: (event: string, props?: Record<string, unknown>) => void
    }
  }
}
export {}

Then call it from a Client Component. The 'use client' directive is required because window.statable only exists in the browser:

'use client'
import { useEffect } from 'react'

export function PricingHero() {
  useEffect(() => {
    window.statable?.t?.('Pricing Page Loaded', { variant: 'A' })
  }, [])

  return <button onClick={() => window.statable?.t?.('CTA Clicked')}>Start free</button>
}

The optional chaining (window.statable?.t?.(...)) silently no-ops if the script hasn't loaded yet — safe to call during hydration.

Tracking page views in SPA

Next.js triggers history.pushState on every client-side route change. Statable listens and fires a pageview automatically. No router subscription, no useEffect on usePathname(), no manual call.

Excluding internal traffic

Statable respects a per-browser opt-out flag in localStorage. To exclude your own dev visits, run this once in DevTools on the live site:

localStorage.setItem('analytics_ignore', 'true')

Remove the flag with localStorage.removeItem('analytics_ignore'). Full procedure in Verify installation.

Verify it's working

  • Open the Statable dashboard, go to Realtime, load any page on your site.
  • The session appears within a few seconds.
  • See Verify installation for the full checklist.

Common pitfalls

  • Putting the script in a single page instead of app/layout.tsx or pages/_app.tsx. It will reload on each navigation and double-count sessions. Always go through the shared layout.
  • Using pages/_document.tsx with afterInteractive. The Pages Router rejects this combination. Either move the <Script> to _app.tsx (recommended), or switch the strategy to beforeInteractive, which is reserved for critical scripts.
  • Calling window.statable from a Server Component. It only exists in the browser. Mark any component that calls it with 'use client'.
  • Forgetting NEXT_PUBLIC_ on the env var. Without that prefix, Next.js does not expose the value to the browser bundle and src resolves to https://statable.com/js/undefined/s.js.
  • Wrapping <Script> in a Client Component just to use onLoad. You only need 'use client' if you actually pass an onLoad, onReady, or onError handler. The basic Statable install does not.

See also: Install the tracking script, Custom events, JavaScript API.


Ready to take control of your web analytics? Try Statable free for 30 days — no credit card required, full feature access, GDPR-compliant by default. Start your free trial or view a live demo.