hono-preact
Overview
Quick Start
The Route Table
Layouts & Nesting
Adding Pages
Head Management
Active Links
Server Loaders
Loading States
Reloading Data
Prefetching
Streaming
Live Loaders
Realtime Channels
Server Actions
Validation
Optimistic UI
Server Caller
View Transitions
Middleware
CSRF Protection
CLI
Vite Config
Project Structure
Styling
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

Vite Configuration#

The hono-preact/vite subpath exports a honoPreact() plugin that configures Vite for the framework's two-pass build and dev server. Your vite.config.ts only needs to wire in the plugins that are specific to your application.

Minimal setup#

import { honoPreact } from 'hono-preact/vite';
import { cloudflareAdapter } from 'hono-preact/adapter-cloudflare';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [honoPreact({ adapter: cloudflareAdapter() })],
});

adapter is required; honoPreact() throws a clear error if it is omitted. The plugin auto-includes @preact/preset-vite internally, so you don't import it yourself. The framework ships two adapters today: cloudflareAdapter() for Cloudflare Workers, imported from hono-preact/adapter-cloudflare, and nodeAdapter() for Node.js (via @hono/node-server), imported from hono-preact/adapter-node. Each adapter supplies its own Vite plugins and entry wrapper, so the framework core stays target-agnostic.

honoPreact(options)#

OptionTypeDefaultDescription
adapterHonoPreactAdapterrequiredDeployment target, e.g. cloudflareAdapter() or nodeAdapter().
layoutstring'src/Layout.tsx'Root layout component path.
routesstring'src/routes.ts'Route table path.
apistring'src/api.ts'Optional custom routes; loaded only if the file exists.
appConfigstring'src/app-config.ts'Optional app config; loaded only if the file exists.
serverDirstring'src/server'Registry folder for route-less server modules; globbed if it exists. See The src/server registry.
clientEntrystring'virtual:hono-preact/client'Client entry module id.
css{ global?: string; autoSplit?: boolean; minSize?: number }undefinedFramework-owned global stylesheet delivery and build-time auto-split tuning. See Styling for the full pipeline.

What the plugin handles#

honoPreact() configures everything the framework requires:

ConcernWhat it sets
Preact deduplicationresolve.dedupe for preact, preact/hooks, preact-iso
Build targetbuild.target: 'esnext', build.assetsDir: 'static'
Client build outputstatic/client.js entry, hashed chunk and asset filenames in the client environment
Deployment targetDelegated to the configured adapter (Cloudflare Workers, Node.js, etc.), which supplies its own Vite plugins and entry wrapper
Server-only importsserverOnlyPlugin stubs static *.server.* imports and dynamic () => import('./*.server.*') calls in the client bundle (see Project Structure for the .server.* file convention)
Loader validationserverLoaderValidationPlugin enforces .server.* export conventions
Module identitymoduleKeyPlugin injects a path-derived __moduleKey into each .server.* file for RPC routing
Guard stripguardStripPlugin rewrites opposite-environment middleware bodies to no-ops so server-only code tree-shakes out of the client bundle
Browser shimclientShimPlugin prepends a globalThis.process ??= ... shim to the client entry so libraries reading process.env.NODE_ENV at module-eval time do not throw
CSS pipelinebuild.cssMinify: 'lightningcss' and Baseline browser targets (css.lightningcss.targets), unless you set cssMinify or css.lightningcss yourself, in which case your config wins outright. See Styling.

Peer dependencies#

Peer deps depend on the adapter you pick. Install the ones that match your deployment target.

npm install -D @cloudflare/vite-plugin wrangler

Add @hono/node-ws to the Node.js install if you want WebSocket support.

Adding MDX#

MDX is a user-space choice and not included in the plugin. Add it alongside honoPreact():

import { honoPreact } from 'hono-preact/vite';
import { cloudflareAdapter } from 'hono-preact/adapter-cloudflare';
import mdx from '@mdx-js/rollup';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    honoPreact({ adapter: cloudflareAdapter() }),
    Object.assign(mdx({ jsxImportSource: 'preact' }), { enforce: 'pre' }),
  ],
});

The enforce: 'pre' placement ensures MDX files are transformed to JSX before other plugins see them.

Path aliases#

honoPreact() does not add path aliases; those belong in your config. A common setup:

import { resolve } from 'node:path';

export default defineConfig({
  resolve: {
    alias: [{ find: '@', replacement: resolve(__dirname, './src') }],
  },
  plugins: [honoPreact({ adapter: cloudflareAdapter() })],
});

Custom client entry path#

The plugin defaults to the framework-generated virtual module virtual:hono-preact/client, which hydrates <Routes> under <LocationProvider> for you. Override it only when you need a hand-written entry:

honoPreact({
  adapter: cloudflareAdapter(),
  clientEntry: 'src/main.tsx',
});

clientEntry is the single source of truth for both the rollup input and the clientShimPlugin transform target, so you only set it in one place.

A custom entry takes over the virtual entry's responsibilities. Before hydrating, call bootClient() (exported from hono-preact). It installs the client runtime services the framework relies on: the history shim (back/forward direction tracking), the navigation transition scheduler (view transitions on route changes), and the stream registry (live-loader streams). Skipping the call silently disables all three, so the dev server warns when the configured clientEntry module never references bootClient.

// src/main.tsx
import { hydrate } from 'preact';
import { LocationProvider } from 'preact-iso';
import { Routes, bootClient } from 'hono-preact';
import routes from './routes.js';

bootClient();

hydrate(
  <LocationProvider>
    <Routes routes={routes} />
  </LocationProvider>,
  document.getElementById('app')
);