renderPage#
Use renderPage in your server's catch-all handler to SSR your app. It prerenders the Preact tree to HTML, injects collected head tags, and returns a complete HTTP response, replacing the boilerplate for dispatcher setup, head tag injection, and HTML assembly that would otherwise live in every app's catch-all handler.
Usage#
import { renderPage } from 'hono-preact/server';
app.get('*', (c) => renderPage(c, <Layout context={c} />));
That single line replaces the boilerplate for dispatcher setup, prerendering, head tag injection, and HTML assembly that would otherwise live in every app's catch-all handler.
API#
renderPage(
c: Context,
node: VNode,
options?: { defaultTitle?: string; appConfig?: AppConfig }
): Promise<Response>
| Param | Description |
|---|---|
c | The Hono Context from your route handler |
node | The root Preact element to render, typically your <Layout> |
options.defaultTitle | Fallback <title> when no page sets one via hoofd. Defaults to ''. Express defaults via <Head defaultTitle="..."> in your Layout for the common case; the option is here for custom server entries that call renderPage directly. |
options.appConfig | The default-exported result of defineApp(). Surfaces app-level config to the render: middleware composition via use, plus opt-in features like speculation. The framework's generated server entry threads this for you; custom server entries must pass it explicitly to enable those features. |
Head tags#
Head tags (<title>, <meta>, <link>) are collected during prerender via hoofd. Pages set them with hoofd's useTitle, useMeta, and useLink hooks:
import { useTitle } from 'hoofd/preact';
export default function MoviesPage() {
useTitle('Movies');
return <main>…</main>;
}
renderPage injects the collected tags into the <head> of the rendered HTML before returning the response.
Redirect outcomes#
If a middleware throws redirect('/path') during SSR, renderPage catches the outcome and turns it into an HTTP redirect via c.redirect(), so you don't need to handle it in the route handler. Deny outcomes thrown during SSR map to an HTTP response at the deny's status; render outcomes substitute an alternative component into the prerender tree.
Custom server routes#
Most apps never write the server entry. The framework's Vite plugin generates it, mounting loadersHandler on POST /__loaders, the page POST action handler, your optional src/api.ts, and the renderPage SSR catch-all on one Hono app exported as the worker's default.
For your own server-side routes, author src/api.ts; the generated entry mounts it alongside the framework routes, so you get custom endpoints without owning the entry:
// src/api.ts
import { Hono } from 'hono';
const api = new Hono().get('/api/movies', async (c) => {
return c.json(await getMovies());
});
export default api;
renderPage is public (see Usage above), so a custom catch-all can call it directly when you need bespoke layout injection or a defaultTitle. The loader and page-action handlers are not a public hand-wiring surface, though. The framework's generated entry delegates the full wiring to an internal createServerEntry factory behind hono-preact/server/internal/runtime, a private, version-coupled contract the codegen emits. That factory builds the route-use resolver carrying your page-level use guards (including auth gates) and threads it into both handlers, so a page guard can never be silently dropped on the loader or action path. Because the factory is internal, hand-assembling a full entry depends on a door that can change in any non-major release: pin your framework version if you reach for it. Short of an alternate runtime, prefer api.ts plus the generated entry.