Next.js
Integrate Vendo web tracking with Next.js: App Router and Pages Router setup with client-side navigation tracking.
Last reviewed September 15, 2026
Add Vendo tracking to your Next.js app. Both App Router and Pages Router are supported.
App Router
Add the CDN snippet to your root layout using Next.js’s <Script> component:
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<Script id="vendo-tracking" strategy="afterInteractive">
{`
(function(w,d,s,e,l,k){w['VendoObject']=e;w[e]=w[e]||function(){
(w[e].q=w[e].q||[]).push(Array.prototype.slice.call(arguments))};w[e].l=1*new Date();
l=d.createElement(s);k=d.getElementsByTagName(s)[0];
l.async=1;l.src='https://cdn.vendodata.com/sdk/v1/vendo.js';k.parentNode.insertBefore(l,k);
})(window,document,'script','vendo');
vendo('init', 'YOUR_WRITE_KEY', {
host: 'https://YOUR_TRACKING_ENDPOINT'
});
`}
</Script>
</head>
<body>{children}</body>
</html>
);
}Note: The snippet from the Vendo app ends with
vendo('page');. Remove that line in Next.js. TheTrackPageViewscomponent below sends the first page view. If you keep the line, Vendo records the first page view 2 times.
Track Client-Side Navigations
Page views are explicit. Since SDK 0.2.0, init never sends one automatically. Add a route-change listener component. It sends one canonical Page Viewed event for the initial route and for each client-side pathname or query-string change:
// components/track-page-views.tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
declare global {
interface Window {
vendo?:
| ((...args: unknown[]) => void)
| {
page: (name?: string, properties?: Record<string, unknown>) => void;
identify: (userId: string, traits?: Record<string, unknown>) => void;
reset: () => void;
};
}
}
// The page can render before the afterInteractive snippet runs. Create the same
// queue stub the snippet uses, so an early page view waits for the SDK.
function getVendo(): NonNullable<Window['vendo']> {
const w = window as any;
w.vendo =
w.vendo ||
function () {
(w.vendo.q = w.vendo.q || []).push(Array.prototype.slice.call(arguments));
};
return w.vendo;
}
export function trackPageView(pathname: string, search: string) {
const page = search ? `${pathname}?${search}` : pathname;
const properties = { path: pathname, search, page };
const vendo = getVendo();
if (typeof vendo === 'function') {
vendo('page', 'Page Viewed', properties);
} else {
vendo.page('Page Viewed', properties);
}
}
export function TrackPageViews() {
const pathname = usePathname();
const searchParams = useSearchParams();
const search = searchParams.toString();
useEffect(() => {
trackPageView(pathname, search);
}, [pathname, search]);
return null;
}Add <TrackPageViews /> to your root layout inside a <Suspense> boundary (required by useSearchParams):
// app/layout.tsx (body section)
import { Suspense } from 'react';
import { TrackPageViews } from '@/components/track-page-views';
<body>
<Suspense fallback={null}>
<TrackPageViews />
</Suspense>
{children}
</body>;Pages Router
Add the snippet to _app.tsx and track route changes with the Next.js router:
// pages/_app.tsx
import type { AppProps } from 'next/app';
import Script from 'next/script';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { trackPageView } from '@/components/track-page-views';
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
useEffect(() => {
const trackCurrentPage = (url: string) => {
const nextUrl = new URL(url, window.location.origin);
trackPageView(nextUrl.pathname, nextUrl.search.slice(1));
};
trackCurrentPage(window.location.href);
router.events.on('routeChangeComplete', trackCurrentPage);
return () => router.events.off('routeChangeComplete', trackCurrentPage);
}, [router.events]);
return (
<>
<Script id="vendo-tracking" strategy="afterInteractive">
{`
(function(w,d,s,e,l,k){w['VendoObject']=e;w[e]=w[e]||function(){
(w[e].q=w[e].q||[]).push(Array.prototype.slice.call(arguments))};w[e].l=1*new Date();
l=d.createElement(s);k=d.getElementsByTagName(s)[0];
l.async=1;l.src='https://cdn.vendodata.com/sdk/v1/vendo.js';k.parentNode.insertBefore(l,k);
})(window,document,'script','vendo');
vendo('init', 'YOUR_WRITE_KEY', {
host: 'https://YOUR_TRACKING_ENDPOINT'
});
`}
</Script>
<Component {...pageProps} />
</>
);
}Identify Users
Call identify once the user is authenticated:
const authUserId = user.id; // Use the same stable ID sent to Mixpanel.
if (typeof window.vendo === 'function') {
window.vendo('identify', authUserId, { plan: user.plan });
} else {
window.vendo?.identify(authUserId, { plan: user.plan });
}Call reset on logout to clear the stored identity. The SDK starts a new anonymous ID on the next page load:
if (typeof window.vendo === 'function') {
window.vendo('reset');
} else {
window.vendo?.reset();
}If Mixpanel is also installed, call mixpanel.identify(authUserId) and mixpanel.reset() at the same lifecycle points. This lets Vendo events and Mixpanel browser activity resolve to the same user profile. Use an opaque stable application ID rather than an email address as the primary identity key.
Related
- JavaScript SDK: Full method reference and configuration options
- Quickstart: Get started in 5 minutes