# Site Tracking for Cloudflare Workers

> Send AI crawler visits and AI referrals to Ranqo from a Cloudflare Worker in front of your site, whatever the site itself runs on.

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

If your site's traffic passes through Cloudflare, a Worker can report every page request to Ranqo without touching the site's own code. The Worker forwards each request to your site as usual and sends the report in the background.

### 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. Store the key as a secret
The Worker reads the key from `env.RANQO_SITE_KEY`. Add it as an encrypted secret rather than a plain variable, from your Worker's settings in the Cloudflare dashboard or with Wrangler:

```bash title="Terminal"
npx wrangler secret put RANQO_SITE_KEY
```

### 3. Add the Worker
```ts
// worker.ts — wire to your route in wrangler.toml
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 interface Env { RANQO_SITE_KEY: string }

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const path = url.pathname;

    // Only GET is a page view. Skip non-GET and any prefetch / RSC
    // request (Next.js apps behind a Worker would otherwise inflate).
    const secFetchDest = request.headers.get('sec-fetch-dest');
    const secPurpose = request.headers.get('sec-purpose');
    const isPrefetch =
      request.method !== 'GET' ||
      (secFetchDest && secFetchDest !== 'document') ||
      !!request.headers.get('next-router-prefetch') ||
      !!request.headers.get('next-router-segment-prefetch') ||
      request.headers.get('rsc') === '1' ||
      (secPurpose && secPurpose.startsWith('prefetch')) ||
      request.headers.get('purpose') === 'prefetch';

    const shouldTrack =
      !isPrefetch &&
      !path.startsWith('/api/') &&
      !SKIP_HOSTS.has(url.host) &&
      (TRACK_AI.test(path) || !SKIP_EXT.test(path));

    if (shouldTrack) {
      const params = new URLSearchParams({
        url: request.url,
        userAgent: request.headers.get('user-agent') || '',
        ref: request.headers.get('referer') || '',
        ip: request.headers.get('cf-connecting-ip') || '',
        websiteKey: env.RANQO_SITE_KEY,
      });
      // waitUntil keeps the fetch alive after we return the response.
      ctx.waitUntil(fetch(`https://app.ranqo.ai/api/v1/intake/pageview?${params}`).catch(() => {}));
    }

    return fetch(request);
  },
};
```
If you already run a Worker on these routes, add the tracking block to its `fetch` handler instead, before it returns its response.

### 4. Route it in front of your site
Attach the Worker to a route that covers your site, such as `example.com/*`, in the Cloudflare dashboard or in your Wrangler configuration. The Worker only sees requests on the routes it is attached to.

### 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 Worker does

- **Passes every request through.** It returns `fetch(request)`, so your site answers exactly as it did before.
- **Keeps the report alive after responding.** `ctx.waitUntil()` lets the call to Ranqo finish after the response has gone to the visitor. Without it, Cloudflare can stop the Worker as soon as the response is returned and the report is lost. A failed report is ignored.
- **Sends the real visitor address.** It reads `cf-connecting-ip`, the header Cloudflare sets to the visitor's IP. Ranqo uses it to check whether a crawler is genuine.
- **Counts page views only.** It skips anything but `GET`, prefetch and framework data requests, paths under `/api/`, and static assets.
- **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.

The `ExecutionContext` type comes from Cloudflare's Workers types package. In plain JavaScript, drop the type annotations.

**Dashboard on the same zone?:**
If your signed-in product runs on a host this Worker also covers, add it to `SKIP_HOSTS` at the top of the file so your team's own clicks are not counted as site visitors.

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