Next.js needs the tag in one place and then leaves you alone: client-side navigations count themselves, so there is no router event to subscribe to and nothing to call on route change.

Get the snippet from your dashboard first. It looks like this, with your own site ID in the path:

<script src="https://t.skomi.com/s/your-site-id.js" defer></script>

App Router

In app/layout.tsx, inside the <body>:

import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://t.skomi.com/s/your-site-id.js"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}

afterInteractive is the default and the right one here. The page should finish loading first, and nothing on it waits for analytics.

Pages Router

The same tag in pages/_app.tsx:

import Script from "next/script";
import type { AppProps } from "next/app";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <Script
        src="https://t.skomi.com/s/your-site-id.js"
        strategy="afterInteractive"
      />
    </>
  );
}

_app rather than _document, so it survives a client-side navigation the same way the rest of your app does.

Client-side navigation is already counted

This is the part people expect to have to write, and there is nothing to write.

Skomi's script wraps history.pushState and history.replaceState and listens for popstate, so every route change Next makes is a page view, whether the visitor clicked a <Link>, went back, or you called router.push yourself.

Two details worth knowing, because they are the ones that look like bugs:

  • A navigation that does not change the address is not counted. Replacing the URL with the same URL, which a filter or a scroll-position update sometimes does, is ignored rather than counted as a second view.
  • The page being left is finished first. Its time on page and scroll depth are flushed before the new view opens, so a single-page app reports attention per route rather than one number for the whole session.

Do not use strategy="worker"

next/script offers a worker strategy that moves a third-party script into a web worker through Partytown. It is a good idea for scripts that only send events, and it breaks this one completely: a worker has no document, so there is no page to read.

Everything Skomi does needs the real page: heatmaps and session recordings are a picture of it, the feedback widget has to appear on it, and even plain analytics needs the address the visitor is actually looking at. Use afterInteractive.

beforeInteractive is not wrong, only unnecessary: it exists for scripts that must run before hydration, and counting a visit is not one of those.

If you have a Content Security Policy

Next.js makes a strict CSP easy to add, and a strict CSP will block this until you tell it not to. The script and the data it sends both use one host:

script-src  https://t.skomi.com
connect-src https://t.skomi.com

If your policy uses a nonce, apply it to this tag the way you do to your own.

Nothing arrives in development

localhost is not the domain you registered, so hits from it are refused by the origin check. That is deliberate, because your own work should not be in your numbers, and it does mean npm run dev is not where you confirm the installation. Deploy to the real domain, or to a subdomain of it, and look there.

Check it is working

Load a page on the real domain, click through to another route, and look at your dashboard: you should see two page views rather than one. If nothing arrives, see what to check when the snippet is not working.