Routing
One file, routes.tsx, maps paths to components. Routes nest: a route with children is a layout.
Map paths to components
defineRoutes checks the tree and returns it. These are the pokedex example's routes:
import { defineRoutes } from '@brust/core/routes'
import AppLayout from './components/AppLayout'
import BrowsePage from './pages/BrowsePage'
import DetailPage from './pages/DetailPage'
import HomePage from './pages/HomePage'
import NotFoundPage from './pages/NotFoundPage'
export const routes = defineRoutes([
{
Component: AppLayout,
children: [
{ path: '/', Component: HomePage },
{ path: '/pokedex', Component: BrowsePage },
{ path: '/pokemon/{name}', Component: DetailPage },
],
},
{ path: '*', Component: NotFoundPage },
])A route has five fields, and only these five: anything else fails the build.
interface Route {
path?: string // '/pokemon/{name}', or '*' at the root
Component?: ComponentType
loader?: (ctx) => data // see Loaders and params
cache?: RouteCacheConfig // see Caching
children?: Route[] // makes this route a layout
}Try a path
The matcher below runs the routes above in the browser: type a path and see which route answers it, with the decoded params.
- Route
- /pokemon/{name}
- Params
- name = "mr mime"
- Status
- 200
Params and the catch-all
- A segment in braces,
{name}, is a param. The loader gets it decoded:%20becomes a space and%2Fbecomes/, while+stays a plus. A value with a broken%sequence is passed raw. '*'is the catch-all. It is allowed once, at the root, and it renders with status 404. Without one, an unknown path gets a plain 404.- Two routes that differ only in a param name, such as
/a/{id}and/a/{slug}, are the same route: the build fails withduplicate-route.
Paths beyond ASCII
A literal path may contain any character: { path: '/café' } matches what a browser sends, /caf%C3%A9. The build stores the pattern in that encoded form and normalises every request path the same way (lowercase hex is uppercased, raw UTF-8 is encoded) before matching, so /café, /caf%c3%a9 and /caf%C3%A9 are one route and one cache entry. The path is never decoded before matching: %2F inside a param stays a literal slash in the param's value, and a loader still gets the decoded value in params.
Layouts and Outlet
A route with children is a layout. It can leave out path, which adds no prefix. Its component renders <Outlet/> where the matched child goes. The outermost layout returns the whole document.
import { Outlet } from '@brust/core/routes'
export default function AppLayout() {
return (
<html lang="en">
<head>
<title>Pokédex</title>
</head>
<body>
<nav><a href="/pokedex">Pokédex</a></nav>
<main>
<Outlet />
</main>
</body>
</html>
)
}A layout must stay static or native. <Outlet/> inside a component that compiles to a React island fails with outlet-in-react, and an Outlet in a component that is not a layout route fails with outlet-outside-layout.
What the build checks
| Rule | When |
|---|---|
route-config | An unknown field, a leaf without path or Component, a bad cache. |
duplicate-route | Two routes match the same paths. |
nested-catch-all | '*' below the root. |
duplicate-catch-all | More than one '*'. |