Route files and matching
Use the exact src/pages conventions, understand deterministic route precedence, and place route-level failure UI correctly.
Map src/pages to URLs
Tavo.js discovers route modules below src/pages. A normal page file contributes a URL; a special module changes how matching, layout, or failure rendering works. Route groups organize files and select layouts without adding a path segment.
textsrc/pages/
index.tsx → /
about.tsx → /about
projects/
_layout.tsx → wraps project descendants
index.tsx → /projects
new.tsx → /projects/new
[id].tsx → /projects/:id
[[tab]].tsx → /projects/:?tab
[...path].tsx → /projects/*path
(account)/
_layout.tsx → selects a layout; no URL segment
settings.tsx → /settings
404.tsx → unmatched and notFound() UI
_error.tsx → fallback for page-loader errors| File syntax | What it matches | Result |
|---|---|---|
[id] | Exactly one required segment. | params.id is a decoded string. |
[[tab]] | Zero or one segment. | params.tab is string | undefined. |
[...path] | One or more remaining segments. | params.path contains the decoded slash-joined value. |
[[...path]] | Zero or more remaining segments. | params.path is string | undefined. |
(account) | No URL segment. | The group remains part of layout identity. |
Keep route modules functional and explicit
The default export renders the page. Named exports add behavior without changing the route path. defineRoutePage is optional route-aware typing; it does not register the route or override its filename. Its path literal is checked with RouteParamsFromPath, while the filesystem remains authoritative.
tsximport type {
PageLoadContext,
PageProps
} from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { id: string; name: string };
type ProjectParams = { id: string };
export async function load({
params,
signal,
url
}: PageLoadContext): Promise<Project> {
const response = await fetch(
new URL(`/api/projects/${encodeURIComponent(params.id)}`, url),
{ signal }
);
if (!response.ok) {
throw new Error("Could not load project");
}
return response.json() as Promise<Project>;
}
export function head({ data }: { data: Project | null }) {
return <title>{data ? data.name : "Project"}</title>;
}
export default function ProjectPage({
data
}: PageProps<Project, ProjectParams>) {
return (
<Page>
<Text as="h1" variant="h1">
{data?.name}
</Text>
</Page>
);
}Page modules may export load, action, middleware, head, pending, error, render, prerender, revalidate, vary,
cacheTags, andgenerateStaticParams.Layout modules use the same data, middleware, head, and rendering exports and receive children from the route beneath them.
Use
defineRoutePagefrom @tavojs/core/router later when path-derived params and one object are clearer for a complex route.Keep the helper path literal aligned with the file path and inspect the generated manifest; the filesystem remains authoritative.
Predict deterministic route precedence
Matching is independent of filesystem discovery order. Tavo.js compares each segment from left to right and tries the more specific pattern first.
textstatic
→ required dynamic [id]
→ optional dynamic [[id]]
→ required catch-all [...path]
→ optional catch-all [[...path]]
/projects/new wins over /projects/[id]
/docs/[version] wins over /docs/[...path]
/files/[...path] wins over /files/[[...path]]When two compiled patterns have identical specificity, Tavo.js uses a lexical path tie-break. Treat equivalent patterns as a collision to fix, not as a way to choose behavior by declaration order.
Place 404 and error UI at the correct boundary
src/pages/404.tsx renders when no route matches and when a loader or middleware calls
notFound(). The response status is 404.A page-local error export handles that page loader's failure and implements the
PageErrorPropscontract: pathname, params, route layers, page data, and the error.src/pages/_error.tsxis the fallback when a page loader fails and the page has no local error export.A layout receives its own loader error through its error prop. Descendant loaders still run unless the failure is
notFound(), and page pending UI is skipped while a layout error exists.Files whose stem begins with an underscore are not public routes. Only documented special filenames receive special behavior.
tsximport { Page, Text } from "@tavojs/ui";
export default function NotFoundPage({
pathname
}: {
pathname?: string;
}) {
return (
<Page>
<Text as="h1" variant="h1">
Page not found
</Text>
<Text>The path {pathname ?? "you requested"} does not exist.</Text>
</Page>
);
}Keep an outer shell when a page skips directory layouts
In Core 1.0.4, src/pages/_root.tsx supplies the outermost wrapper for matched routes. A page can export const layout = false to skip directory layouts while keeping that root wrapper. Use this for a print or embedded page that needs the shared root but a different shell.
Only
src/pages/_root.tsxis the root module; a nested_root.tsxdoes not define another root. Its loader, middleware, and head run before directory layers, and its layer ID is _root.A page-level layout = false skips all directory
_layout.tsxmodules for that page, including their loaders, middleware, and metadata. It retains_root.tsxand its behavior.Keep the default directory layout model unless a page needs this opt-out. Do not assume authentication middleware on a skipped layout still runs; protect every server action and data endpoint independently.
Inspect the resulting route to confirm its wrapper and loader chain.
Root-levelcacheTagshave a separate limitation described in Static output and route cache.
Verify the route graph
bashnpx tavo routes
npx tavo inspect route /projects/new --json
npx tavo inspect route /projects/example --json
npx tavo checkVerify both the static and dynamic examples so precedence is observable. Also request an unknown path and a loader path that calls notFound() to confirm the same 404 module and status are used.
Look up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.