# Site Tracking for Express

> Send AI crawler visits and AI referrals from an Express app to Ranqo with one middleware function, with notes for Fastify, Hono and Koa.

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

On an Express app, Site Tracking is one middleware function. It reports each page request to Ranqo after your response has gone out, so your visitors never wait on it.

### 1. Copy your site key
On the **Traffic** page, press **Set up Site Tracking**, then **Generate site key** on the **Install Site Tracking** page. Copy the key. It starts with `rk_live_`.

### 2. Add the key to your environment
```bash
RANQO_SITE_KEY=rk_live_YOUR_SITE_KEY
```
Set the same variable in your hosting provider's environment for every environment you want tracked. Keep it out of your repository.

### 3. Add the middleware file
Save this as `ranqo-intake.ts` (or `.js`, without the type imports). It uses the global `fetch`, available from Node.js 18.
```ts
// ranqo-intake.ts — wire with: app.use(ranqoIntake);
import type { Request, Response, NextFunction } from 'express';

const SKIP_EXT = /\.(png|jpg|jpeg|svg|gif|webp|ico|css|js|mjs|map|woff2?|ttf|otf|eot|txt|xml|json)$/i;
const TRACK_AI = /^\/(robots\.txt|llms(-full)?\.txt|sitemap(-.*)?\.xml|ai\.txt|\.well-known\/.*)$/;

// OPTIONAL: skip self-tracking on subdomains you don't want counted as
// "marketing site visitors" (e.g. your authenticated dashboard).
const SKIP_HOSTS = new Set<string>([
  // 'app.yoursite.com',
  // 'admin.yoursite.com',
]);

export function ranqoIntake(req: Request, res: Response, next: NextFunction) {
  // Forward immediately — fire-and-forget happens after the handler returns.
  next();

  res.on('finish', () => {
    // Only track GET. POST/PUT/DELETE/HEAD/OPTIONS never represent a page view.
    if (req.method !== 'GET') return;

    // Skip prefetch / RSC requests (matters when Express is proxying or
    // hosting a Next.js app — every <Link> hover would otherwise count).
    const secFetchDest = req.get('sec-fetch-dest');
    if (secFetchDest && secFetchDest !== 'document') return;
    if (req.get('next-router-prefetch')) return;
    if (req.get('next-router-segment-prefetch')) return;
    if (req.get('rsc') === '1') return;
    const secPurpose = req.get('sec-purpose');
    if (secPurpose && secPurpose.startsWith('prefetch')) return;
    if (req.get('purpose') === 'prefetch') return;

    const path = req.path;
    if (path.startsWith('/api/')) return;
    if (SKIP_EXT.test(path) && !TRACK_AI.test(path)) return;
    if (SKIP_HOSTS.has(req.get('host') || '')) return;

    const params = new URLSearchParams({
      url: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
      userAgent: req.get('user-agent') || '',
      ref: req.get('referer') || '',
      ip: ((req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim()) || req.ip || '',
      websiteKey: process.env.RANQO_SITE_KEY || '',
    });

    fetch(`https://app.ranqo.ai/api/v1/intake/pageview?${params}`).catch(() => {});
  });
}
```

### 4. Register it before your routes
Import it and call `app.use(ranqoIntake)` after your `trust proxy` setting and before your routes and any `express.static` middleware. Express runs middleware in order, and a route or static handler that sends a response does not pass the request on, so middleware registered after it never sees that request.

```ts title="app.ts"
import express from "express";
import { ranqoIntake } from "./ranqo-intake";

const app = express();
app.set("trust proxy", true); // match your proxy setup
app.use(ranqoIntake);
app.use(express.static("public"));
// ...your routes
```

### 5. Deploy and verify
Deploy, then follow [Verify your install](https://ranqo.ai/docs/integrations/site-tracking/verify). The install page in the dashboard watches for your first real visit and confirms it when it arrives.

## What the middleware does

- **Adds no latency.** It calls `next()` straight away and sends the report to Ranqo when the response has finished, without waiting for Ranqo's answer. A failed report is ignored.
- **Counts page views only.** It skips anything but `GET`, paths under `/api/`, and the prefetch and framework data requests a Next.js front end makes when it is served through Express.
- **Keeps the files AI crawlers read first.** `robots.txt`, `llms.txt`, `sitemap.xml` and `/.well-known/*` are reported even though other files with those extensions are skipped as static assets.
- **Sends the real visitor address.** It reads the first address in `x-forwarded-for`, which your proxy or load balancer sets, and falls back to `req.ip`. Ranqo uses the address to check whether a crawler is genuine, so a proxy's own address makes real crawlers look unverified.

**Dashboard on the same app?:**
If the same Express app also serves your signed-in product on another host, add that host to `SKIP_HOSTS` at the top of the file so your team's own clicks are not counted as site visitors.

## Fastify, Hono and Koa

The same logic works in any Node framework: after the response is sent, apply the same checks, build the same query string and call the intake endpoint without awaiting it.

- **Fastify:** put the body of the `res.on('finish', ...)` handler in an `onResponse` hook, reading headers from `request.headers`.
- **Hono and Koa:** in `app.use(async (c, next) => { ... })` (Koa: `(ctx, next)`), call `await next()` first, then run the checks and fire the request.

On a serverless or edge runtime, a request still in flight when the handler returns can be cut off. Use the platform's way of keeping work alive after the response, as the [Cloudflare Worker](https://ranqo.ai/docs/integrations/site-tracking/cloudflare-worker) install does with `waitUntil`.

## 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): Crawlers, assistants and human referrals, and how each is told apart.
