JavaScript API
The routingcat package, which npx routingcat init installs, runs on your host, in the entry init writes:
routingcat/vercel, Vercel’s root middleware;routingcat/cloudflare, a Worker, or a Pages middleware.
Your pages carry the head partial npx routingcat push writes, with RoutingCat’s tracker. A conversion only the page sees is sent with the browser script, and one your server sees through the HTTP events API.
routingcat/vercel
middleware()
middleware(handler: MiddlewareHandler | null, options: MiddlewareOptions): (request: Request, context?: MiddlewareContext) => Promise<Response>Vercel’s Routing Middleware, your root middleware.ts’s default export, on Node.js, as init writes it: middleware(null, { … }). It picks each visitor’s variants before Vercel serves the page, from its cache or not, and passes /_rc on to RoutingCat, so that the browser’s reports reach it through your own domain. Around a middleware of your own, middleware(yourMiddleware, { …, paths }), your code first, as in worker(app, …): your middleware runs first on your own paths.
From the example app:
// Vercel's root middleware for a static site, the file `init` writes (integrations/a_architecture// §4.4): RoutingCat decides every page before Vercel's cache, on Node.js, with the build, which// `routingcat build` writes to .routingcat/ before Vercel builds this file.import { HTML_DESCRIPTOR, middleware } from 'routingcat/vercel'import build from './.routingcat/values.mjs'
export const config = { runtime: 'nodejs', matcher: ['/((?!_rc/|.*\\.(?!(?:html?|php)$)[a-z0-9]+$).*)', '/_rc/:path*'], // RoutingCat: the pages, and /_rc}
export default middleware(null, { framework: HTML_DESCRIPTOR, matcher: config.matcher, build,})| Option | Type | Default | What it does |
|---|---|---|---|
framework |
FrameworkDescriptor |
Required | What renders your pages, as init wrote it: its descriptor, whose contents are RoutingCat’s own. |
matcher |
string | string[] |
Required | config.matcher, as it is: the paths RoutingCat runs on, which the middleware reads as Vercel does, with the test of what is one of your pages. Around a middleware of your own, RoutingCat’s entries of it alone. |
build |
RoutingCatBuild |
Required | .routingcat/values.mjs’s default export, the build, which your build writes and the package types: its values, and the snapshot, the routing data a new instance decides with until its first refresh and while RoutingCat can’t be reached. Without values, pages serve their defaults and nothing is recorded. |
paths |
string[] |
None | Your own middleware’s matcher entries, unchanged, as data: your handler runs only on those paths. |
imagePreload |
boolean |
Sent, but behind a Cloudflare zone | Whether a routed page sends a Link preload of its selected variants’ main images, which data-rc-lcp marks: true always, false never. Unset, it is sent, but to a visitor who comes through a Cloudflare zone in front of Vercel, since Cloudflare’s Early Hints would send one visitor’s preload to the next: set true once Early Hints is off for your routed paths. |
Also exported
HTML_DESCRIPTOR, the framework of a site its templates render.- The types
MiddlewareOptions,MiddlewareHandler,MiddlewareContext,RoutingCatBuildandFrameworkDescriptor.
routingcat/cloudflare
worker()
worker<Env>(app: CloudflareWorker<Env> | null, options: WorkerOptions): CloudflareWorker<Env> & { fetch(request: Request, env: Env, ctx: WorkerContext): Promise<Response> }The Worker that picks each visitor’s variants before the page is served, your Worker’s default export, as init writes it, and that passes /_rc on to RoutingCat.
It stands in one of two places:
| Mode | Where | Its call |
|---|---|---|
assets |
In front of your site’s files, which it serves from the ASSETS binding |
worker(null, { mode: 'assets', … }) |
upstream |
In front of your own server, on the route of your production address | worker(null, { mode: 'upstream', … }) |
From the example app, in assets mode:
// The Worker in front of the site's files on Cloudflare Workers, in assets mode, the file `init`// writes for a static site (integrations/a_architecture §4.5): RoutingCat decides each page, then// serves it from the site's files, with the build and the snapshot compiled from its push, which// `routingcat build` writes to .routingcat/ before Wrangler bundles this.import { HTML_DESCRIPTOR, worker } from 'routingcat/cloudflare'import build from './.routingcat/values.mjs'
export default worker(null, { mode: 'assets', framework: HTML_DESCRIPTOR, // The same literal as run_worker_first in the Wrangler configuration. matcher: ['/*'], build, // Cloudflare's Early Hints would hint one visitor's image to the next: true once it is off here. imagePreload: false,})| Option | Type | Default | What it does |
|---|---|---|---|
mode |
'assets' | 'upstream' |
Required | Where the Worker stands, as above. |
upstream |
string |
None | In upstream mode, your server’s address, as https://upstream.example.com. Without it, the Worker fetches each page from the address it was asked for, as its route sends it. |
framework |
FrameworkDescriptor |
Required | What renders your pages, as init wrote it: its descriptor, whose contents are RoutingCat’s own. |
matcher |
string | string[] |
Required | The paths RoutingCat runs on, as init wrote them: the same patterns as run_worker_first in your Wrangler configuration, which the Worker reads as Cloudflare does, with the test of what is one of your pages. |
build |
RoutingCatBuild |
Required | .routingcat/values.mjs’s default export, the build, which your build writes and the package types: its values, and the snapshot, the routing data a new instance decides with until its first refresh and while RoutingCat can’t be reached. Without values, pages serve their defaults and nothing is recorded. |
imagePreload |
boolean |
Not sent: Cloudflare forwards every request | Whether a routed page sends a Link preload of its selected variants’ main images, which data-rc-lcp marks: true always, false never. Unset, it is sent unless a Cloudflare zone forwarded the request, as one forwards every request your deployed Worker receives, since Cloudflare’s Early Hints would send one visitor’s preload to the next: set true once Early Hints is off for your routed paths. |
pages()
pages<Env>(options: PagesOptions): (context: PagesContext<Env>) => Promise<Response>A static site’s Pages middleware, export const onRequest = pages({ … }) in functions/_middleware.ts, as init writes it. It picks each visitor’s variants before Pages serves the page from your files, and registers under the deployment’s own address, which your build embeds in the build.
| Option | Type | Default | What it does |
|---|---|---|---|
framework |
FrameworkDescriptor |
Required | What renders your pages, as init wrote it: its descriptor, whose contents are RoutingCat’s own. |
matcher |
string | string[] |
Required | ['/*'], as init wrote it: every path, of which the test of what is one of your pages keeps the pages. |
build |
RoutingCatBuild |
Required | .routingcat/values.mjs’s default export, the build, which your build writes and the package types: its values, and the snapshot, the routing data a new instance decides with until its first refresh and while RoutingCat can’t be reached. Without values, pages serve their defaults and nothing is recorded. |
imagePreload |
boolean |
Not sent: Cloudflare forwards every request | Whether a routed page sends a Link preload of its selected variants’ main images, which data-rc-lcp marks: true always, false never. Unset, it is sent unless a Cloudflare zone forwarded the request, as one forwards every request your deployed Worker receives, since Cloudflare’s Early Hints would send one visitor’s preload to the next: set true once Early Hints is off for your routed paths. |
Also exported
HTML_DESCRIPTOR, the framework of a site its templates render.- The types
WorkerOptions,CloudflareOptions,CloudflareWorker,WorkerContext,RoutingCatBuild,FrameworkDescriptor,PagesOptionsandPagesContext.
The head partial
Each page’s <head> includes the head partial npx routingcat push writes, first, after the charset and viewport tags; on Nuxt 2, init writes the same two scripts first in nuxt.config.js’s head.script. Never edit the partial: push writes it again when RoutingCat’s version changes. It holds:
- the head snippet,
<script id="rc-snippet">, inline: it shows the variants your host’s middleware or Worker picked, from the response’sServer-Timingheader, before the page paints; - the tracker, from
cdn.routingcat.com, pinned by its version and its integrity hash and loadeddefer: it reports what each slot showed and how it was seen, with your site’s public key,data-key.
The example app’s tracker:
<script src="https://cdn.routingcat.com/tracker/1.0.3.js" integrity="sha384-rOp3ncjPCQM7Wz68DCdWQhSGdppbYMFXIdrPVVqpeI38PMK/vOu7LJVZAFhbHzEr" crossorigin="anonymous" defer data-key="pk_routingcat-static-example_Tc5Wm9Rb"></script>routingcat.routingcatProperties
The tracker puts its calls on the page’s window.routingcat, whose routingcatProperties is PostHog’s before_send hook: it puts the visitor id and the variants the page shows on each PostHog event as it leaves. The tracker loads defer, after PostHog’s snippet has run init, and a blocker may stop it, so your PostHog init calls the hook only once window.routingcat exists, as the example app’s home page does:
<script> … posthog.init('phc_routingcat_static_example', { api_host: 'https://us.i.posthog.com', defaults: '2026-08-30', // RoutingCat's visitor id and each slot's variant on every event, once its tracker has // loaded: the tracker runs after this, deferred, and an ad blocker may stop it. before_send: (event) => (window.routingcat ? routingcat.routingcatProperties(event) : event), }) </script>Events PostHog captures before the tracker has loaded, which can include a page view’s first $pageview, go without the properties; where the tracker doesn’t load, each event goes as it came. A page that also loads the browser script has one window.routingcat all the same: whichever of the two scripts loads first makes it, and the other adds its calls, replacing none.
routingcat.prepareNavigation() and routingcat.navigate()
routingcat.prepareNavigation(path: string): Promise<void>routingcat.navigate(): voidThe tracker’s calls for a router that renders the next page in the browser, as Nuxt 2’s does: before the next page renders, prepareNavigation(path) asks your middleware or Worker for that page’s variants and shows them, waiting a second at most, after which the page shows its defaults; once the address shows the page, navigate() starts its view, which the tracker reports. Both never throw. See Decide Nuxt 2’s in-page navigations.
routingcat.start()
routingcat.start(options: StartOptions): voidStarts the tracker, once per page load. Its script tag starts it from data-key and data-env, so your pages need not call it.
The browser script
routingcat/browser as a script, for a conversion only the page sees, such as a form a third party handles. It puts its calls on the page’s window.routingcat, beside the tracker’s, and sends events with your site’s public key through your site’s /_rc path, which RoutingCat’s middleware or Worker passes on to RoutingCat. It loads at a URL that never changes for a version, with its integrity hash:
<script src="https://cdn.routingcat.com/browser/1.0.3.js" integrity="sha384-ZnRbP0Groc12kRo2DHm+EpSi/r27mWVkwT7ehqGhXHZY3NjqdZvTJHtolZL8IbFW" crossorigin="anonymous" data-key="pk_…"></script>The copy of this page in node_modules/routingcat/docs/ gives the installed version’s hash. The script reads data-key, your site’s public key, from routingcat.config.json; data-env, preview for a preview; and data-collector, as routingcat.init’s collector. Or copy node_modules/routingcat/dist/browser.global.js into your site and load it from there. A page’s own code bundled with routingcat/browser takes the same calls from its routingcat object, and its types InitOptions, TrackOptions, IdentifyOptions and LineItem.
routingcat.init()
routingcat.init(key: string, options?: InitOptions | null): voidSets your public key and where to send, as the tag’s attributes do. Calls before it send nothing.
| Option | Type | Default | What it does |
|---|---|---|---|
env |
'production' | 'preview' |
production |
The environment the events count in: a preview’s stay out of your results. |
collector |
string |
The page’s own | The site whose /_rc path to send through, such as https://www.lumen.com for a page on another domain. |
routingcat.track() and routingcat.identify()
routingcat.track(name: string, options?: TrackOptions): Promise<void>routingcat.identify(accountId: string, options?: IdentifyOptions): Promise<void>Report an event, such as a conversion, and link the visitor to your app’s account id, from the page. Both never throw. An event’s time is when it arrives, and its options are:
| Option | Type | Default | What it does |
|---|---|---|---|
id |
string |
None | The dedup key: an event sent again with the same name and id counts once. Generated when absent. |
visitorId |
string |
The rc_vid cookie’s |
The visitor. |
accountId |
string |
None | Your app’s id for the account, when you have it: a record id, never an email address. |
value |
number |
None | A number. With currency, money in major units: 29.99 for $29.99. Without, a plain quantity. |
currency |
string |
None | The ISO 4217 code, in either case. |
properties |
object | None | An object of at most 8,000 bytes of JSON, recorded with the event. |
items |
LineItem[] |
None | Line items: { id, quantity?, unitValue }, in the event’s currency, quantity 1 when absent. |
routingcat.link()
routingcat.link(url: string | URL): stringA link to another of your domains, carrying the visitor id, so the visit continues there: the script on that domain adopts the id. Add the domain with npx routingcat domains add.
The page has one window.routingcat, whichever of the browser script and the head partial’s tracker loads first: the first makes it, and the other adds its calls, replacing none. Both scripts give the same PostHog hook, routingcat.routingcatProperties, which your PostHog init calls once window.routingcat exists.
The HTTP events API
Your server reports a conversion with one request, with the server events key, which npx routingcat keys server prints: the identity link first, as identify makes it, then the event, each with the visitor id from the request’s rc_vid cookie.
POST https://ingest.routingcat.com/v1/eventsAuthorization: Bearer sk_…Content-Type: application/json
[{"type":"identity_link","id":"idl:acct_42:v_8v_ztmJDpYzDKH7UxXwXPA","visitor_id":"v_8v_ztmJDpYzDKH7UxXwXPA","account_id":"acct_42"}, {"name":"signup","id":"signup:acct_42","visitor_id":"v_8v_ztmJDpYzDKH7UxXwXPA","account_id":"acct_42"}]The HTTP events API has every field, the limits and the answers.