Client-rendered apps
An app that renders in the browser, such as a React app built with Vite, works as any site RoutingCat decides in the page: its head partial picks each visitor’s variants, its templates carry the slots, and push registers them. This guide covers what differs: where the partial goes, the two ways a component renders a slot, and how each route change counts. Build a page has the rules every slot follows. The example is a React app built with Vite, with two pages, / and /pricing.
Set up the app
In the app’s folder, npx routingcat init recognizes an app built with Vite, by the index.html at its root, or with Create React App, by its public/index.html. It sets the app up as Set up RoutingCat shows, with these differences:
- Your templates are that
index.htmland the HTML files undersrc/, since a React component can’t carry the slot markuppushreads. In a Vue app, they are theindex.htmland the.vuefiles undersrc/. - The head partial,
routingcat-head.html, goes into theindex.html, which includes no file:initprints the edit. Paste the partial’s content first in its<head>, after the charset and viewport tags, and again each timepushchanges it, then commit both. Your build copies it into the page it writes. - The partial takes each route change as a view, as below.
The example app’s config, with its objective and the outcome it tracks beside it:
{ "$schema": "https://cdn.routingcat.com/schemas/1.0.3/config-1.json", "site": "routingcat-vite-example", "public_key": "pk_routingcat-vite-example_Rv7Kq3Wd", "integration": "script", "routingcat": "1.0.3", "environments": { "production": { "hosts": ["vite.example.com"] } }, "sources": ["index.html", "src/**/*.html"], "head": { "partial": "routingcat-head.html" }, "objective": "signup", "outcomes": ["purchase"], "pages": { "home": { "routes": ["/"], "url": "/" }, "pricing": { "routes": ["/pricing"], "url": "/pricing" } }}Write a slot with every variant
A slot written in the slot markup, every variant in it, needs no code of RoutingCat’s in your components. The example’s home page keeps its headline in src/slots/hero.html, as push stamped it:
<div data-rc-page="home" data-rc-slot="hero" style="display:contents"> <div data-rc-variant="current" data-rc-hash="58c2042d15ca41d3269bcc357d905fd4f509d74006e38b53a11ebb6edcf2af0d" style="display:contents"> <h1>Edit photos like a pro</h1> </div> <div data-rc-variant="oneclick" data-rc-hash="c4a2d99ee3bdf59575744430bd29c6ab8d80e4fbb8cb25f7ff7556218754ed67" hidden data-nosnippet> <h1>Pro-level photo edits in one click</h1> </div></div>The page renders it once, as written, from the file’s text, which Vite’s ?raw import gives:
import { SignUp } from './SignUp'import hero from './slots/hero.html?raw'
export function Home() { return ( <main> {/* biome-ignore lint/security/noDangerouslySetInnerHtml: the site's own markup, built in */} <div style={{ display: 'contents' }} dangerouslySetInnerHTML={{ __html: hero }} /> <p>Retouch, crop and color your photos in the browser.</p> <SignUp /> </main> )}RoutingCat’s style rules show the variant each view picked, and no re-render of yours undoes them. Every variant is in the page, so the rules that keep what visitors see measurable apply as written.
Render the picked variant alone
A component can instead render only the variant RoutingCat picked, as the example’s pricing page does for its plan. The variants are declared in src/slots/plan.html, which push stamps, and which renders nothing itself:
<div data-rc-page="pricing" data-rc-slot="plan" style="display:contents"> <div data-rc-variant="monthly" data-rc-hash="0907e445ef3c48244ce235e937ee59f5d93fc3adf75a8847580d892057f9a853" style="display:contents"> <p>Pro, $12 a month, cancel any time.</p> </div> <div data-rc-variant="annual" data-rc-hash="dfc750f35d56188d8d271696924bec69c38303b49fc6bc35f3ad85cd7201ec37" hidden data-nosnippet> <p>Pro, $96 a year: four months free.</p> </div></div>The component waits for the page’s variants, then renders the picked variant’s content in one element that names the page, the slot, the variant and its version’s hash:
import { useEffect, useState } from 'react'import declared from './slots/plan.html?raw'
const slot = document.createElement('template')slot.innerHTML = declared
/** A variant child of the slot's markup: the one decided, else the default, not hidden. */const variantOf = (key: string | null): Element => (key === null ? null : slot.content.querySelector(`[data-rc-variant="${key}"]`)) ?? (slot.content.querySelector('[data-rc-variant]:not([hidden])') as Element)
function Plan() { const [decided, setDecided] = useState(window.routingcat === undefined) useEffect(() => { let mounted = true window.routingcat?.ready('pricing').then(() => { if (mounted) setDecided(true) }) return () => { mounted = false } }, []) if (!decided) return null const variant = variantOf(window.routingcat?.variant('pricing', 'plan') ?? null) return ( <div style={{ display: 'contents' }} data-rc-page="pricing" data-rc-slot="plan" data-rc-variant={variant.getAttribute('data-rc-variant') ?? undefined} data-rc-hash={variant.getAttribute('data-rc-hash') ?? undefined} // biome-ignore lint/security/noDangerouslySetInnerHtml: the site's own markup, built in dangerouslySetInnerHTML={{ __html: variant.innerHTML }} /> )}window.routingcat.ready('pricing')resolves once RoutingCat has picked the page’s variants, or once its wait for routing data has ended and the page keeps its defaults.window.routingcat.variant('pricing', 'plan')then gives the key of the variant picked for the slot, ornullwhere there is none, as when the page keeps its defaults: the example then renders the default.- The element’s four attributes,
data-rc-page,data-rc-slot,data-rc-variantanddata-rc-hash, tell RoutingCat which variant and version the slot showed, which it reports for the view. - Until the page is decided, the component renders nothing. Where RoutingCat’s script doesn’t run,
window.routingcatis undefined, and the component renders the default. - Change the address before you render the next page, as the example’s router does below:
readyandvariantanswer for the view RoutingCat starts when the path changes.
The JavaScript API gives each call’s signature.
Each route change is a view
The partial init writes for your app takes each change of the address’s path as a new view of the page at that path, as a page load is: RoutingCat picks the page’s variants, keeping those the visitor already had on that page in this visit, records the view, and reports what its slots show. It follows the address through the browser’s history, so your router calls nothing of RoutingCat’s: window.routingcat.view(), the call it makes at each change of path, starts a view for the current address. A change of the query string or the # fragment alone stays in the same view.
The example’s router changes the address, then renders the next page:
const follow = (event: MouseEvent<HTMLAnchorElement>) => { event.preventDefault() window.history.pushState(null, '', event.currentTarget.href) setPath(window.location.pathname) }Track conversions
The page reports a conversion with window.routingcat.track(), as the example’s sign-up does:
const signUp = (event: FormEvent<HTMLFormElement>) => { event.preventDefault() void window.routingcat?.track('signup') setSignedUp(true) }The call never throws, and sends the visitor id itself. A conversion your server sees, such as an account it creates, is safer sent from there: Track conversions gives both ways.
Type window.routingcat
In TypeScript, routingcat/runtime gives the type of window.routingcat, with routingcat among your dev dependencies, at the version your config pins. The example declares it optional, since a page whose RoutingCat script doesn’t run has none:
import type { RoutingCatPage } from 'routingcat/runtime'
declare global { interface Window { routingcat?: RoutingCatPage }}Deploy
Your build copies the index.html, the partial in it, into the page it writes, so the app deploys as before. Register each production deploy with push --check before it and push right after, as on any site RoutingCat decides in the page: Change, preview and deploy gives the example’s GitHub Actions workflow. On your machine, Vite’s development server runs your pages in development mode, where ?rc= shows a variant, as See a variant in development describes.