Stickybeak Docs Dashboard Get early access

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

  1. Put your site ID in an environment variable

    Set NEXT_PUBLIC_STICKYBEAK_SITE_ID in .env.production or production host settings.

    NEXT_PUBLIC_STICKYBEAK_SITE_ID=YOUR_SITE_ID
  2. Add the script to your root layout

    Add it inside <body> in app/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.com with your configured domain.

  3. 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-src and connect-src; pass a nonce to <Script>.
  • Nextra and Fumadocs use App Router.

See Troubleshooting.