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/${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>
);
}Treat two implemented conventions as contract work in progress
Current Core source and tests recognize a top-level _root.tsx and a page-level layout = false export. Their final public 1.0 semantics are not yet ratified.
_root.tsxcurrently wraps every matched route before directory layouts and uses the layer ID _root.layout = false currently skips directory
_layout.tsxmodules for that page while retaining_root.tsx.Do not make reusable application architecture depend on either convention until Core publishes its stable contract, inheritance rules, and migration guarantees.
Use explicit directory layouts for production documentation and examples in the meantime.
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.