# Hono Inertia

> Use @hono/inertia on the server and @ts-76/inertia-hono-jsx on the client to get SPA-style interactivity without leaving the Hono/hono-jsx stack.

- Skill: `yusukebe/hono-inertia` (Agent Skill)
- Install (CLI): `npx skillmds@latest add yusukebe/hono-inertia`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yusukebe/hono-inertia/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: yusukebe (https://skillmd.com/u/yusukebe)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yusukebe/hono-inertia

---


# Build SPA-feel apps with Inertia.js + Hono + hono/jsx

Inertia.js lets you build single-page apps without writing a separate frontend or a JSON API layer. The server returns Inertia responses (page name + props), and a thin client adapter swaps the current page component on the fly. With Hono as the server and `hono/jsx` as the client, you get SPA interactivity while staying inside one TypeScript stack — no React.

Reach for this when server-rendered `hono/jsx` is no longer enough — that is, when you need client-side state, optimistic UI, or rich forms.

## Stack

- **Server:** `hono` + `@hono/inertia`. The `inertia({ rootView })` middleware adds `c.render(name, props)` for returning Inertia responses.
- **Client:** `@ts-76/inertia-hono-jsx`. Provides `createInertiaApp`, `<Link>`, `<Form>`, `<Head>`, `useForm`, `useHttp`, etc., on top of `@inertiajs/core` and `hono/jsx/dom`.
- **Bundling / SSR:** Vite (with `@cloudflare/vite-plugin` when deploying to Workers) and `vite-ssr-components` for asset wiring. Start from the `cloudflare-workers+vite` Hono starter template — it has the Vite + Workers wiring already in place: <https://github.com/honojs/starter/tree/main/templates/cloudflare-workers+vite>.
- **Page typing:** `@hono/inertia/vite` generates `pages.gen.ts` so `PagePropsFor<Name>` resolves to the props passed to `c.render(Name, ...)`.

## Reference example

`yusukebe/hono-inertia-example` is the canonical layout (Hono + Inertia on Cloudflare Workers). It is written in **React**, so do not copy its `app/client.tsx` and `app/root-view.tsx` verbatim — those need to be rewritten against `@ts-76/inertia-hono-jsx` and `hono/jsx`. The server side (`app/server.ts`, route shape, Vite config, `wrangler.jsonc`, validation with `@hono/zod-validator`) transfers as-is.

<https://github.com/yusukebe/hono-inertia-example>

## Server sketch

```ts
// app/server.ts
import { Hono } from 'hono'
import { inertia } from '@hono/inertia'
import { rootView } from './root-view'

const app = new Hono()

app.use(inertia({ rootView }))

const routes = app
  .get('/', (c) => c.render('Home', { message: 'Hono x Inertia' }))
  .get('/users', (c) => c.render('Users/Index', { users: listUsers() }))
  .get('/users/:id{[0-9]+}', (c) => {
    const id = Number(c.req.param('id'))
    const user = findUser(id)
    if (!user) return c.notFound()
    return c.render('Users/Show', { user })
  })

export default routes
```

`c.render(name, props)` returns either a full HTML document (on the first request) or a JSON Inertia page object (on subsequent client navigations) — `@hono/inertia` looks at the `X-Inertia` request header to decide.

## Client sketch (hono/jsx)

```tsx
// app/client.tsx
import { createInertiaApp } from '@ts-76/inertia-hono-jsx'

createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('./pages/**/*.tsx', { eager: true })
    return pages[`./pages/${name}.tsx`]
  },
})
```

Without a custom `setup`, the adapter mounts `<App />` for you. It uses `hydrateRoot` (from `hono/jsx/dom`) when the root has `data-server-rendered`, and `createRoot` otherwise.

## Page component (hono/jsx)

```tsx
// app/pages/Users/Index.tsx
import { Head, Link, type PageComponent } from '@ts-76/inertia-hono-jsx'

const UsersIndex: PageComponent<'Users/Index'> = ({ users }) => (
  <main>
    <Head title='Users' />
    <h1>Users</h1>
    {users.map((u) => (
      <Link href={`/users/${u.id}`} key={u.id}>
        {u.name}
      </Link>
    ))}
  </main>
)

export default UsersIndex
```

`PageComponent<'Users/Index'>` pulls the prop type from `pages.gen.ts`, which is generated by `@hono/inertia/vite` from the server routes — so a mismatch between server and client props is a build error, not a runtime one.

## Defaults

- Set `jsxImportSource: "hono/jsx"` in `tsconfig.json`.
- Use `vite-ssr-components/hono` (not `/react`) in `root-view.tsx` to inject the Vite client and asset tags.
- Deploy with `@cloudflare/vite-plugin` so `wrangler deploy` ships the SSR worker and the client bundle together.

## Pitfalls

- `@ts-76/inertia-hono-jsx` is community-scoped and "partial" `hono/jsx` support per its README — browser rendering targets `hono/jsx/dom`, but server-side rendering fidelity is not React-grade. Verify SSR output for any non-trivial page.
- Inertia is _not_ an API framework. If a non-Inertia client (mobile, third-party) needs the same data, build a separate JSON endpoint — do not try to reuse the Inertia response shape.
- Asset versioning: make sure `X-Inertia-Version` matches your client bundle hash, or clients silently force a full reload on every navigation.
- On Cloudflare Workers, serve the client bundle via Workers Assets (`@cloudflare/vite-plugin` handles this), not via fetch-to-R2.

## Going deeper

- The Inertia protocol itself — short, worth reading in full: <https://inertiajs.com/>.
- The hono/jsx adapter API surface and typing model: <https://github.com/ts-76/inertia-hono-jsx>.
- The example layout to copy: <https://github.com/yusukebe/hono-inertia-example>.

