Next.js Analytics Without Cookies
Add Fusionaly to a Next.js app with one Script tag in the root layout. It counts client-side page changes and uses no cookies.
Fusionaly is self-hosted analytics with no cookies. In a Next.js app, you add one Script tag to the root layout. Fusionaly then counts every page, also the pages that the Next.js router opens without a reload.
Before you start
You need a Fusionaly server, with your site's domain added as a website. Fusionaly takes events only from the domains you added. See Installation.
In the examples, replace analytics.example.com with the domain of your Fusionaly server.
Add the script
-
Open the root layout,
app/layout.tsx. -
Import
Scriptfromnext/script, and add it at the end of<body>: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://analytics.example.com/y/api/v1/sdk.js" strategy="afterInteractive" /></body></html>);}
With the Pages Router, put the same Script tag in pages/_app.tsx.
Page changes
The Next.js router (App Router and Pages Router) changes pages with history.pushState, and Fusionaly counts a page view each time the path changes. A change to only the query string or the hash is not a new page view.
Skip it in development
This is optional. Fusionaly refuses events from localhost, so your own tests never count.
To not load the script at all in development, check NODE_ENV in the layout:
{process.env.NODE_ENV === "production" && ( <Script src="https://analytics.example.com/y/api/v1/sdk.js" strategy="afterInteractive" />)}Track events
Fusionaly counts page views and button clicks without any setup. To give an event your own name,
use the data-fusionaly-event-name attribute. See Tracking.
React passes data-* attributes to the HTML, so the attribute also works in Server Components:
<button data-fusionaly-event-name="signup_clicked">Sign up</button>For data that only the browser knows, call the SDK from a Client Component. The ?. skips the call when the script is not loaded, for example in development:
"use client";
export function PlanPicker() { return ( <select onChange={(e) => window.Fusionaly?.sendCustomEvent("plan_selected", { plan: e.target.value }) } > <option value="monthly">Monthly</option> <option value="yearly">Yearly</option> </select> );}With TypeScript, declare the SDK in a .d.ts file that your tsconfig.json includes:
declare global { interface Window { Fusionaly?: { sendCustomEvent(name: string, metadata?: Record<string, unknown>): void; sendPageView(): void; }; }}
export {};Check that it works
- Open your site in a normal browser window. Fusionaly skips automated browsers, such as Playwright, and browsers that send Do Not Track.
-
In the developer tools, open the network tab, and find a
POSTrequest to/x/api/v1/events. A 403 status means that your site's domain is not added as a website. - Open your Fusionaly dashboard. The visit shows up within about a minute.
Notes
- Vercel preview deployments have their own domain. Fusionaly does not count them, unless you add that domain as a website.
- If your app sends a
Content-Security-Policyheader, add the domain of your Fusionaly server toscript-srcandconnect-src.
Next steps
- Revenue Tracking: count purchases and their amounts.
- Section Tracking: count the sections that visitors see.
- Exclude your own visits: stop counting your own browser.
- AI Clients (MCP): ask Claude Code, Codex, or Cursor about your traffic.
Guides for other frameworks: React, Astro, Nuxt, SvelteKit, WordPress, Shopify, Webflow, Ghost, Hugo, and the generic guide.