Project Structure

← docs

Project Structure

hono-preact/                    # monorepo root
├── apps/
│   └── app/                    # the deployed application
│       ├── src/
│       │   ├── server.tsx      # Hono app — API routes + catch-all SSR handler
│       │   ├── client.tsx      # Browser entry — hydrates the Preact app
│       │   ├── iso.tsx         # Shared component tree — Router + route registration
│       │   ├── pages/          # Page components (one file per route)
│       │   │   └── docs/       # MDX content pages (auto-discovered as /docs/* routes)
│       │   ├── server/         # Server-only app code
│       │   │   └── layout.tsx  # HTML shell rendered on every request
│       │   ├── components/     # Shared UI components
│       │   ├── styles/         # Global CSS
│       │   └── shims/          # Browser environment shims (e.g. process)
│       ├── vite.config.ts      # Two build configs: client bundle + Worker bundle
│       └── wrangler.jsonc      # Cloudflare Workers deployment config
└── packages/
    ├── iso/                    # @hono-preact/iso — isomorphic loader utilities
    ├── server/                 # @hono-preact/server — HonoContext + server middleware
    └── vite/                   # @hono-preact/vite — server-only Vite plugins

Key files

apps/app/src/server.tsx

The Hono application. Defines API routes and a catch-all GET * handler that SSR-renders the full Preact app via renderPage from @hono-preact/server and returns the HTML response.

app
  .get('/api/movies', async (c) => { ... })
  .use(location)
  .get('*', (c) =>
    renderPage(c, <Layout context={c} />, { defaultTitle: 'hono-preact' })
  );

apps/app/src/client.tsx

The browser entry point. Picks up the server-rendered HTML and hydrates it in place. Runs only in the browser.

hydrate(<App />, document.getElementById('app'));

apps/app/src/iso.tsx

The shared component tree used by both the server (via Layout) and the client (via hydration). Defines the <Router> with all routes, including lazy-loaded standard pages and auto-discovered MDX pages.

const mdxModules = import.meta.glob('./pages/docs/*.mdx');
// Each .mdx file becomes a lazy /docs/* route — no manual registration needed.

apps/app/src/server/layout.tsx

Renders the full HTML shell: <head>, <body>, the #app mount point, and the <script> tag that loads the client bundle. The catch-all route in server.tsx renders this component and wraps its output in <!doctype html>.

@hono-preact/iso (workspace package)

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

  • loader.tsxgetLoaderData HOC, useReload hook, and Loader<T> type
  • page.tsxPage component: runs serverLoader on the server, RPC stub in the browser
  • cache.ts — single-value in-memory cache (createCache) to avoid re-fetching on client-side navigation
  • guard.tscreateGuard, runGuards, guard types
  • preload.ts — reads loader data serialized into data-loader attributes on first hydration

@hono-preact/server (workspace package)

Server-only utilities (packages/server/):

  • context.tsHonoContext and useHonoContext for accessing the Hono Context from Preact components during SSR
  • middleware/location.tslocation middleware that attaches the request URL to the Hono context for preact-iso's LocationProvider

@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, @hono/vite-build, and @hono/vite-dev-server
  • serverOnlyPlugin — during the client bundle build, replaces any *.server.* import with a no-op stub so server code never reaches the browser
  • serverLoaderValidationPlugin — fails the build if a .server.* file has named exports other than serverGuards; the server loader must be the default export