Navigated to /docs/core/route-files-and-matching

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.

TEXT
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 syntaxWhat it matchesResult
[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.

TSX
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, and generateStaticParams.

  • Layout modules use the same data, middleware, head, and rendering exports and receive children from the route beneath them.

  • Use defineRoutePage from @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.

TEXT
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 PageErrorProps contract: pathname, params, route layers, page data, and the error.

  • src/pages/_error.tsx is 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.

TSX
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.tsx currently wraps every matched route before directory layouts and uses the layer ID _root.

  • layout = false currently skips directory _layout.tsx modules 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

BASH
bashnpx tavo routes
npx tavo inspect route /projects/new --json
npx tavo inspect route /projects/example --json
npx tavo check

Verify 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.