Skip to content

JavaScript API

The routingcat package, which npx routingcat init installs, gives your Nuxt site:

  • routingcat/nuxt, the module in nuxt.config;
  • routingcat/vue, the component and the composable your pages use, routingcat, whose calls report conversions from your server routes and your pages, routingcatProperties for your PostHog init, and the declarations;
  • routingcat/vercel or routingcat/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:

nuxt.config.ts
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> and useRoutingCat() available in every component;
  • at each nuxt build, registers what routingcat/ 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 routingcat available in your server code, its calls taking the request’s event first;
  • 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:

app/pages/index.vue
<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:

app/pages/pricing.vue
<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:

server/api/waitlist.post.ts
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:

server/api/signup.post.ts
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(url: string | URL): string

A 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:

app/plugins/posthog.client.ts
// 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’s event, and TrackOptions and IdentifyOptions, of the calls’ options.
  • The types RoutingCatSlotProps, RoutingCatSlotScope, RoutingCatState, Shown, ConfigDeclaration, PageDeclaration, SlotDeclaration and VariantDeclaration.

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:

middleware.ts
// 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, RoutingCatBuild and FrameworkDescriptor.

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:

worker.ts
// 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, RoutingCatBuild and FrameworkDescriptor.

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): void

Sets 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(url: string | URL): string

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