Project Structure
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. DocsRoute 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 location middleware, 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 via apps/site/src/components/DocsRoute.tsx, which is registered as the view for /docs and /docs/* in routes.ts. The DocsRoute component owns its own import.meta.glob and inner <Router> for sub-page discovery.
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, use, Wrapper })factory plus<Page>,WrapperProps. Used by views that need a Wrapper or page-level middleware. Theusearray holds entries built viadefineServerMiddleware,defineClientMiddleware, anddefineStreamObserver; the dispatcher partitions members by environment at runtime. Loader and fallback bindings live onserverLoaders.name.View()inside the page's JSX. - Loaders:
defineLoader,LoaderRef,useReload. (LoaderRefcarriesuseData(),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 constructorsredirect,deny(andrenderat thehono-preact/pagesubpath) and predicatesisOutcome/isRedirect/isDeny/isRender. See Middleware. - Utilities:
prefetch,isBrowser,env,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 HonoContextfrom Preact components during SSR.location: middleware that attaches the request URL to the Hono context for preact-iso'sLocationProvider.pageActionHandler,loadersHandler: page POST action handler andPOST /__loadersmiddleware. Accept either a Viteimport.meta.globresult or the record produced byrouteServerModules.routeServerModules: adapter that converts aRoutesManifestinto the lazy-glob shape the handlers consume.
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 requiredadapteroption, currentlycloudflareAdapter()fromhono-preact/adapter-cloudflareornodeAdapter()fromhono-preact/adapter-node.serverOnlyPluginrewrites*.server.*imports during the client bundle build, replacing static imports with no-op stubs and dynamicimport('./*.server.*')calls withPromise.resolve({})so server code never reaches the browser.serverLoaderValidationPluginfails the build if a.server.*file has named exports other thanserverLoaders,serverActions, or the recognizeduseexports (pageUse,loaderUse,actionUse).moduleKeyPluginrewrites each.server.*file at build time with a__moduleKeyderived from the file path, which the loader/action handlers use to dispatch RPC requests.clientShimPluginprepends aglobalThis.process ??= { env: { NODE_ENV } }shim to the client entry so libraries that readprocess.env.NODE_ENVat module-eval time in the browser do not throw.