Site Tracking
Site Tracking for Next.js
Send AI crawler visits and AI referrals from a Next.js site to Ranqo with one middleware file. Works on Next.js 15 and 16.
Ranqo's Site Tracking sees every request your server handles, including the AI crawlers that never run JavaScript and so never reach GA4. On Next.js it is one middleware file.
Copy your site key
Open Traffic in the dashboard and choose Set up Site Tracking. Select Generate site key, then copy the key. Keys start with
rk_live_.Add the key to your environment
.envRANQO_SITE_KEY=rk_live_YOUR_SITE_KEYSet it in your hosting provider's environment variables too, for every environment you want tracked.
Add the middleware
Create
middleware.tsat the root of your project, next toapp/orpages/. On Next.js 16, name the fileproxy.tsand the functionproxy.middleware.ts// middleware.ts (Next.js 15) — or proxy.ts (Next.js 16, rename function to `proxy`) import { NextRequest, NextResponse } from 'next/server'; // OPTIONAL: skip self-tracking on subdomains you don't want counted as // "marketing site visitors" (e.g. your authenticated dashboard on // app.yoursite.com). Most teams can leave this empty. const SKIP_HOSTS = new Set<string>([ // 'app.yoursite.com', // 'admin.yoursite.com', ]); export function middleware(request: NextRequest) { // Only track GET. POST/PUT/DELETE/HEAD/OPTIONS never represent a page view. if (request.method !== 'GET') return NextResponse.next(); // CRITICAL — skip Next.js prefetch / RSC requests. Without these checks, // every <Link> hover fires a fake page view; traffic inflates 5-10x. const secFetchDest = request.headers.get('sec-fetch-dest'); if (secFetchDest && secFetchDest !== 'document') return NextResponse.next(); if (request.headers.get('next-router-prefetch')) return NextResponse.next(); if (request.headers.get('next-router-segment-prefetch')) return NextResponse.next(); if (request.headers.get('rsc') === '1') return NextResponse.next(); const secPurpose = request.headers.get('sec-purpose'); if (secPurpose && secPurpose.startsWith('prefetch')) return NextResponse.next(); if (request.headers.get('purpose') === 'prefetch') return NextResponse.next(); const { pathname } = request.nextUrl; if (pathname.startsWith('/api/')) return NextResponse.next(); if (SKIP_HOSTS.has(request.headers.get('host') || '')) return NextResponse.next(); const params = new URLSearchParams({ url: request.url, userAgent: request.headers.get('user-agent') || '', ref: request.headers.get('referer') || '', ip: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || '', websiteKey: process.env.RANQO_SITE_KEY!, }); fetch(`https://app.ranqo.ai/api/v1/intake/pageview?${params}`).catch(() => {}); return NextResponse.next(); } // AI-discovery files (robots.txt, llms.txt, sitemap.xml, .well-known/*) are // the highest-signal hits — allowlist them above the static-asset skip rule. export const config = { matcher: [ '/robots.txt', '/llms.txt', '/llms-full.txt', '/sitemap.xml', '/sitemap-:path*', '/ai.txt', '/.well-known/:path*', '/((?!api|_next/static|_next/image|favicon.ico|manifest.webmanifest|.*\\.(?:png|jpg|jpeg|svg|gif|webp|ico|css|js|mjs|map|woff2?|ttf|otf|eot|txt|xml|json)$).*)', ], };Deploy and verify
Deploy, then run the check in Verify your install. The install page in the dashboard also watches for your first real visit and confirms it when it arrives.
What the middleware does
- Never slows your pages. The call to Ranqo is fired without waiting for an answer, so it adds no latency.
- Counts page views only. It skips
POSTrequests, API routes, and the prefetch and RSC requests Next.js makes when a visitor hovers a link. Without those checks, a site's traffic reads five to ten times higher than it is. - Keeps the files AI crawlers read first.
robots.txt,llms.txt,sitemap.xmland/.well-known/*are the highest-signal requests an AI crawler makes, so the matcher lets them through ahead of the static-file rule. - Skips images, fonts, scripts and styles, which are noise.