hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Active Links
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
View Transitions
Middleware
CSRF Protection
CLI
Vite Config
Project Structure
Composing Hono Middleware
WebSockets
Rooms & Presence
renderPage
Link Prefetch
Build & Deploy
Overview
Dialog
Popover
Tooltip
Menu
Context Menu
Select
Combobox
Toast
renderElement
useControllableState
mergeRefs
useListNavigation
useTypeahead
useListboxSelection
usePosition
usePositioner
useDismiss
useFocusReturn
useSafeArea
usePresence

Project Structure#

A hono-preact app is organized around a few key files: a route table, a layout shell, and paired view and server modules. This page maps the directory layout and explains the role of each entry point.

hono-preact/                    # monorepo root
├── apps/
│   └── site/                    # the deployed application
│       ├── src/
│       │   ├── api.ts          # Optional: custom Hono routes mounted before SSR
│       │   ├── Layout.tsx      # HTML shell with <Head>, <ClientScript />
│       │   ├── routes.ts       # The route table (defineRoutes manifest)
│       │   ├── pages/          # View components, paired .server.ts files, MDX content
│       │   ├── components/     # Shared UI components (incl. MdxArticle for MDX)
│       │   └── styles/         # Global CSS
│       ├── vite.config.ts      # Two build configs: client bundle + Worker bundle
│       └── wrangler.jsonc      # Cloudflare Workers deployment config
└── packages/
    ├── iso/                    # hono-preact: routing primitives + isomorphic data
    ├── server/                 # hono-preact/server: render + handler wiring
    └── vite/                   # hono-preact/vite: framework Vite plugins

Key files#

apps/site/src/routes.ts#

The single source of truth for URL routing. Exports a defineRoutes(...) manifest naming every page in the app, paired with an optional server import for any route that needs a loader or actions. See The Route Table.

import { defineRoutes } from 'hono-preact';

export default defineRoutes([
  { path: '/', view: () => import('./pages/home.js') },
  { path: '/movies', layout: () => import('./pages/movies-layout.js'), children: [...] },
  { path: '/demo/projects/:projectId', view: () => import('./pages/project.js'), server: () => import('./pages/project.server.js') },
  { path: '*', view: () => import('./pages/not-found.js') },
]);

Client and server entries (generated)#

The framework owns both entries as virtual modules. The browser entry (virtual:hono-preact/client) hydrates the app and the server entry (virtual:hono-preact/server) wires the Hono app from your routes.ts and Layout.tsx, including the loader RPC handler, the page action handler, the WebSocket upgrade endpoint, and the catch-all SSR handler. <Routes>, LocationProvider, and onRouteChange are all wrapped inside the generated entries; no iso.tsx or client.tsx lives in user code. To add custom REST routes, author an optional src/api.ts that exports a Hono app (or a function returning one); the generated entry mounts it before the SSR catch-all.

apps/site/src/Layout.tsx#

Renders the HTML shell. Uses <Head> and <ClientScript /> from hono-preact to declare the document title/meta and inject the client bundle script tag. Default-exports a component the framework's generated server entry renders for the catch-all route; the framework emits the <!doctype html> prefix and post-processes hoofd-collected head tags into the user's </head>. View Transitions fire automatically on every client-side route change in browsers that support document.startViewTransition; style them via ::view-transition-old(root) / ::view-transition-new(root) CSS.

apps/site/src/pages/#

View components and their paired server modules. Files in this folder are referenced by entries in routes.ts; nothing is auto-discovered. Naming and folder structure are the user's choice (the demo uses kebab-case movies-list.tsx + movies-list.server.ts siblings).

layout.server.ts is also valid alongside any layout.tsx. A loader declared there is auto-scoped to the layout's matched location, not the deepest active child route. The auto-scoping is structural: the framework infers scope from which route entry owns the .server.* file, with no opt-in flag required.

MDX content (pages/docs/**/*.mdx) is mounted by contentRoutes(import.meta.glob('./pages/docs/**/*.mdx')) in routes.ts, which turns each file into a route node under the /docs layout group.

hono-preact (workspace package)#

Isomorphic primitives shared between server and client (packages/iso/):

  • Routing: defineRoutes, Routes, RouteDef, LayoutProps, ViewProps, RoutesManifest, FlatRoute, ServerRoute. The route-table primitive and runtime mounter.
  • Page bindings: definePage(Component, { errorFallback, Wrapper }) factory plus <Page>, WrapperProps. Used by views that need a Wrapper. Page-layer middleware is declared via use on the route node in routes.ts; entries are built with defineServerMiddleware, defineClientMiddleware, and defineStreamObserver. Loader and fallback bindings live on serverLoaders.name.View() inside the page's JSX.
  • Loaders: defineLoader, LoaderRef, useReload. (LoaderRef carries useData(), useError(), invalidate(), cache, .View(), and .Boundary.)
  • Actions: defineAction, useAction, useOptimistic, useOptimisticAction, <Form>.
  • Caching: createCache (advanced: shared caches across loaders).
  • Middleware: defineServerMiddleware, defineClientMiddleware, defineStreamObserver, defineApp, plus the outcome constructors redirect, deny (and render at the hono-preact/page subpath) and predicates isOutcome / isRedirect / isDeny / isRender. See Middleware.
  • Utilities: prefetch, isBrowser, Route/Router/lazy (re-exports of preact-iso for advanced use).

App-level config (optional)#

// apps/site/src/app-config.ts
import { defineApp, defineServerMiddleware } from 'hono-preact';

const withRequestId = defineServerMiddleware(async (ctx, next) => {
  ctx.c.header('X-Request-Id', crypto.randomUUID());
  await next();
});

export default defineApp({ use: [withRequestId] });

The framework's generated server entry imports the default export from src/app-config.ts (configurable via the appConfig option to honoPreact()). If the file doesn't exist, the framework treats it as defineApp({}). The appConfig.use array is the outermost layer of the middleware chain: it wraps every loader, action, and page render. See Middleware: The three layers.

hono-preact/server (workspace package)#

Server-only utilities (packages/server/):

  • renderPage: SSR entry that prerenders to HTML, injects head tags via hoofd, and turns middleware-thrown redirect / deny / render outcomes into the appropriate HTTP response.
  • HonoContext / useHonoContext: access the Hono Context from Preact components during SSR.

hono-preact/vite (workspace package)#

Vite plugins for the framework (packages/vite/). The primary export is honoPreact(), which bundles all framework Vite configuration into a single plugin. See Vite Configuration for usage.

  • honoPreact() configures resolve deduplication, SSR bundling, client/server build outputs, and the client-shim auto-injection. Deployment target plugins (the worker or dev-server toolchain) come from the required adapter option, currently cloudflareAdapter() from hono-preact/adapter-cloudflare or nodeAdapter() from hono-preact/adapter-node.
  • serverOnlyPlugin rewrites *.server.* imports during the client bundle build, replacing static imports with no-op stubs and dynamic import('./*.server.*') calls with Promise.resolve({}) so server code never reaches the browser.
  • serverLoaderValidationPlugin fails the build if a .server.* file has named exports other than serverLoaders or serverActions.
  • moduleKeyPlugin rewrites each .server.* file at build time with a __moduleKey derived from the file path, which the loader/action handlers use to dispatch RPC requests.
  • clientShimPlugin prepends a globalThis.process ??= { env: { NODE_ENV } } shim to the client entry so libraries that read process.env.NODE_ENV at module-eval time in the browser do not throw.