Pages and layouts
Turn files into routes, compose nested application shells, and provide route-specific pending and error views.
Start with the route tree
Tavo.js derives application routes from src/pages. Keep route modules focused on route concerns: loader data, metadata, render mode, and the page component. Move reusable interface and business behavior into components, controllers, and stores.
Dynamic segments use brackets, catch-all segments use three dots, and folders wrapped in parentheses organize files without changing the public URL.
textsrc/pages/index.tsx → /
src/pages/dashboard/index.tsx → /dashboard
src/pages/blog/[id].tsx → /blog/:id
src/pages/docs/[[...slug]].tsx → /docs/*?slug
src/pages/(marketing)/about.tsx → /aboutDefine a functional page
Export route behavior as named functions and render the completed result from the default component. The filename determines the URL; PageProps, PagePendingProps, PageErrorProps, and PageLoadContext provide explicit data and parameter types without wrapping the module.
tsximport type {
PageErrorProps,
PageLoadContext,
PagePendingProps,
PageProps
} from "@tavojs/core/router";
import { Page, Text } from "@tavojs/ui";
type Project = { name: string };
type ProjectParams = { id: string };
export async function load({ params, signal, url }: PageLoadContext): Promise<Project> {
const endpoint = new URL(`/api/projects/${params.id}`, url);
const response = await fetch(endpoint, { signal });
if (!response.ok) throw new Error("Project could not be loaded");
return response.json();
}
export function pending({ params }: PagePendingProps<ProjectParams>) {
return <Page aria-busy="true"><Text>Loading project {params.id}…</Text></Page>;
}
export function error({ pathname }: PageErrorProps<ProjectParams>) {
return <Page><Text role="alert">Could not load {pathname}.</Text></Page>;
}
export default function ProjectPage({ data }: PageProps<Project, ProjectParams>) {
return <Page><Text as="h1" variant="h1">{data?.name}</Text></Page>;
}Compose layouts and failures
A _layout.tsx file wraps every descendant route. Layouts compose from root to leaf, so the root can own global navigation while a dashboard layout owns its sidebar and shared loader data. Each layout receives its own loader result as data and its own loader failure as error.
Use route groups for alternate shells without adding a URL segment.
Add
src/pages/404.tsxfor unmatched URLs.Export pending from a page for active client-navigation feedback after its layout loaders resolve.
Export error from a page for its contextual loader-failure view; otherwise
src/pages/_error.tsxis the application-wide fallback.A
notFound() signal bypasses both error views and renderssrc/pages/404.tsx.Handle a layout-loader failure from that layout's error prop; descendant loaders continue unless the loader signals not found.
Keep request-specific data in layout loader results, not process-wide stores during SSR.
Page module reference
A page or layout is a functional default export with optional named route exports. The filename determines the public route, while load, action, middleware, head, render, prerender, revalidate, vary, cacheTags, and generateStaticParams add route behavior. Page modules can additionally export pending and error components.
Page and layout components receive the resolved URL params and their own loader data. The layers array preserves every layout and page result, while layerData exposes those results by layer ID.
tstype PageModuleRecord = {
default: Component<Record<string, unknown>>;
pending?: Component<PagePendingProps>;
error?: Component<PageErrorProps>;
load?: PageLoader;
action?: PageAction;
head?: PageHeadExport | ((context: PageLoadContext & {
data: unknown; error: unknown;
}) => PageHeadExport);
middleware?: PageMiddleware | PageMiddleware[];
render?: "csr";
prerender?: boolean;
static?: boolean;
revalidate?: number | false;
vary?: string | string[];
cacheTags?: string | string[] | ((context: PageLoadContext) => MaybePromise<string | string[]>);
generateStaticParams?: () => MaybePromise<Record<string, string>[]>;
};
type PageProps<TData = unknown, TParams = Record<string, string | undefined>> = {
pathname?: string;
params: TParams;
data?: TData;
error?: unknown;
layers?: Array<{ id: string; kind: "layout" | "page"; data: unknown; error: unknown }>;
layerData?: Record<string, unknown>;
};
type PagePendingProps<TParams = Record<string, string | undefined>> = {
pathname: string;
params: TParams;
layers: RouteDataLayer[];
layerData: Record<string, unknown>;
};
type PageErrorProps<TParams = Record<string, string | undefined>> =
PagePendingProps<TParams> & {
data: unknown;
error: unknown;
};Route conventions and matching
index.tsxmaps to its folder path;_layout.tsxwraps descendants;404.tsxhandles unmatched paths; a page error export handles its loader failure before the global_error.tsxfallback.[id] is a required dynamic segment, [[id]] is optional, [...all] is a required catch-all, and [[...all]] is an optional catch-all.
Folders in parentheses are route groups: they organize files and select layouts without adding a URL segment.
RouteParamsFromPath<TPath> derives string parameters. Optional parameters are string | undefined.LoaderData<TLoader> unwraps the loader's awaited return type.defineRoutePageis optional route-aware typing assistance. It does not register or rename a route; keep its path literal aligned with the filename and confirm the result with tavo routes.defineRoutePagealso infers route params for pending and error components when the helper form is useful.The CLI generates functional modules by default. Use
tavogenerate page <name>--typed-routeonly when the helper form is useful.Routes are sorted by segment specificity before matching: static segments win over dynamic segments, required parameters win over optional ones, and catch-all segments come last. Equivalent patterns use a lexical path tie-break, so filesystem discovery order never changes the result.
tsx// src/pages/projects/[projectId]/tasks/[[taskId]].tsx
import type { PageProps } from "@tavojs/core/router";
type Params = {
projectId: string;
taskId?: string;
};
export default function TaskPage({ params }: PageProps<unknown, Params>) {
return (
<main>
Project {params.projectId}; task {params.taskId ?? "overview"}
</main>
);
}Route pending and error reference
A page can export pending for unresolved browser navigation and error for a contextual page-loader failure. Both exports are normal Tavo.js components: use a function component for render-only feedback or createTavo when the state needs a model, controller, lifecycle, or cleanup.
Client navigation changes the URL, runs middleware, resolves target layout loaders, renders pending inside those layouts, runs the page loader, and then renders the completed page or route error.
The default page component and controller do not mount until the page loader completes. Pending props intentionally omit page loader data but include resolved layout layers and
layerData.A
createTavopending component's controller receives the target route throughthis.page, including pathname, route, status, params, layers, andlayerData;this.page.datais unavailable while the loader is unresolved.Without pending, the previous page stays visible while the target content region is marked busy.
The target page error export wins for its loader failure; otherwise
Tavo.jsrenderssrc/pages/_error.tsx.notFound() bypasses both and renderssrc/pages/404.tsxwith status 404.A layout-loader failure enters error handling instead of rendering pending with invalid layout data.
Normal SSR, static prerendering, prefetching, and fresh route-cache hits do not render pending.
A replaced navigation aborts obsolete resolution, removes its pending view, and prevents stale data or errors from becoming active.
Use
aria-busyand an accessible label for pending UI, avoid moving focus into a skeleton, and announce route errors without exposing private diagnostic details.
tsximport type {
PageErrorProps,
PagePendingProps
} from "@tavojs/core/router";
export function pending({ params }: PagePendingProps<{ id: string }>) {
return <main aria-busy="true">Loading report {params.id}…</main>;
}
export function error({ pathname }: PageErrorProps<{ id: string }>) {
return <main role="alert">Could not load {pathname}.</main>;
}Rendering, static output, and cache options
SSR is the normal server render mode. A route becomes CSR when its module chain selects render: "csr". Static and revalidation policy composes across root, layouts, and page rather than belonging only to the leaf page.
In functional modules, export const prerender = true enables static output. false or revalidate = false disables an inherited static policy.
revalidate is measured in seconds, rounded down, clamped to zero, and the shortest finite value in the module chain wins.
vary header names are lowercased and deduplicated. Localization also varies cached output by Accept-Language.
cacheTagscan be static strings or request-aware resolvers. Tags support targeted invalidation in runtimes that expose it.CSR routes ignore static, revalidate, vary, cache tags, and
generateStaticParams. The manifest reports incompatible declarations.generateStaticParamsis required to enumerate build-time paths for a dynamic static route.
tsximport { defineRoutePage } from "@tavojs/core/router";
export default defineRoutePage("/catalog/[id]", {
static: true,
revalidate: 300,
vary: "accept-language",
cacheTags: ({ params }) => ["catalog", `product:${params.id}`],
generateStaticParams: async () => [{ id: "starter" }],
load: ({ params, signal }) => getProduct(params.id, { signal }),
default: ({ data }) => <ProductPage product={data} />
});Configure shared route behavior
Put application-wide page props, not-found UI, CSR fallback content, middleware, localization, redirect policy, trusted hosts, and cache limits under ssr in
tavo.config.ts.Configure
ssr.csrFallbackthroughdefineConfig; normal applications use the framework boot flow and do not construct a pages runtime.Install plugins through the top-level plugins configuration. Plugin graph compilation and runtime construction are framework host responsibilities.
Use
tavoroutes andtavoinspect route <path>--jsonfor route inspection. Experimental tooling can use the supported@tavojs/core/devinspection exports.
ts// tavo.config.ts
import { defineConfig } from "@tavojs/core/config";
export default defineConfig({
ssr: {
csrFallback: "Loading application…",
maxResolvedCacheEntries: 512
}
});Look up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.