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 after revalidate seconds, serve stale-while-revalidate in between.

Global ISR defaults are ttl 60s and swr 1h.

Info

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