Routing & Rendering
Routes live in src/app/ as a file-based tree. Directory nesting maps to URL nesting, and
special filenames define what each segment renders.
Route files
| File | Role |
|---|---|
page.tsx |
The page. Default export is the Page component; props { params, searchParams, data?, actionData? }. |
layout.tsx |
Wraps its subtree: ({ children, route }) => Renderable. Nests by directory. The root layout owns <html>. |
route.ts |
API route. Export GET / POST / PUT / DELETE / … each (request: Request) => Response. |
loading.tsx |
Suspense fallback for the segment. |
error.tsx |
Error boundary: ({ error, reset, route }). |
not-found.tsx |
Rendered for a 404 in the segment. |
template.tsx |
Like a layout, but a fresh instance per navigation. |
A route file may also export data functions (loader / action) — see
Loaders & Actions.
The Page component
The default export of page.tsx receives:
/** @jsxImportSource @bext-stack/framework */
export default function Page({ params, searchParams, data, actionData }) {
return <h1>Hello {params.slug}</h1>;
}
The Page never sees the raw request — only loader and action do. Read cookies and
headers there and pass what you need down via data / actionData.
Dynamic, catch-all, groups, and parallel routes
| Pattern | Meaning | Example URL |
|---|---|---|
[slug] |
Dynamic segment → params.slug |
/posts/hello |
[...slug] |
Catch-all → params.slug (array) |
/docs/a/b/c |
(group) |
Grouping only — no effect on the URL | — |
@name/page.tsx |
Named parallel-route slot | rendered into a slot |
Metadata
Add head tags with a metadata export, or generateMetadata() when they depend on the
request or loaded data:
export const metadata = {
title: "Posts",
description: "All posts",
};
// or, dynamic:
export function generateMetadata({ params, data }) {
return { title: data.post.title };
}
Rendering modes
The rendering mode is set in bext.config.toml:
[rendering]
mode = "isr" # "isr" | "ssr"
revalidate = 60 # seconds
ssr— render on every request.isr— incremental static regeneration: cache the rendered output, revalidate afterrevalidateseconds, serve stale-while-revalidate in between.
Global ISR defaults are ttl 60s and swr 1h.
Under ISR, a route that exports a loader or action auto-skips the ISR cache and
renders fresh on each request (SWR still bounds staleness). Dynamic, per-request data is
therefore always current without extra config.
Opting out of caching
export const dynamic = "force-dynamic";
force-dynamic opts the route out of caching entirely.
Streaming
Streaming SSR is automatic or opt-in. A route streams when any of these hold:
- it imports Suspense,
- it has a
loading.tsx, - a component is an async generator, or
- it exports
export const renderingMode = "streaming".
Response helpers
The framework exports helpers for building responses and revalidating cached routes:
import {
json, html, text, xml, redirect, notFound, cached,
revalidatePath, revalidateTag,
} from "@bext-stack/framework";
| Helper | Use |
|---|---|
json, html, text, xml |
Build a typed Response with the right content type. |
redirect |
Return a redirect Response. |
notFound |
Return a 404 Response. |
cached |
Wrap a response with cache semantics. |
revalidatePath(path) |
Invalidate a cached route by path. |
revalidateTag(tag) |
Invalidate cached routes by tag. |
Returning or throwing a Response from a loader or action short-circuits the page
render — see Loaders & Actions.
Where to next
| Page | What it covers |
|---|---|
| Loaders & Actions | Loading data, mutations, request/session access |
| SDK Overview | Site layout, the loopback SDK, vendoring |
| CLI Reference | bext dev, bext build, bext routes |
| Config Reference | bext.config.toml |