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, how a Nuxt page carries its variants, 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. This landing page varies its headline and the line under it, as two slots:
import { page, slot, variant } from 'routingcat/vue'
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 use the notation of Nuxt’s page files:
/pricing, or/blog/[slug]forapp/pages/blog/[slug].vue. 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 render: 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, which your page’s template renders. What differs between ideas goes in their fields; how a slot shows them stays in the template, which every variant of the slot shares.
RoutingCat keeps a version of what you declare, a variant’s fields:
- an edit to the page’s template makes no version, since it changes every variant of the slot alike;
- 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 each slot
Every routed page renders its slots the same way, whether Nuxt prerenders it, regenerates it or renders it at each request: with <RoutingCatSlot>, which RoutingCat’s module makes available in every component.
<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>:pagetakes the page’s declaration, which the page imports fromroutingcat/, andslotnames one of its slots.v-slot="{ fields }"renders a variant from its fields, typed from the declaration. The same template renders every variant of the slot;variant, besidefields, gives each one’s key.- The classes and ids you give
<RoutingCatSlot>go on the element that holds the slot. Give it nostyle, and style your own elements inside it instead: the slot’s element keeps its own style, which a Content Security Policy can allow by its hash.
Know how a Nuxt page carries its variants
<RoutingCatSlot> renders every variant of its slot into the page: the default shown, the others hidden. Every visitor gets the same HTML, so Vue’s hydration always matches, and Nuxt can prerender, regenerate or cache the page as usual.
- Before the page paints, the head snippet, a small script RoutingCat’s module puts early in every page’s
<head>, shows the variants RoutingCat picked for that visitor, from your response’sServer-Timingheader. - When a visitor follows a link to another routed page, RoutingCat’s module asks for that page’s variants beside Nuxt’s own payload fetch, and shows them as the page renders. Run it in production gives what that costs.
- Wherever RoutingCat doesn’t pick, as in development without
?rc=, the page shows its defaults.
Render a slot inside a sentence
In a paragraph, or anywhere else only inline content can go, give the slot tag="span": its elements are then spans. A div would close the open <p>, and Vue’s hydration would find the markup moved. This pricing page varies its lead line, and the trial’s length inside a sentence:
import { page, slot, variant } from 'routingcat/vue'
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 } }), }, }), },})<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>Know which variant shows
useRoutingCat(page), which the module also makes available everywhere, tells code which variant each slot of a page shows: for code that must know, such as an analytics call or a widget initialized when shown, and never to render a slot, which <RoutingCatSlot> renders whole. It returns two refs:
shown: through the server’s render and the hydration, the slots’ defaults, so a render that reads it matches the server’s; from mount on, what the page shows, following it as an in-page navigation shows other variants;applied:falsewhileshownholds the defaults in place of what the page shows,truefrom mount on, so that your code tells a visitor shown the defaults from one whose variants aren’t read yet.
So a template that reads them renders again after mount, as the pricing page’s last line does, shown once applied is true.
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. - Render each slot on its own page’s routes. RoutingCat decides one page per request, so a slot of another page, such as one in a layout every page shares, isn’t routed there.
- Write variants that can sit hidden in the page, since every variant is in it. Inside a variant:
- no
idthat appears elsewhere on the page; - at most one
h1, and only in a slot that holds the page’s heading; - forms and widgets only if they work when shown later, and do nothing while hidden;
- nothing that differs by visitor, such as a name, a cart or a sign-in state. Content that differs by item, such as a product’s title, is fine;
- no script that runs, no
application/ld+json, no<style>and no stylesheet link: they act on the whole page, hidden or not.
- no
- Give the images of every variant of a slot the same
loading, so that no variant loads faster than another. Withloading="lazy", hidden variants’ images aren’t fetched; eager ones are fetched for every variant, shown or not. - Put each slot where the browser leaves it in place: inside a table cell or a list item, or around the whole table or list, never directly inside a table, a list or a
select; and withtag="span"among inline content. Slots don’t nest, and<head>content can’t be a slot. - Leave the slot’s own elements unstyled. A rule such as
.hero > div { display: flex }reaches them, and can show hidden variants.