Honox
HonoX - Hono based meta framework
Install / Use
/learn @honojs/HonoxREADME
HonoX
HonoX is a simple and fast meta-framework for creating full-stack websites or Web APIs - (formerly Sonik). It stands on the shoulders of giants; built on Hono, Vite, and UI libraries.
Note: HonoX is currently in the "alpha stage". Breaking changes are introduced within the same major version, following semantic versioning for zerover.
Features
- File-based routing - You can create a large application like Next.js.
- Fast SSR - Rendering is ultra-fast thanks to Hono.
- BYOR - You can bring your own renderer, not only one using hono/jsx.
- Islands hydration - If you want interactions, create an island. JavaScript is hydrated only for it.
- Middleware - It works as Hono, so you can use a lot of Hono's middleware.
Installing
You can install the honox package from the npm.
npm install hono honox
Starter template
If you are starting a new HonoX project, use the hono-create command. Run the following and choose x-basic (use the arrow keys to find the option).
npm create hono@latest
Get Started - Basic
Let's create a basic HonoX application using hono/jsx as a renderer. This application has no client JavaScript and renders JSX on the server side.
Project Structure
Below is a typical project structure for a HonoX application.
.
├── app
│ ├── global.d.ts // global type definitions
│ ├── routes
│ │ ├── _404.tsx // not found page
│ │ ├── _error.tsx // error page
│ │ ├── _renderer.tsx // renderer definition
│ │ ├── merch
│ │ │ └── [...slug].tsx // matches `/merch/:category`, `/merch/:category/:item`, `/merch/:category/:item/:variant`
│ │ ├── about
│ │ │ └── [name].tsx // matches `/about/:name`
│ │ ├── blog
│ │ │ ├── index.tsx // matches /blog
│ │ │ └── (content)
│ │ │ ├── _renderer.tsx // renderer definition for routes inside this directory
│ │ │ └── [name].tsx // matches `/blog/:name`
│ │ └── index.tsx // matches `/`
│ └── server.ts // server entry file
├── package.json
├── tsconfig.json
└── vite.config.ts
vite.config.ts
The minimum Vite setup for development is as follows:
import { defineConfig } from 'vite'
import honox from 'honox/vite'
export default defineConfig({
plugins: [honox()],
})
Server Entry File
A server entry file is required. The file should be placed at app/server.ts. This file is first called by the Vite during the development or build phase.
In the entry file, simply initialize your app using the createApp() function. app will be an instance of Hono, so you can use Hono's middleware and the showRoutes() in hono/dev.
// app/server.ts
import { createApp } from 'honox/server'
import { showRoutes } from 'hono/dev'
const app = createApp()
showRoutes(app)
export default app
Routes
There are three ways to define routes.
1. createRoute()
Each route should return an array of Handler | MiddlewareHandler. createRoute() is a helper function to return it. You can write a route for a GET request with default export.
// app/routes/index.tsx
// `createRoute()` helps you create handlers
import { createRoute } from 'honox/factory'
export default createRoute((c) => {
return c.render(
<div>
<h1>Hello!</h1>
</div>
)
})
You can also handle methods other than GET by export POST, PUT, and DELETE.
// app/routes/index.tsx
import { createRoute } from 'honox/factory'
import { getCookie, setCookie } from 'hono/cookie'
export const POST = createRoute(async (c) => {
const { name } = await c.req.parseBody<{ name: string }>()
setCookie(c, 'name', name)
return c.redirect('/')
})
export default createRoute((c) => {
const name = getCookie(c, 'name') ?? 'no name'
return c.render(
<div>
<h1>Hello, {name}!</h1>
<form method='POST'>
<input type='text' name='name' placeholder='name' />
<input type='submit' />
</form>
</div>
)
})
2. Using a Hono instance
You can create API endpoints by exporting an instance of the Hono object.
// app/routes/about/index.ts
import { Hono } from 'hono'
const app = new Hono()
// matches `/about/:name`
app.get('/:name', (c) => {
const name = c.req.param('name')
return c.json({
'your name is': name,
})
})
export default app
3. Just return JSX
Or simply, you can just return JSX.
// app/routes/index.tsx
export default function Home(_c: Context) {
return <h1>Welcome!</h1>
}
Renderer
Define your renderer - the middleware that does c.setRender() - by writing it in _renderer.tsx.
Before writing _renderer.tsx, write the Renderer type definition in global.d.ts.
// app/global.d.ts
import type {} from 'hono'
type Head = {
title?: string
}
declare module 'hono' {
interface ContextRenderer {
(content: string | Promise<string>, head?: Head): Response | Promise<Response>
}
}
The JSX Renderer middleware allows you to create a Renderer as follows:
// app/routes/_renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
export default jsxRenderer(({ children, title }) => {
return (
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
{title ? <title>{title}</title> : <></>}
</head>
<body>{children}</body>
</html>
)
})
The _renderer.tsx is applied under each directory, and the app/routes/posts/_renderer.tsx is applied in app/routes/posts/*.
Not Found page
You can write a custom Not Found page in _404.tsx.
// app/routes/_404.tsx
import { NotFoundHandler } from 'hono'
const handler: NotFoundHandler = (c) => {
return c.render(<h1>Sorry, Not Found...</h1>)
}
export default handler
Error Page
You can write a custom Error page in _error.tsx.
// app/routes/_error.tsx
import { ErrorHandler } from 'hono'
const handler: ErrorHandler = (e, c) => {
return c.render(<h1>Error! {e.message}</h1>)
}
export default handler
Get Started - with Client
Let's create an application that includes a client side. Here, we will use hono/jsx/dom.
Project Structure
Below is the project structure of a minimal application including a client side:
.
├── app
│ ├── client.ts // client entry file
│ ├── global.d.ts
│ ├── islands
│ │ └── counter.tsx // island component
│ ├── routes
│ │ ├── _renderer.tsx
│ │ └── index.tsx
│ └── server.ts
├── package.json
├── tsconfig.json
└── vite.config.ts
Renderer
This is a _renderer.tsx, which will load the /app/client.ts entry file for the client. It will load the JavaScript file for production according to the variable import.meta.env.PROD. And renders the inside of <HasIslands /> if there are islands on that page.
// app/routes/_renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { HasIslands } from 'honox/server'
export default jsxRenderer(({ children }) => {
return (
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
{import.meta.env.PROD ? (
<HasIslands>
<script type='module' src='/static/client.js'></script>
</HasIslands>
) : (
<script type='module' src='/app/client.ts'></script>
)}
</head>
<body>{children}</body>
</html>
)
})
If you have a manifest file in dist/.vite/manifest.json, you can easily write it using <Script />.
// app/routes/_renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { Script } from 'honox/server'
export default jsxRenderer(({ children }) => {
return (
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<Script src='/app/client.ts' />
</head>
<body>{children}</body>
</html>
)
})
Note: Since <HasIslands /> can slightly affect build performance when used, it is recommended that you do not use it in the development environment, but only at build time. <Script /> does not cause performance degradation during development, so it's better to use it.
nonce Attribute
If you want to add a nonce attribute to <Script /> or <script /> element, you can use Security Headers Middleware.
Define the middleware:
// app/routes/_middleware.ts
import { createRoute } from 'honox/factory'
import { secureHeaders, NONCE } from 'hono/secure-headers'
export default createRoute(
secureHeaders({
contentSecurityPolicy: {
scriptSrc: [NONCE],
},
})
)
You can get the nonce value with c.get('secureHeadersNonce'):
// app/routes/_renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { Script } from 'honox/server'
export default jsxRenderer(({ children }, c) => {
return (
<html lang='en'>
<head>
<Script src='/app/client.ts' async nonce={c.get('secureHeadersNonce')} />
</head>
<body>{children}</body>
</html>
)
})
Client Entry File
A client-side entry file should be in app/client.ts. Simply, write createClient().
// app/client.ts
import { createClient } from 'honox/client'
createClient()
Interactions
If you want to add interactions to your page, create Island components. Island components should be:
- Placed under
app/islandsdirectory or named with$prefix like$componentName.tsx. - It should be exported as a
defaultor a proper component name that uses camel case but does not contain_and is not all uppercase.
For example, you can write an interactiv
