Set up RoutingCat
This tutorial takes a site whose templates Vercel or Cloudflare serves from nothing to a working install, in four steps. Your agent does most of the work; you approve a sign-in, set your build command, choose what to track, deploy and add a card, with nothing charged. One real conversion of your own, recommended and your choice, confirms your tracking end to end. Your visitors see no change, because no page is routed until you declare one.
It follows Lumen, a photo-editing app whose marketing site is built with Hugo and deployed on Vercel. Where Cloudflare, plain HTML, a site your own server renders or Nuxt 2 differ, each step says so. What your site needs lists the requirements.
1. Install with init
In your site’s folder, you or your agent run:
npx routingcat initIt checks your project first, and stops before changing anything when something needs you:
- Your stack, by its templates: Hugo’s
layouts/, Jekyll’s layouts and includes, Eleventy’s Nunjucks, Liquid and HTML templates and includes, plain HTML pages, a Nuxt 2 app, or the views of Laravel, Rails, Django, a WordPress theme or PHP.initrecognizes plain HTML pages bypublic/index.html, or byindex.htmlat the root of the folder where you run it, and sets them up inpublic/: at the root, beside the project’s own files, it stops, since your host would publish that whole folder, the build’s credential included. Pages in any other folder, such aswww/, go intopublic/too, since from the project’s folderinitdoesn’t recognize them. A Nuxt 2 app builds static, withtarget: 'static'andnuxt generate, or init stops. - Your host, from the folder: a Vercel link or
vercel.json, or a Wrangler configuration for Cloudflare Workers or Pages. Where the folder names none or several,initasks at a terminal where the site deploys, or, for a site your own server renders, where it is served; run by an agent, it stops: run it again with the host.routingcat.config.jsonthen names it, andinitrun later with another host moves the site there, its pages, variants and results kept, and binds it to the new host’s project as setup does, in place of the old host, whose builds register it no more:- For Hugo, Jekyll, Eleventy or plain HTML:
--host vercel,--host cloudflare-workersor--host cloudflare-pages. A site on Workers or Pages moves to Vercel, or to any other host. - For Laravel, Rails, Django, WordPress or PHP:
--host vercel, or--host cloudflare-workersfor a Worker in front of your server. Pages serves static sites only. Moved off a Worker in front of your server, that Worker keeps deciding on its route until you delete it in Cloudflare, asinit’s closing note says. - On any other host:
--host nonesets the site up to decide in the page, which the docs for any other website cover.
- For Hugo, Jekyll, Eleventy or plain HTML:
- On Vercel, a folder linked to a Vercel project, and the Vercel CLI signed in: run
npx vercel linkornpx vercel login, theninitagain. - On Cloudflare, Wrangler signed in: run
npx wrangler login, theninitagain.
It signs you in. A browser page opens, where you sign in, or sign up, and allow the command on this machine. Where no browser can open, init prints the link, waits 50 seconds and exits with code 3: open the link, approve, and run init again, which continues with the same link.
It shows its plan, then applies it. At a terminal it asks whether to add a note to AGENTS.md, then whether to apply the plan; run by an agent, it asks nothing, applies the plan and prints what changed. --dry-run stops after the plan. On Lumen’s Hugo site, run by an agent and signed in already:
routingcat 1.0.0 · <project>
Checks ✓ Hugo: its templates layouts/**/*.html ✓ Vercel project lumen · Vercel CLI signed in as emma ✓ no middleware.ts yet: init writes one ✓ signed in to RoutingCat
Plan 1 create the site lumen, bound to Vercel project lumen 2 npm install routingcat@1.0.0 3 middleware.ts new: middleware() from routingcat/vercel on Node.js, and RoutingCat's matcher 4 .gitignore new: .routingcat/, where the build writes RoutingCat's credential 5 routingcat.config.json new: the site and its public key 6 layouts/partials/routingcat-head.html new: the head partial, as routingcat push --head-only writes it 7 layouts/_default/baseof.html include the head partial first in <head>: {{ partial "routingcat-head.html" . }} 8 the skill .agents/skills/routingcat/, copied to .claude/skills/routingcat/Nothing reaches visitors until you deploy.
✓ site lumen created (op_01m3yesth0fjfsctr17tbq1z82), bound to Vercel project lumen✓ routingcat 1.0.0 added · 2 files edited, 4 files and 2 folders created (see git diff)Ask the person: "Add a two-line note to AGENTS.md so coding agents always know RoutingCat is here? (recommended)" If yes, run npx routingcat init --agents-md.Dashboard: https://app.routingcat.com/lumenNext: in Vercel's project settings, run npx routingcat build after the site's build: hugo --gc --minify && npx routingcat build; read .agents/skills/routingcat/setup.md and follow itThe site is named after your host’s project and bound to it. It already shows in the dashboard, at the address init prints: Lumen’s is https://app.routingcat.com/lumen. Your plan covers it: Hobby, at no cost, holds 3 sites, and init refuses a 4th with the way out. Each edit has a purpose:
| File | What init does |
So that |
|---|---|---|
package.json |
adds routingcat, at init’s own version |
one package carries the command, the code and the skill |
middleware.ts |
creates RoutingCat’s middleware, on Node.js, with its matcher | Vercel runs it before every page, so each visitor’s variants are picked before the page is served |
.gitignore |
adds .routingcat/ |
the build writes RoutingCat’s credential there, which stays out of your repository |
routingcat.config.json |
creates it, with your site, its public key, your templates and the head partial’s path | the key names your site; it is public and can only write |
layouts/partials/routingcat-head.html |
creates the head partial: RoutingCat’s head snippet, then the tracker’s script tag | the snippet shows each visitor’s variants before the page paints, and the tracker reports what each page showed |
layouts/_default/baseof.html |
includes the partial first in <head>, after the charset and viewport tags |
every page carries it |
.agents/skills/routingcat/, .claude/skills/routingcat/ |
installs the skill | your agent knows the commands and the words |
AGENTS.md, and CLAUDE.md if you have one |
with your agreement, adds a two-line note | every coding agent knows RoutingCat is here, and reads the skill before changing its code |
Every edit shows in git diff.
On Cloudflare, init writes RoutingCat’s Worker in place of middleware.ts:
- on Workers,
worker.tsin front of your site’s files, and inwrangler.jsoncitsmain,run_worker_firstand the version metadata bindingCF_VERSION_METADATA; - on Pages,
functions/_middleware.ts, in front of your site’s files; - in front of your own server,
worker.tson your production address’s route, aswww.lumen.com/*, with Wrangler’s build commandnpx routingcat buildinwrangler.jsonc.
On Workers or Pages, init also puts routingcat build before Wrangler in your scripts that serve the site with it, wrangler dev or wrangler pages dev, unless something before it in the script runs it already, so that RoutingCat’s Worker or middleware finds on your machine what it imports. Here on a Jekyll site’s preview script:
6 package.json run routingcat build before wrangler dev in scripts.preview: bundle exec jekyll build && routingcat build && wrangler devA script that runs Wrangler another way gets the edit printed for you to make. In front of your own server, Wrangler’s build command runs it.
init then closes with two notes, here on Workers:
· RoutingCat needs Workers Paid: on the Free plan, past 100,000 requests a day, every page the Worker runs first on answers 429 until midnight UTC.· RoutingCat leaves the variants' image preload off, which Cloudflare's Early Hints would hint from one visitor to the next: once Early Hints is off for your routed paths, set imagePreload: true in worker.ts.Cloudflare gives these settings and the zone’s others.
On Cloudflare, routingcat.config.json lists your production address, under environments. Your builds tell RoutingCat the address from it, and doctor and preview links open your pages there. init takes it from --origin, your Wrangler routes and custom domains, or your stack’s configuration, such as baseURL in hugo.toml or APP_URL in .env, and prints a ✓ production address line when it finds one. Where it finds none, as is usual on Pages, it asks for it at a terminal, or stops: run it again with your address, as npx routingcat init --origin https://www.lumen.com. A Worker in front of your own server also runs on that address’s route.
Each stack carries the head partial its own way:
- A layout that includes files, in Hugo, Jekyll, Eleventy, Blade, ERB, Django’s templates, WordPress or PHP, includes it with one line in the stack’s language, such as
@include('routingcat-head')in Blade.initadds the line where exactly one template holds<head>, and otherwise prints it for your agent to place. - Plain HTML includes no file, so each page carries the partial’s content itself: paste it first in each page’s
<head>, after the charset and viewport tags, and again whenevernpx routingcat pushchanges it. - Nuxt 2 writes every page’s head from
nuxt.config.js.initadds RoutingCat’s two entries first inhead.scriptthere, and names the snippet’shidin__dangerouslyDisableSanitizersByTagID, so that vue-meta writes the snippet as it is.
On Nuxt 2, add RoutingCat’s router hook, which init doesn’t write: a <NuxtLink> renders the next page in the browser, out of the middleware’s sight, and the hook decides that navigation. Build a page gives the plugin and where it goes.
Laravel, Django and WordPress keep their admin pages undecided, because staff are not visitors: Laravel’s /admin and /nova and the pages under them, Django’s pages under /admin/, and WordPress’s under /wp-admin/, with /wp-login.php. They are left out of the middleware’s matcher on Vercel, and out of the Worker’s patterns on Cloudflare.
On Laravel, init leaves rc_vid out of the cookie encryption, with $middleware->encryptCookies(except: ['rc_vid']); in bootstrap/app.php, or before Laravel 11, 'rc_vid' in $except in app/Http/Middleware/EncryptCookies.php: Laravel empties a cookie it didn’t encrypt, and your own code reads the visitor id from the one RoutingCat’s middleware or Worker sets.
The note in AGENTS.md is your choice, and recommended. Coding agents read AGENTS.md in every session, but may open a skill only when a request matches it; the note tells them RoutingCat is here and to read the skill first. At a terminal, init asks, yes by default. Run by your agent, it leaves the choice to you: your agent asks, then runs npx routingcat init --agents-md if you agree. See Add the note to AGENTS.md.
Run init again at any time. It marks with ✓ what is done and does only what is missing or out of date, leaving your own edits as they are:
- RoutingCat’s entries in Nuxt 2’s
nuxt.config.js, where they differ from this version’s; - on Cloudflare, your production address, in a
routingcat.config.jsonthat lacks it; - a
public_keythat isn’t your site’s public key; - the skill, unless you edited it, and the note in
AGENTS.mdonce you have it, without asking again.
2. Set your build to register each deploy
Each deploy registers what it changed through npx routingcat build, which runs after your site’s own build: it reads your templates, registers them, and writes what RoutingCat’s middleware or Worker needs, in .routingcat/.
-
On Vercel, set your project’s build command in Vercel’s project settings, as
init’s last line says: your site’s build, thennpx routingcat build, ashugo --gc --minify && npx routingcat build, ornuxt generate && npx routingcat buildfor Nuxt 2. Each build proves itself with Vercel’s own token, so there is no secret to set, a site moved to Vercel included, whichinitbinds to the Vercel project. -
On Cloudflare Workers or Pages, set the same build command in your project’s build settings. Then the site’s owner mints the deploy key,
npx routingcat keys deploy --branch main, naming your production branch, and you set it asROUTINGCAT_DEPLOY_KEYin Cloudflare’s build variables, since Cloudflare’s builds carry no token of their own for RoutingCat. The command prints the key once:$ routingcat keys deploy --branch maindeploy key · production · branch main · minted now (op_01m4zbfqr0fwqttp9y5atppz0f)dk_qEi3Pi0oOEMbi6gb9l2uUsWQPF2A7H_OEIrNnn1cO3iCUcOn3YG4MgeQpxfKf4c-v6BzUec_KQP9ml31_8pHZOJqlNX_OP9EtLs1tZs_OrPB1YozQ4JQhqFMeKhYKYge17RdO_0ANyBicK6Set it as ROUTINGCAT_DEPLOY_KEY in Cloudflare's build variables. It is shown only when minted or rotated.It proves production only for builds of main, as your CI reports the branch. -
In front of your own server, the Worker builds with Wrangler’s build command,
npx routingcat build, whichinitset inwrangler.jsonc, and takes the deploy key the same way. Only that build registers your templates: each change to them needs the Worker built and deployed, beside your server’s own deploy, which registers nothing. Until both serve the same templates, a slot whose markup changed shows its default and records no decision, anddoctornames it.
Where vercel.json’s buildCommand holds your build command, init adds routingcat build there itself, and so it does in package.json’s build script for Eleventy, plain HTML and a site your server renders; its plan says so. Hugo’s, Jekyll’s and Nuxt 2’s builds are the generator’s own, run by your host, so where vercel.json doesn’t hold their command, init names the step in its closing lines. Cloudflare says which builds the deploy key proves.
To run the site on your machine, build it first: RoutingCat’s Worker or middleware runs from what routingcat build writes to .routingcat/, so run npx routingcat build after your site’s generator and before wrangler dev on Workers, wrangler pages dev on Pages or vercel dev on Vercel, as hugo && npx routingcat build && wrangler dev. On Workers or Pages, a script init edited does it for you, as the Jekyll site’s npm run preview. This build registers nothing, and its pages run in development mode, where ?rc=hero:learns shows that variant and nothing is recorded: see See a variant in development. In production, preview shows a variant. To deploy production to Cloudflare from your machine instead, with Wrangler, run routingcat build with ROUTINGCAT_ENV=production: it registers under your sign-in once you confirm its plan, as Deploy from your machine describes.
3. Choose what to track
RoutingCat counts the conversions your code reports. Your agent reads your code, and asks you two questions:
- The main conversion, pick one. This is the objective: what routing learns to get more of. It counts at most once per visit, when it happens within its window after the visitor first saw the page, 24 hours by default.
- Also track, pick any. These outcomes are counted and reported beside it, and never steer routing.
Answer in your own words when the list misses something. A main conversion can be several events where any one counts, such as any plan start; it still counts once per visit.
For a SaaS with trials, choose sign-up or trial start as the main conversion, and track purchase beside it. A payment at a trial’s end comes days or weeks after the visit: the default 24-hour window misses it, and a longer window, up to the visit’s length, delays learning by as much. See Conversions and visits.
Lumen chooses sign-up, and tracks its newsletter beside it.
Your agent wires each choice where it happens:
- From your server, or any other backend, through the HTTP events API, with your server events key: at sign-up and at every sign-in, a link from the visitor to your own account id, so that a conversion on another device still counts; then the conversion itself, with an
id, because providers deliver some events twice. - From a page with no backend, such as a form a third party handles, through the browser entry’s script tag, with
routingcat.identify()androutingcat.track().
The recipes are in Track conversions, with Clerk and other auth providers and PostHog on their own pages.
Then it declares them in routingcat.config.json, beside what init wrote: the main conversion as "objective": "signup", and the newsletter as "outcomes": ["newsletter"].
- Events under names you haven’t declared are recorded, but not analyzed until you declare them.
- Declare no page yet: a page comes with your first idea.
4. Deploy and verify
Commit and deploy to production the usual way, or let your agent do it where your repository’s rules allow. The build log shows RoutingCat’s receipt, which names the objective and outcomes your config declares while no page is routed yet, here on Vercel:
$ routingcat buildroutingcat registered ch_01m3yfrpt0fbttmb64x6008p1n · objective signup · outcomes newsletter · no page routed yetOn Cloudflare, a line starting with routingcat: follows the receipt, and says which routing data the Worker carries. When such a line takes the receipt’s place, this build registered nothing: visitors get your defaults, and the deploy ships as usual. The line says what to fix where the build runs. In Vercel’s build, without OIDC federation:
routingcat: no Vercel or GitHub Actions OIDC token and no ROUTINGCAT_DEPLOY_KEY, so this build cannot identify itself to RoutingCat: it registers nothing, and visitors get your defaults. On Vercel, turn on Settings → Security → Secure backend access with OIDC federation, then redeploy.In Cloudflare’s builds, without the deploy key:
routingcat: no Vercel or GitHub Actions OIDC token and no ROUTINGCAT_DEPLOY_KEY, so this build cannot identify itself to RoutingCat: it registers nothing, and visitors get your defaults. Run npx routingcat keys deploy --branch <your production branch>, set what it prints as the build variable ROUTINGCAT_DEPLOY_KEY in Cloudflare's build settings, then redeploy.Run doctor and wait. Once the deploy is live:
npx routingcat doctor --waitIt requests your production page, so the new deployment starts, then waits up to 5 minutes, printing each check as it passes: the build and the deployment registered, RoutingCat’s middleware or Worker on /, /_rc reaching RoutingCat, the head snippet on your pages, your pages’ Content Security Policy, the first arrival. It stops early when only a person can move the rest: the main conversion’s first event and, on Hobby, the card on file.
Make one real conversion, recommended. It is the only end-to-end proof that a conversion reaches RoutingCat and joins its visit. Lumen’s is a sign-up, so you sign up once on the production site, with a second email address. This sign-up is recorded as a real one, credited to no variant until a page is routed, and shows within a minute. Where converting costs money, as an order does, you may prefer to skip it: the main conversion’s check then passes with your site’s first real conversion, and until then doctor waits on it, with exit code 3, which is expected. Nothing else waits on it.
Add a card to start routing. On Hobby, routing waits for a card on file, and nothing is charged: doctor shows card on file waiting, with the link. You add it in the dashboard’s Settings → Billing, from the account menu, or start Pro’s 14-day trial instead; one card serves every site you own. Routing starts within about 2 minutes of it, with no new deploy. Until then, a deploy that routes a page logs routingcat NOT LIVE routing waits for a card on file: add one at …, nothing charged, and visitors see your defaults. See Add a card to start routing.
Then run npx routingcat doctor again. Every check passes, and it exits with 0, here on Vercel:
$ routingcat doctorlumen · production ✓ build registered ch_01m3yfrpt0ewpateq9yrq61xkb · objective signup · outcomes newsletter 14:19:52 ✓ deployment registered dp_01m3yfn1m0exhr06xh9d10mvbr · commit 2528229 · routingcat 1.0.0 14:21:10 ✓ runtime Node.js 22.18.0, as dp_01m3yfn1m0exhr06xh9d10mvbr reports it ✓ middleware on / rc_vid on lumen.com, 400 days 14:40:00 ✓ /_rc reaches the collector 14:40:00 ✓ head snippet 1 page carries it ✓ policy lets RoutingCat run, apply and record ✓ records ingest.routingcat.com accepts them from https://www.lumen.com ✓ markup 1 slot as declared ✓ first arrival arrivals in the pulse, last 14:39:18 14:39:18 ✓ signup 2 events 14:33:59 · server · identify 14:32:05 14:33:59 ✓ card on file Visa •••• 4242 · AGENTS.md has no RoutingCat note, so coding agents may skip the skill unless a request matches it; if the person wants the note, run npx routingcat init --agents-md12 of 12 passed (exit 0)On Workers, the Worker’s own checks take the place of runtime and middleware on /:
✓ Worker version b2c3d4e5-0000-4000-8000-000000000001, which dp_01m3yfn1m0exhr06xh9d10mvbr's isolates register under ✓ Worker on / rc_vid on lumen.com, 400 days 14:40:00Troubleshooting reads each check.
| Exit code | Means |
|---|---|
| 0 | Every check passed. |
| 1 | A check failed. Its line gives the message and its id; Troubleshooting has the fix. |
| 3 | A check is still waiting, usually for that first conversion or the card on file. |
| 4 | A request couldn’t get through from here, as in a sandbox without network access. |
Until a page is routed, doctor and status note that conversions are recorded but not yet credited to a variant. That is expected.
Close the setup. When your agent ran it, it ends by telling you, in plain words:
- that RoutingCat works, naming the conversion that came through; without one, that the main conversion’s check passes with your site’s first real conversion, and that nothing else waits on it;
- that your visitors see no change until a page is routed;
- what RoutingCat now records, and which conversion routing will learn to get more of, which you can change by asking;
- how it goes from here: you say what you’d like to try, at any level of detail, ask for ideas, or hand your agent the whole program;
- that 10% of visitors keep seeing your current site, which is how the gain is measured;
- your dashboard’s address: Lumen’s is
https://app.routingcat.com/lumen, which Read the dashboard walks through.