Inertia.js v3 server-side adapter for Hono.
pnpm add @brachkow/hono-inertiaimport { Hono } from 'hono'
import { inertia } from '@brachkow/hono-inertia'
import type { InertiaEnv } from '@brachkow/hono-inertia'
const app = new Hono<InertiaEnv>()
app.use(
inertia({
version: '1.0',
render: (page) =>
`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="module" src="/src/main.ts"></script>
</head>
<body>
<div id="app"></div>
<script type="application/json" id="page">${JSON.stringify(page)}</script>
</body>
</html>`,
}),
)
app.get('/', (c) => {
return c.var.inertia.render('Home', { title: 'Hello' })
})
export default appinertia({
// Asset version — string or function (sync/async)
version: '1.0',
version: () => readFileSync('dist/manifest.json', 'utf-8'),
// HTML render function — receives page object, view data, and optional SSR result
render: (page, viewData, ssr) => {
if (ssr) {
return `<html><head>${ssr.head}</head><body>${ssr.body}</body></html>`
}
return `<html><body><div id="app"></div><script type="application/json" id="page">${JSON.stringify(page)}</script></body></html>`
},
// Global shared props — merged into every response
share: (c) => ({
auth: { user: getUser(c) },
}),
// SSR — optional, posts to an Inertia SSR server
ssr: {
url: 'http://127.0.0.1:13714',
enabled: true,
},
})app.get('/users', (c) => {
return c.var.inertia.render('Users/Index', {
users: await db.users.findMany(),
})
})Props can be lazy functions — they're only called when the prop is actually included in the response:
app.get('/dashboard', (c) => {
return c.var.inertia.render('Dashboard', {
stats: async () => computeExpensiveStats(),
users: () => db.users.findMany(),
})
})Global shared props via config:
inertia({
share: (c) => ({
auth: { user: getUser(c) },
flash: getFlash(c),
}),
})Per-request shared props via middleware:
app.use(async (c, next) => {
c.var.inertia.share({ notifications: getNotifications(c) })
await next()
})Render props override shared props. Shared props override config-level props.
Always included in every response, including partial reloads:
import { always } from '@brachkow/hono-inertia'
c.var.inertia.render('Dashboard', {
auth: always({ user: currentUser }),
})Excluded from initial visits. Only included when explicitly requested in a partial reload:
import { optional } from '@brachkow/hono-inertia'
c.var.inertia.render('Users/Index', {
permissions: optional(() => fetchPermissions()),
})Excluded from the initial response. The client automatically fetches them after mount:
import { deferred } from '@brachkow/hono-inertia'
c.var.inertia.render('Dashboard', {
stats: deferred(() => computeStats()),
comments: deferred(() => fetchComments(), 'sidebar'),
likes: deferred(() => fetchLikes(), 'sidebar'),
})Client appends/prepends/deep-merges new data instead of replacing. Useful for infinite scroll:
import { merge, prepend, deepMerge } from '@brachkow/hono-inertia'
c.var.inertia.render('Feed', {
posts: merge(() => fetchPosts(page)),
newPosts: prepend(() => fetchNewPosts()),
settings: deepMerge(() => fetchSettings()),
})Use .setMatchOn(field) for array matching:
c.var.inertia.render('Feed', {
posts: merge(() => fetchPosts()).setMatchOn('id'),
})Resolved once, then cached by the client across navigations:
import { once } from '@brachkow/hono-inertia'
c.var.inertia.render('Pricing', {
plans: once(() => fetchPlans()),
config: once(() => fetchConfig(), 'app-config', 86400),
})Prop types can be combined:
// Deferred + merge + once
deferred(() => fetchFeed(), 'main').merge().once()
// Deferred + prepend
deferred(() => fetchNew(), 'top').prepend()
// Optional + once
optional(() => fetchExpensiveData()).once('cache-key', 3600)
// Deep merge with match key
deepMerge(() => fetchItems()).setMatchOn('id')Redirect to a non-Inertia URL (returns 409 for Inertia requests, 302 for regular):
app.get('/download', (c) => {
return c.var.inertia.location('https://example.com/file.pdf')
})Encrypt page state in browser history to prevent back-button data exposure:
app.get('/dashboard', (c) => {
c.var.inertia.encryptHistory()
return c.var.inertia.render('Dashboard', { secret: 'data' })
})Clear encrypted history (e.g., on logout):
app.post('/logout', (c) => {
c.var.inertia.clearHistory()
return c.redirect('/login')
})Pass data to the render function without exposing it to the client-side JavaScript:
app.get('/users', (c) => {
c.var.inertia.viewData({ metaTitle: 'User List' })
return c.var.inertia.render('Users/Index', { users })
})Access it in your render function:
render: (page, viewData) => `
<html>
<head><title>${viewData.metaTitle}</title></head>
<body><div id="app"></div><script type="application/json" id="page">${JSON.stringify(page)}</script></body>
</html>
`Configure an Inertia SSR server (works with @inertiajs/vue3/server, @inertiajs/react/server, @inertiajs/svelte/server):
inertia({
ssr: {
url: 'http://127.0.0.1:13714', // default
enabled: true,
},
render: (page, viewData, ssr) => {
if (ssr) {
return `<html><head>${ssr.head}</head><body>${ssr.body}</body></html>`
}
return `<html><body><div id="app"></div><script type="application/json" id="page">${JSON.stringify(page)}</script></body></html>`
},
})Falls back to client-side rendering if the SSR server is unavailable.
Use InertiaEnv for typed c.var.inertia access:
import type { InertiaEnv } from '@brachkow/hono-inertia'
const app = new Hono<InertiaEnv>()Compose with your own env types:
type AppEnv = InertiaEnv & {
Variables: { db: Database }
}
const app = new Hono<AppEnv>()MIT