Frameworks

Next.js
Next.js analytics with Stickybeak starts by adding its script to your rendered pages; this guide shows you where it belongs and how to track page views, goals and purchase revenue. It is a lighter alternative to Google Analytics for Next.js, with live visitors, goals and revenue attribution in one script.
Add the script
App Router
Put your site ID in an environment variable
Set
NEXT_PUBLIC_STICKYBEAK_SITE_IDin.env.productionor production host settings.NEXT_PUBLIC_STICKYBEAK_SITE_ID=YOUR_SITE_IDAdd the script to your root layout
Add it inside
<body>inapp/layout.tsx:import Script from "next/script"; const siteId = process.env.NEXT_PUBLIC_STICKYBEAK_SITE_ID; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} {siteId && ( <Script defer data-site={siteId} data-domain="example.com" src="https://app.usestickybeak.com/_cfw/s.js" strategy="afterInteractive" /> )} </body> </html> ); }Replace
example.comwith your configured domain.Deploy
Public variables are build-time values, so rebuild after changes.
Use afterInteractive or lazyOnload, not Partytown's worker.
Pages Router
Render the same script in pages/_app.tsx. Apps using both routers need both.
import type { AppProps } from "next/app";
import Script from "next/script";
const siteId = process.env.NEXT_PUBLIC_STICKYBEAK_SITE_ID;
export default function App({ Component, pageProps }: AppProps) {
return (
<>
{siteId && (
<Script
defer
data-site={siteId}
data-domain="example.com"
src="https://app.usestickybeak.com/_cfw/s.js"
strategy="afterInteractive"
/>
)}
<Component {...pageProps} />
</>
);
}
Check it's working
Test the deployed site, then verify your install.
Track goals
Add data-goal to rendered links or buttons. For other events, use the typed queued helper below. See Custom goals.
import Link from "next/link";
export default function Pricing() {
return (
<Link href="/signup?plan=pro" data-goal="start_signup" data-goal-plan="pro">
Start free trial
</Link>
);
}
type GoalProps = Record<string, string | number | boolean>;
type Stickybeak = ((goal: string, props?: GoalProps) => void) & { q?: unknown[][] };
declare global {
interface Window {
stickybeak?: Stickybeak;
}
}
export function track(goal: string, props?: GoalProps): void {
if (typeof window === "undefined") return; // on the server: nothing to do
if (!window.stickybeak) {
// Hold calls made before the script loads; it sends them when it arrives.
const queue: Stickybeak = (...args) => {
(queue.q ??= []).push(args);
};
window.stickybeak = queue;
}
window.stickybeak(goal, props);
}
"use client";
import { track } from "@/lib/stickybeak";
export function NewsletterForm() {
async function subscribe(formData: FormData) {
const res = await fetch("/api/subscribe", { method: "POST", body: formData });
if (res.ok) track("newsletter_signup", { source: "footer" });
}
return (
<form action={subscribe}>
<input type="email" name="email" required />
<button>Subscribe</button>
</form>
);
}
Track purchases
On your order confirmation page, render a small client component with the confirmed order's real values. It uses the track helper from lib/stickybeak.ts (see Track goals in the Next.js guide). Each order_id counts once, so reloads don't double it.
"use client";
import { useEffect } from "react";
import { track } from "@/lib/stickybeak";
export function PurchaseGoal({ orderId, total }: { orderId: string; total: number }) {
useEffect(() => {
track("purchase", { revenue: total, currency: "USD", order_id: orderId });
}, [orderId, total]);
return null;
}
More on currencies and attribution: Revenue.
Page changes
Both routers count new paths from links, pushes, back and forward. Replace, query-only and hash-only changes do not. See Single-page apps.
If it isn't working
- Set the variable only in production. Subdomains count; other preview domains do not.
- For CSP, allow the origin in
script-srcandconnect-src; pass a nonce to<Script>. - Nextra and Fumadocs use App Router.
See Troubleshooting.