Core concepts
Routing
src/routes.ts — the one file rshono requires, and the only place routes are declared.
Routes are an explicit table, not a directory scan. One file lists every page and endpoint, matched in order.
import { defineRoutes } from '@rshono/core';
export const routes = defineRoutes({
routes: [
{ path: '/', component: () => import('./components/home') },
{ path: '/profile/:id', component: () => import('./components/profile') },
{
path: '/docs/:slug',
render: 'static',
component: () => import('./components/documentation'),
staticPaths: async () => [{ slug: 'getting-started' }, { slug: 'deployment' }],
},
{ type: 'endpoint', path: '/api/health', server: () => import('./health') },
],
notFound: { component: () => import('./components/404') },
error: { component: () => import('./components/500') },
});
routes.ts only ever runs on the server, so importing server-only modules from it — inside
staticPaths, say — is safe. A plain array is shorthand when there are no notFound / error pages:
export const routes = defineRoutes([{ path: '/', component: () => import('./components/home') }]);
Paths
Paths use Hono's syntax: :id, :id{[0-9]+} and * all work.
PageProps<'/users/:id/posts/:postId'> turns that literal into { id: string; postId: string }, and
defineRoutes checks each page's props against the path it is mounted at. Out of step, and the
component field errors with component props are not satisfied by PageProps<'/…'> — at the route
definition, not at runtime.
Page routes
type can be omitted; 'page' is the default.
| Field | Meaning |
|---|---|
path |
Hono-style pattern, e.g. /, /profile/:id, /files/*. |
component |
Dynamic import of the page module; its default export is the page. |
render |
'static' prerenders at build time; 'dynamic' (the default) renders per request. |
staticPaths |
For a parameterised static route: the param sets to prerender, one file each. |
Write component inline as () => import('…'). The framework detects that exact form and injects
Rspack's 'use server-entry' directive for you — see Pages
for when you have to write it yourself.
Static rendering
render: 'static' builds a route once, at build time, in both the forms a page is asked for:
index.html for a hard load, and index.rsc — the flight payload — for a soft navigation. Both carry a
weak ETag, so a revalidation costs a 304 rather than the page.
A static route with params needs staticPaths. It runs at build time only, on the server, so it may
hit a database or the filesystem:
staticPaths: async () => (await db.docs.all()).map((d) => ({ slug: d.slug })),
Rules worth knowing:
- Reading
ctxthrows. There is no request at build time. Useparamsandurl, or make the route dynamic. url.searchParamsis always empty. One file answers every request whatever its query. Read the query withuseNavigation().urlon the client.- Set
siteUrlif the page builds absolute URLs. Without it the origin ishttp://localhostand the build warns. - Wildcard, optional and regex params cannot be prerendered. A parameterised static route without
staticPathsfalls back to per-request rendering, and the build warns. So does a page that did not render cleanly. - With
csp: truethe document is rendered per request — a prerendered file cannot carry a per-request nonce. The flight payload is still served from the prerender.
Endpoint routes
An endpoint route is served by a raw Hono handler instead of a component — JSON APIs, webhooks,
redirects, feeds. method defaults to 'all'.
{ type: 'endpoint', path: '/api/health', method: 'get', server: () => import('./health') }
// src/health.ts
import type { Handler } from 'hono';
export const handler: Handler = (c) => c.json({ ok: true });
The module only ever loads on the server, so reading secrets from it is safe.
notFound and error
Both are optional, and both are real server components with a page's contract minus a path of their own.
notFound— rendered with a 404 for unmatched paths and fornotFound()calls.error— rendered with a 500 when a request throws, and given an extraerrorprop: message-only in production, message plus stack in dev.
See Configuration & security for what happens when a failure is
bad enough that the error page itself cannot be reached.