JavaScript API
The routingcat package, which npx routingcat init installs, gives your Nuxt site:
routingcat/nuxt, the module innuxt.config;routingcat/vue, the component and the composable your pages use,routingcat, whose calls report conversions from your server routes and your pages,routingcatPropertiesfor your PostHoginit, and the declarations;routingcat/vercelorroutingcat/cloudflare, the entry that runs on your host: Vercel’s middleware, or the Worker on Cloudflare Workers;routingcat/browser, for other frontends, such as an app on another of your domains, also as a script tag.
routingcat/nuxt
The module, in nuxt.config’s modules, as init adds it. From the example app:
export default defineNuxtConfig({ modules: ['routingcat/nuxt'], compatibilityDate: '2026-10-01', devtools: { enabled: false }, // A page of each kind RoutingCat routes, all through `select`: prerendered, and regenerated // incrementally where the host does it; /about renders at each request and is not routed. routeRules: { '/': { prerender: true }, '/pricing': { isr: 3600 }, }, // PostHog's project key and host, which app/plugins/posthog.client.ts initializes it with: the // key is a placeholder that names no PostHog project, so what the pages send reaches none. runtimeConfig: { public: { posthogKey: 'phc_routingcat_nuxt_example', posthogHost: 'https://us.i.posthog.com', }, },})It:
- puts the head snippet among the first tags of every page’s
<head>, which shows each visitor’s variants before the page paints; - makes
<RoutingCatSlot>anduseRoutingCat()available in every component; - at each
nuxt build, registers whatroutingcat/declares, and writes the build’s values for your host’s entry; - on each in-page navigation to a routed page, asks for that page’s variants beside Nuxt’s own payload;
- makes
routingcatavailable in your server code, its calls taking the request’seventfirst; - under
nuxt dev, lets?rc=force variants, and records nothing.
It runs on Nuxt 4’s default server, Nitro 2, and matches your pages’ routes from your site’s root, so your app is served from /, with no app.baseURL of its own; the build says when either is not so.
Its option goes in nuxt.config, as routingcat: { … }:
| Option | Type | Default | What it does |
|---|---|---|---|
posthog |
string |
None | The module whose default export is your app’s PostHog client, which RoutingCat registers the visitor id and the variants with where your app provides no $posthog: a path from your project’s root, or one of Nuxt’s aliases, as '~/lib/posthog'. Unset, posthog-js’s own instance, where your project depends on posthog-js; else window.posthog. See PostHog. |
The entry also exports its type, RoutingCatNuxtOptions.
routingcat/vue
<RoutingCatSlot>
Renders one slot of a page with every variant in it, the default shown and the others hidden, so every visitor gets the same HTML and Vue’s hydration always matches; the head snippet shows each visitor’s variants before the page paints. The module makes it available in every component. From the example app:
<script setup lang="ts">import { home } from '~~/routingcat/home'</script>
<template> <main> <RoutingCatSlot :page="home" name="hero" v-slot="{ fields }"> <h1>{{ fields.headline }}</h1> </RoutingCatSlot> <RoutingCatSlot :page="home" name="subhead" v-slot="{ fields }"> <p>{{ fields.text }}</p> </RoutingCatSlot> <NuxtLink to="/pricing">See pricing</NuxtLink> </main></template>| Prop | Type | Default | What it does |
|---|---|---|---|
page |
PageDeclaration |
Required | The page’s declaration, which the page imports from routingcat/. |
name |
string |
Required | The slot’s key, one the page declares. |
tag |
'div' | 'span' |
div |
span inside a paragraph, or anywhere else only inline content can go, where a div would close the paragraph. |
Its default slot renders each variant, and receives:
| Member | Type | What it does |
|---|---|---|
fields |
object | The variant’s fields, typed from the declaration. |
variant |
string |
The variant’s key. |
The classes and ids you give it go on the element that holds the slot.
useRoutingCat()
useRoutingCat<P extends PageDeclaration>(page: P): RoutingCatState<P>Which variant each slot of a page shows, for code that must know, such as an analytics call; it never renders a slot, which <RoutingCatSlot> renders whole. The module makes it available everywhere. It returns two refs:
| Member | Type | What it does |
|---|---|---|
shown |
ref | The variant each slot shows, by slot key. Through the server’s render and the hydration, it holds the slots’ defaults, so a render that reads it matches the server’s; from mount on, what the page shows, following each in-page navigation. |
applied |
ref | false while shown holds the defaults in place of what the page shows, through the server’s render and the hydration; true from mount on, the defaults then being what the page shows, where it shows them. |
From the example app, which shows the lead once the page has read it:
<script setup lang="ts">import { pricing } from '~~/routingcat/pricing'
// Which lead the visitor sees, for code that must know: `applied` once the page has read it, after// mount; until then, `shown` holds the defaults in its place.const { shown, applied } = useRoutingCat(pricing)</script>
<template> <main> <h1>Pricing</h1> <RoutingCatSlot :page="pricing" name="lead" v-slot="{ fields }"> <p class="lead">{{ fields.lead }}</p> </RoutingCatSlot> <p> Start your <RoutingCatSlot :page="pricing" name="trial" tag="span" v-slot="{ fields }">{{ fields.days }}-day</RoutingCatSlot> trial. </p> <p v-if="applied" class="shown">Showing {{ shown.lead }}</p> </main></template>routingcat.track()
routingcat.track(event: ServerEvent, name: string, options: TrackOptions & { readonly id: string }): Promise<void>routingcat.track(name: string, options?: TrackOptions): Promise<void>Reports an event, such as a conversion. In a server route, pass the route’s event first: the module makes routingcat available in your server code, where its calls take the event, and your editor flags a call without it, which would send nothing. Call it without await: it sends within the request’s lifetime, so a page is never slower, and never throws, so it never breaks a sign-up. In a component, import routingcat from routingcat/vue and leave the event out: it sends from the browser, with your site’s public key, and the event’s time is when it arrives. From the example app:
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
export default defineEventHandler(async (event) => { const body = await readBody<{ email?: unknown } | null>(event) const email = String(body?.email ?? '').trim() if (!EMAIL.test(email)) { setResponseStatus(event, 400) return { ok: false, message: 'Enter a valid email address.' } } const digest = await crypto.subtle.digest( 'SHA-256', new TextEncoder().encode(email.toLowerCase()), ) const id = Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('') routingcat.track(event, 'waitlist', { id: `waitlist:${id}` }) return { ok: true, message: 'You are on the list.' }})| Option | Type | What it does |
|---|---|---|
id |
string |
The dedup key: an event sent again with the same name and id counts once. Required on the server: the provider’s event id, or signup:<accountId>; a call without it is not sent, and says so once in your logs. Generated in the browser. |
occurredAt |
Date | string | number |
When it happened: a Date, an RFC 3339 string, or a number of Unix seconds, or of milliseconds as Date.now() gives them; the call’s time when absent. The browser always uses the time the event arrives. |
visitorId |
string |
The visitor, when the request is not theirs, as in a webhook: the rc_vid its checkout recorded. The request’s rc_vid cookie by default. |
accountId |
string |
Your app’s id for the account, when you have it: a record id, never an email address. |
value |
number |
A number. With currency, money in major units: 29.99 for $29.99. Without, a plain quantity. |
currency |
string |
The ISO 4217 code, in either case. |
properties |
object | An object of at most 8,000 bytes of JSON, recorded with the event. |
items |
LineItem[] |
Line items: { id, quantity?, unitValue }, in the event’s currency, quantity 1 when absent. |
routingcat.identify()
routingcat.identify(event: ServerEvent, accountId: string, options?: IdentifyOptions): Promise<void>routingcat.identify(accountId: string, options?: IdentifyOptions): Promise<void>Links the visitor to your app’s account id, at sign-up and at every sign-in, so conversions on another device count. In a server route, pass the route’s event first. It never delays the response and never throws. Write await before it, as the examples do; RoutingCat joins a link and the events in whichever order they arrive. From the example app:
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
export default defineEventHandler(async (event) => { const body = await readBody<{ email?: unknown } | null>(event) const email = String(body?.email ?? '').trim() if (!EMAIL.test(email)) { setResponseStatus(event, 400) return { ok: false, message: 'Enter a valid email address.' } } const account = `acct_${crypto.randomUUID()}` await routingcat.identify(event, account) return { ok: true, account }})| Option | Type | What it does |
|---|---|---|
visitorId |
string |
The visitor to link, when the request is not theirs, as in a webhook. The request’s rc_vid cookie by default. |
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. It runs in the browser, in a component: in the server’s render it returns the link unchanged, so set the link once the component is mounted, or in its click handler.
routingcatProperties
PostHog’s before_send hook, for your app’s PostHog init in the browser: it puts the visitor id and the page’s variants on every PostHog event as it leaves, a page view’s first $pageview included. From the example app:
// PostHog, installed as its guide for Nuxt has an app write it: posthog-js, initialized in a client// plugin with the project's key and host from the runtime config, its client provided as `$posthog`.// RoutingCat's tracker takes `$posthog` first, and registers the visitor id and the live variants// with it as super properties at each page view's render beacon. PostHog captures a page view's// $pageview before that, so one line of `init` is RoutingCat's: `before_send`, PostHog's hook on// each event as it leaves, takes routingcatProperties, which puts them on the event.import posthog from 'posthog-js'import { routingcatProperties } from 'routingcat/vue'
export default defineNuxtPlugin(() => { const { posthogKey, posthogHost } = useRuntimeConfig().public const client = posthog.init(posthogKey, { api_host: posthogHost, defaults: '2026-08-30', before_send: routingcatProperties, }) return { provide: { posthog: () => client } }})config(), page(), slot() and variant()
The declarations: see Config and declarations.
Also exported
DeclarationError, what the declarations throw for a declaration that breaks a rule.- The types
ServerEvent, of a server route’sevent, andTrackOptionsandIdentifyOptions, of the calls’ options. - The types
RoutingCatSlotProps,RoutingCatSlotScope,RoutingCatState,Shown,ConfigDeclaration,PageDeclaration,SlotDeclarationandVariantDeclaration.
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 Nuxt site, the file `init` is to write (integrations/a_architecture// §4.4): RoutingCat decides every page before Vercel's cache, on Node.js, with the build, which// `nuxt build` writes to .routingcat/ before Vercel builds this file.import { middleware, NUXT_DESCRIPTOR } from 'routingcat/vercel'import build from './.routingcat/values.mjs'
export const config = { runtime: 'nodejs', matcher: ['/((?!_nuxt/|_ipx/|_rc/|api/|.*\\.[a-z0-9]+$).*)', '/_rc/:path*'], // RoutingCat: Nuxt's pages, and /_rc}
export default middleware(null, { framework: NUXT_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. |
Also exported
NUXT_DESCRIPTOR, the framework that renders a Nuxt site’s pages.- 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.
On Workers, it stands in front of Nitro’s own Worker, which Nitro’s cloudflare_module preset builds: worker(nitro, { mode: 'wrap', … }). On Cloudflare Pages, your build wraps Nitro’s Worker itself, with no file of yours. From the example app:
// The Worker's entry on Cloudflare Workers for a Nuxt site built with Nitro's `cloudflare_module`// preset, the file `init` writes (integrations/a_architecture §4.5): RoutingCat decides each// page, then runs Nitro's Worker, which renders it, with the build and the snapshot compiled from// its push, which `nuxt build` writes to .routingcat/ before Wrangler bundles this.import { NUXT_DESCRIPTOR, worker } from 'routingcat/cloudflare'import nitro from './.output/server/index.mjs'import build from './.routingcat/values.mjs'
export default worker(nitro, { mode: 'wrap', framework: NUXT_DESCRIPTOR, // The same literal as run_worker_first in the Wrangler configuration: Nuxt's page test leaves out // the payloads and API routes it runs first on. matcher: ['/*', '!/_nuxt/*'], build,})| Option | Type | Default | What it does |
|---|---|---|---|
mode |
'wrap' |
Required | In front of Nitro’s own Worker, the first argument. |
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: 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, which those patterns can’t say. |
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. |
Also exported
NUXT_DESCRIPTOR, the framework that renders a Nuxt site’s pages.- The types
WorkerOptions,CloudflareOptions,CloudflareWorker,WorkerContext,RoutingCatBuildandFrameworkDescriptor.
routingcat/browser
For a frontend that is not your Nuxt site, such as an app on another domain: it sends events with the site’s public key, through your Nuxt site’s /_rc path.
Its calls are its routingcat object’s, import { routingcat } from 'routingcat/browser', the same object its script tag puts on the page’s window.routingcat.
routingcat.init()
routingcat.init(key: string, options?: InitOptions | null): voidSets the site’s public key, from routingcat.config.ts, and where to send. Calls before it send nothing.
| Option | Type | What it does |
|---|---|---|
env |
'production' | 'preview' |
production, by default, or preview. |
collector |
string |
The site whose /_rc path to send through, such as https://lumen.so for an app on another domain. The page’s own by default. |
routingcat.track() and routingcat.identify()
routingcat.track(name: string, options?: TrackOptions): Promise<void>routingcat.identify(accountId: string, options?: IdentifyOptions): Promise<void>As your own app’s routingcat.track() and routingcat.identify() do, from the browser: an event’s id is generated where you give none, and its time is when it arrives. An option set to null reads as 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 entry also exports the types InitOptions, TrackOptions, IdentifyOptions and LineItem.
routingcatProperties
PostHog’s before_send hook, as routingcat/vue’s: in your PostHog init, before_send: routingcatProperties puts the visitor id and the page’s variants on every PostHog event.
The script tag
The same entry as a script, 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, the site’s public key; data-env, preview for a preview build; and data-collector, as routingcat.init’s collector. It puts its calls on the page’s window.routingcat: routingcat.track, routingcat.identify, routingcat.link, routingcat.init and routingcat.routingcatProperties. Or copy node_modules/routingcat/dist/browser.global.js into your site and load it from there.