Blocking
Routing and layout
Routing is React Router v8 in framework mode: you list routes in one config file, each route is a module in app/routes/, and the Vite plugin generates types for each one.
The route config
Every route is registered in app/routes.ts. The template starts with one:
import { type RouteConfig, index } from "@react-router/dev/routes";
export default [index("routes/home.tsx")] satisfies RouteConfig;There's no file-system routing: a file in app/routes/ does nothing until it's listed here.
Adding a page
-
Create the module, for example
app/routes/about.tsx:export default function About() { return ( <> <title>About</title> <meta name="description" content="Who we are." /> <h1>About</h1> </> ); }React moves the
<title>and<meta>into<head>, during server rendering too. Head tags has the details. -
Register it in
app/routes.ts:import { type RouteConfig, index, route } from "@react-router/dev/routes"; export default [ index("routes/home.tsx"), route("about", "routes/about.tsx"), ] satisfies RouteConfig; -
Link to it with
<Link to="/about">. shadcn components that render links take arenderprop rather thanasChild:<NavigationMenuLink render={<Link to="/about" />}>About</NavigationMenuLink>
A route that exports a loader, action or meta types it with import type { Route } from "./+types/about". That import is generated: if your editor can't find it, run pnpm typecheck, which runs react-router typegen first.
Layouts
A layout route renders shared UI around its children through <Outlet />, without adding a URL segment. This site's docs pages work this way:
layout("routes/docs/layout.tsx", [
route("docs/call-sheet", "routes/docs/call-sheet.tsx"),
route("docs/blocking", "routes/docs/blocking.tsx"),
]);app/root.tsx is the outermost layout. It holds the document shell (<Meta />, <Links />, the dark-mode script) and the app-wide error boundary.
Head tags
Head tags come in two kinds, set in two places:
- Site-wide tags (site name, social image, card type and the canonical link) are rendered by
app/root.tsxfromapp/lib/site.ts. Every page gets them. - Page tags (title and description) go in the route component as React
<title>and<meta>elements, like the About page above.
app/lib/site.ts holds facts about the site and nothing else:
export const site = {
name: "That's your cue",
description: "Places, everyone.",
url: "", // full public address, subpath included
image: "", // social card image in public/, ideally 1200x630
};urlis where the site will be public, such ashttps://you.github.io/repo, neverlocalhost. Its path setsbasenameand Vite'sbase, so a subpath is configured in one place.- While
urlis empty, the site serves from/and leaves outog:image,og:urland the canonical link, because they need an absolute address.pnpm buildprints a note. Dev,pnpm startand Docker work the same either way. - Why not the
metaexport? A child route'smetareplaces its parent's, so site-wide tags set there would vanish from any page that sets its own. Since React 19, React Router recommends the elements instead. Keepmetafor tags built fromloaderData.
Rendering modes
The same app builds three ways. Only react-router.config.ts changes.
Pre-rendering
import type { Config } from "@react-router/dev/config";
export default {
ssr: false,
prerender: true, // every static path in routes.ts, or a list: ["/", "/about"]
} satisfies Config;Loaders on pre-rendered routes run once, at build time, and their data is written next to the HTML. This site's Props page uses that to syntax-highlight component source without sending a highlighter to the browser. Actions aren't allowed without a server.
Deploying a pre-rendered build under a subpath, such as a GitHub Pages project site, takes one extra step. Wrap covers it.