Build a page
A routed page needs a declaration in routingcat/, which says what can vary, and a page that renders each slot with <RoutingCatSlot>. This guide covers both, for prerendered pages and pages rendered at each request, and the rules that keep what visitors see measurable. To choose a page’s shape first, see Pages, variants and versions.
Declare the page
Each page is one file in routingcat/ that exports one page(): its key, its routes and its slots. Each slot() names its default and its variants, and each variant() holds what it shows. All three, and config(), come from routingcat/astro. This landing page varies its headline and the line under it, as two slots:
import { page, slot, variant } from 'routingcat/astro'
export const home = page('home', { routes: ['/'], slots: { hero: slot({ default: 'current', variants: { current: variant({ fields: { headline: 'Route every visitor to what converts' } }), learns: variant({ fields: { headline: 'Your pages learn what converts each visitor' } }), shows: variant({ fields: { headline: 'Show each visitor the page that converts them' } }), }, }), subhead: slot({ default: 'current', variants: { current: variant({ fields: { text: 'Each visitor sees the version of the page most likely to convert them, and you see which version works for whom.', }, }), short: variant({ fields: { text: 'Every visitor gets the page that works for them. You see what works.' }, }), }, }), },})- Keys are lowercase letters, digits,
-and_, up to 40 characters. Addresses are built from them:home,home/hero,home/hero:learns. - Routes are the paths of your pages, with a dynamic segment in brackets, as Astro’s file names write it:
/pricing,/blog/[slug]. A path belongs to one page. - The default is what your site shows today, kept under its own key. The control always sees it.
- Slots vary independently, so every combination must read well: here 3 headlines and 2 lines make 6 combinations.
- A page can have its own objective, with
objectivein itspage(), in the same forms as the site’s inroutingcat.config.ts. - Limits: at most 20 slots and 100 live variants per page. See Limits and defaults. On Hobby, your sites serve at most 10 live variants together, the defaults not counted, and hold the rest.
A variant is made of fields: plain values, such as text or numbers, that the page renders. <RoutingCatSlot> renders variants made of fields, and a variant declared as a component renders nothing, with a warning in development.
RoutingCat keeps a version of what you declare: a variant’s fields, with the images they reference. An image imported from your repository is kept by its bytes; one given by URL, by its URL. Content fetched at run time isn’t captured, so its changes leave no version. Pages, variants and versions says what makes a new version.
Render a prerendered page
A page renders each slot in a <RoutingCatSlot>, from routingcat/astro/components: page takes the page’s declaration, name the slot’s key, and its child is a function that receives the variant’s fields, then its key, and returns what to show. It takes no other prop than tag, below, so classes and styles go on the elements inside it. The landing page, src/pages/index.astro, is prerendered:
---// `/`, prerendered: every variant in the page, the head snippet showing the host's selection.import { RoutingCatSlot } from 'routingcat/astro/components'import { home } from '../../routingcat/home'import Base from '../layouts/Base.astro'---<Base title="RoutingCat"> <main> <RoutingCatSlot page={home} name="hero"> {(fields: { headline: string }) => <h1>{fields.headline}</h1>} </RoutingCatSlot> <RoutingCatSlot page={home} name="subhead"> {(fields: { text: string }) => <p>{fields.text}</p>} </RoutingCatSlot> <a href="/pricing">See pricing</a> </main></Base>A prerendered page is the same HTML for every visitor, so it carries every variant: in each slot, the default shows and the others are hidden. RoutingCat picks the visitor’s variants before your host serves the page, and <RoutingCat />, in the layout’s <head>, shows them before the page paints. So the page stays one file that your host can cache, and each visitor still sees their own variants.
- RoutingCat follows Astro’s own reading of whether a page is prerendered: the page’s
export const prerender, else your config’soutput. - Without RoutingCat’s middleware or Worker, or in a browser too old to show the visitor’s variants, the page shows its defaults.
- Every variant is in the page’s HTML, so each adds its markup to the page’s weight, and its content must follow the rules below.
Render a page at each request
A page rendered at each request takes the same component. Its declaration and its page, src/pages/pricing.astro:
import { page, slot, variant } from 'routingcat/astro'
export const pricing = page('pricing', { routes: ['/pricing'], slots: { lead: slot({ default: 'growth', variants: { growth: variant({ fields: { lead: 'Free while you grow, then by billed visitors' } }), start: variant({ fields: { lead: 'Start free; pay only for the visitors you bill' } }), }, }), trial: slot({ default: 'days14', variants: { days14: variant({ fields: { days: 14 } }), days30: variant({ fields: { days: 30 } }), }, }), },})---// `/pricing`, rendered at each request: the variants the host's decision selects, alone.import { RoutingCatSlot } from 'routingcat/astro/components'import { pricing } from '../../routingcat/pricing'import Base from '../layouts/Base.astro'
export const prerender = false---<Base title="Pricing"> <main> <h1>Pricing</h1> <RoutingCatSlot page={pricing} name="lead"> {(fields: { lead: string }) => <p class="lead">{fields.lead}</p>} </RoutingCatSlot> <p> Start your <RoutingCatSlot page={pricing} name="trial" tag="span"> {(fields: { days: number }) => `${fields.days}-day`} </RoutingCatSlot> trial. </p> </main></Base>RoutingCat picks the visitor’s variants before Astro renders the page, and <RoutingCatSlot> renders those alone.
tag="span"renders a slot inside a sentence, astrialis here, since adivwould close the open<p>.- A browser too old to show a selection, Safari before 16.4, Chrome before 105 or Firefox before 121, gets the defaults here too, since it couldn’t show the visitor’s variant in a server island.
- Its HTML is one visitor’s, so RoutingCat’s integration keeps it out of shared caches: it takes
s-maxageand the CDN cache headers off the response, and turns Astro’s route cache off for that render. - On Vercel, a page rendered on demand but kept for every visitor, by the adapter’s
isror by a route rule of Astro’s route cache, carries every variant instead, as a prerendered page does, since every visitor gets the same render. The build reads which pages those are. - Server code that must know which variant shows, such as an analytics call, reads
Astro.locals.routingcat.shown(pricing): each slot’s variant, by slot key, typed from the page’s declaration. It is null on a prerendered page, or one a cache serves every visitor, where the head snippet shows each visitor theirs in the browser.
Keep <RoutingCat /> in the head
Every page renders through a layout whose <head> holds <RoutingCat />, first after the charset and viewport tags, as init placed it in src/layouts/Base.astro:
---// …import { ClientRouter } from 'astro:transitions'import { RoutingCat } from 'routingcat/astro/components'
interface Props { title: string}
const { title } = Astro.props---<html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <RoutingCat /> <title>{title}</title> <ClientRouter /> … </head> <body> <header> <nav> <a href="/">Home</a> <a href="/pricing">Pricing</a> <a href="/about">About</a> </nav> </header> <slot /> </body></html>- It writes the head snippet inline, so that Astro neither bundles nor defers it: the snippet shows a prerendered page’s variants before the body is parsed. Then comes the tracker, which Astro bundles with your scripts, and which reports what each slot showed.
- A page whose layout lacks it sends no render beacon, and if it carries every variant, it shows its defaults to every visitor;
doctornames the page without the snippet. - With Astro’s client router,
<ClientRouter />, each next page is decided as the router fetches it, and its variants show as the router swaps it in.
Keep what visitors see measurable
- Render each slot through
<RoutingCatSlot>. It marks the slot in the page, so the browser can report what each visitor saw; content rendered outside it goes unreported. - Name a slot its page declares. A
slotthe page doesn’t declare renders nothing, with a warning in development. - Route a slot on its own page. RoutingCat decides one page per request, so a slot of another page, as in a layout every page shares, shows its default there.
- Put each slot where its markup is allowed:
tag="span"in a sentence, and never directly inside a table, a list or a<select>, which can’t hold it: put it inside the cell or the item, or around the whole list. Slots don’t nest, and<head>content can’t be a slot. - A slot in a server island shows the visitor’s variant late, on a page rendered at each request as on a prerendered one: the island renders after the page’s shell, in a request of its own, so its slot carries every variant, and the head snippet shows the visitor’s once the island arrives. Keep slots above the fold out of islands.
On a page that carries every variant, prerendered or kept for every visitor, the hidden variants are in the HTML too, and the browser still reads them:
- No
idthat appears elsewhere on the page, hidden variants included, so that ids stay unique. - One
h1per variant at most, and only in a slot that owns the page’s heading. - No script that runs, no stylesheet and no structured data in a variant: a hidden variant’s scripts still run, its styles apply to the whole page, and search engines read its structured data.
- Forms and widgets only where they work when shown later, since a hidden variant’s forms still submit.
- No per-visitor content, such as a name or a cart, since the page is the same for every visitor.
- The same
loadingon every variant’s images, so that no variant loads faster than another: an eager image in a hidden variant is fetched too. - No
displayrule on a slot’s own elements, such as.hero > div { display: flex }: it beats the browser’s rule forhiddenand shows every variant at once. Style the elements inside them.