Site Tracking
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.
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.
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_.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:Terminalnpx wrangler secret put RANQO_SITE_KEYAdd the Worker
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
fetchhandler instead, before it returns its response.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.Deploy and verify
Deploy, then follow Verify your install. 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.xmland/.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.