mcpField
H
hono-inertia

hono-inertia

github.com/yusukebeDevelopment3699 installsUpdated 23w ago
Repository

About hono-inertia

A guide for TypeScript developers building SPA-style apps with Hono, Inertia.js, and hono/jsx in one stack, without a separate frontend or JSON API layer. It covers typed page props and Vite and Cloudflare Workers deployment setup.

Key features

  1. 01Return Inertia pages from Hono
  2. 02Build client pages with hono/jsx
  3. 03Generate page prop types
  4. 04Bundle and SSR with Vite
  5. 05Configure Cloudflare Workers deployment

Use cases

  1. 01Add client-side state to a Hono app
  2. 02Build optimistic UI and rich forms
  3. 03Avoid maintaining a separate SPA frontend stack

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/yusukebe/skills --skill hono-inertia

After installing, type/hono-inertiain Claude Code to confirm it loaded.

Details

From the project README

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

// 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)

// 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)

// 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

Comments

Markdown supported