# 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.

Source: https://ranqo.ai/docs/integrations/site-tracking/nextjs

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.

### 1. 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_`.

### 2. Add the key to your environment
```bash
RANQO_SITE_KEY=rk_live_YOUR_SITE_KEY
```
Set it in your hosting provider's environment variables too, for every environment you want tracked.

### 3. Add the middleware
Create `middleware.ts` at the root of your project, next to `app/` or `pages/`. On Next.js 16, name the file `proxy.ts` and the function `proxy`.
```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)$).*)',
  ],
};
```

### 4. Deploy and verify
Deploy, then run the check in [Verify your install](https://ranqo.ai/docs/integrations/site-tracking/verify). 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 `POST` requests, 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.xml` and `/.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.

**Dashboard on the same project?:**
If your authenticated app is served from the same Next.js project on another subdomain, add that host to `SKIP_HOSTS` at the top of the file so your own team's clicks are not counted as marketing traffic.

## Next steps

- [Verify your install](https://ranqo.ai/docs/integrations/site-tracking/verify): Send a test event and confirm Ranqo accepted it.
- [How Ranqo classifies visits](https://ranqo.ai/docs/integrations/site-tracking): AI crawlers, AI assistants and human referrals, and how each is told apart.
