Hono Framework — Full-Leverage Skill
Hono is a small, ultrafast web framework built on Web Standards (Request/Response,
fetch). It runs on Cloudflare Workers, Deno, Bun, Node, and the edge. For the user's
stack it is the default for new sites and the target for React-SPA → SSR conversions
(global rule: "Default to Hono SSR for new sites", 2026-05-15).
Ground-truth note: every API in this skill was verified against hono.dev docs via
Context7 (/websites/hono_dev) on 2026-06-04. Before asserting a Hono API that is NOT in
these references, re-verify against hono.dev — do not invent middleware names, import
paths, or hook names from memory. Hono's API surface is small; if you can't find it in the
references or live docs, it probably doesn't exist under that name.
When to use this skill
- Creating a new website or web app on Cloudflare Workers (or any edge runtime)
- Converting/remaking an existing React/Vue/SPA site into Hono SSR + islands
- Adding server-rendered HTML, routing, middleware, or an API to an existing Hono worker
- Deciding
hono/jsx (server SSR) vs hono/jsx/dom (client hydration/islands)
- Wiring the Vite build for a Hono Worker with interactive client islands
- Any "make it Hono / is this Hono / Hono SSR / Hono on Workers" request
The 90-second mental model
- One
app = new Hono(). Routes are app.get/post/...('/path', handler). The handler
gets a single Context c and returns a Response (via c.text, c.json, c.html,
c.redirect, or a raw Response).
- Server JSX is just a function that returns HTML.
import { ... } from 'hono/jsx',
write components, return c.html(<Page/>). No virtual DOM on the server — it renders to a
string. This is your SSR.
- Interactivity = islands. A small client bundle built with
hono/jsx/dom mounts into a
placeholder via render(<Widget/>, el). useState/useEffect from hono/jsx work there.
A counter island is 2.8 KB brotli vs 47.8 KB for React — that's the whole point.
c.env is your bindings, typed via new Hono<{ Bindings }>(). No process.env on
Workers.
- Static assets are served by the platform (
assets in wrangler.jsonc), not by route
handlers. The Worker handles dynamic routes; the SPA/asset fallback handles the rest.
Core decision: server vs client JSX (do not mix import sources)
|
Server (SSR) |
Client (island) |
| Import |
hono/jsx |
hono/jsx/dom |
| Renders to |
HTML string (c.html) |
live DOM (render(node, el)) |
jsxImportSource |
hono/jsx (default) |
hono/jsx/dom (set per client build) |
| Hooks |
for structure only |
useState, useEffect, useRef, etc. are live |
| Ship size |
0 KB to the browser |
tiny (counter ≈ 2.8 KB brotli) |
The classic mistake is rendering an interactive component with hono/jsx and expecting clicks
to work — server JSX has no event loop in the browser. Interactive = build it into the client
entry with hono/jsx/dom and mount it.
Workflow for any Hono task
- Identify the runtime + build. Cloudflare Workers? Then
wrangler.jsonc main =
server entry, assets for static, and a Vite config that emits BOTH a server bundle and a
client island bundle. See references/cloudflare-workers-vite.md.
- Lay out routes + SSR pages with
hono/jsx. Shared <Layout> via the html helper or
the jsxRenderer middleware. See references/ssr-jsx.md.
- Carve out islands — only the genuinely interactive pieces (maps, charts, modals,
toggles). Each island = a client entry that
render()s into an SSR'd <div id="...">.
See references/client-islands.md.
- Add middleware (logger, CORS, cache, secureHeaders, etc.) with
app.use(). See
references/core-routing-middleware.md.
- Verify:
c.html returns 200 with real markup (curl it), island bundle loads and
hydrates (check the DOM updates), no hono/jsx import in client code and no
hono/jsx/dom in server code.
Converting a React SPA → Hono (the big one)
Read references/react-spa-to-hono-migration.md. The short version:
- Pages → SSR JSX. Each React Router route becomes a Hono route returning
c.html(<Page/>).
Static content (text, layout, data-driven markup) renders on the server — instant, SEO-friendly,
great Lighthouse.
- Interactive components → islands.
react-leaflet map, d3 force graph, modals, anything
with useState/event handlers → a hono/jsx/dom island OR keep the underlying lib
(Leaflet/D3 are framework-agnostic) and mount it from a client entry. D3 and Leaflet don't
need React at all.
- Icons:
lucide-react is React-specific. On the server use inline SVG (or lucide's
framework-agnostic SVG strings); don't import lucide-react into hono/jsx.
- Router: delete
react-router; routing is server-side in Hono. Client nav can be plain
<a href> (full SSR navigation) or a tiny island if you want SPA-like transitions.
- Data modules (plain
.ts exports) port over unchanged — import them into server pages.
- Don't big-bang it blindly. Keep the build green at every step; convert page-by-page and
curl each route. A working SSR page with a missing island beats a broken全-rewrite.
🛑 "Using Hono" means SSR — a Hono ROUTER in front of static HTML is NOT the default (2026-08-31)
The most likely way to think you built a Hono site and not have one: new Hono() for the
API, secureHeaders(), typed Bindings — all correct — and then the actual page is a
hand-written public/index.html served through the ASSETS binding. Every Hono symbol is
present. Zero HTML is server-rendered. That is Hono-as-router, and it satisfies none of the
reasons the Hono default exists (real markup to crawlers/curl/Wayback, no CSR shell, FCP).
It passes every check you would naively run: tsc clean, wrangler deploy --dry-run rc=0,
import { Hono } from "hono" right there at the top of the file.
The one-command test — run it before claiming a site is Hono:
grep -rn 'c\.html(' src/ | grep -v node_modules # ZERO hits => you have a router, not SSR
grep -rln 'hono/jsx' src/ # ZERO files => no server JSX exists
Any page a human reads must reach the browser via c.html(<Page/>). ASSETS.fetch is for
assets — .js, .css, images, fonts — never for the document.
Reference incident (2026-08-31, improvecortland). Built the Worker in Hono, wrote the portal
as public/index.html, and reported the site as built on Hono. The user asked "did you build
this on hono framework btw like /carmack says to" — the honest answer was partially. The
conversion afterwards was ~30 minutes and had one non-obvious constraint worth stealing:
Converting static-HTML → SSR when tests execute the shipped page. The harnesses regex-
extracted JS out of public/index.html and ran it, so deleting that file would have broken
them. The fix is better than the original on both axes: move the executable blocks into a real
public/foil-core.js, <script src> it from the SSR page, and point the tests at that file.
One source, no HTML parsing in the test, and the tests still execute the shipped bytes.
Deleting the old index.html then doubles as a free negative control — the suites would fail if
they were still reading it. Keep DOM-touching wiring in a separate public/ui.js, because the
harnesses run the core in plain Node with no document.
Also: JSX in a .ts file is error TS1005: '>' expected. Rename to .tsx and update
main in wrangler.toml — the rename alone leaves the config pointing at a file that is gone.
Hard rules
- A page is not "in Hono" until it renders through
c.html(<Page/>). A Hono router serving
static HTML via ASSETS is the anti-pattern above — grep c.html( before you claim SSR.
- Never mix
hono/jsx and hono/jsx/dom in the same module. Pick by where the code runs.
- No
process.env on Workers — use c.env (typed Bindings) or env(c) from hono/adapter.
- Don't hand-serve static files from route handlers when the platform has an
assets binding.
- Verify against live docs before using an unfamiliar Hono API — the framework is small and
memory-invented middleware/hooks are the #1 failure mode.
- Client islands must be code-split so SSR pages ship 0 JS except the island that page needs.
Reference files
| File |
Use when |
references/core-routing-middleware.md |
Routing, Context (c), middleware, bindings, error handling |
references/ssr-jsx.md |
Server JSX, c.html, html helper, jsxRenderer, layouts, streaming |
references/client-islands.md |
hono/jsx/dom, render(), hooks, hydration, mounting D3/Leaflet |
references/cloudflare-workers-vite.md |
wrangler.jsonc, dual client/server Vite build, assets, deploy |
references/react-spa-to-hono-migration.md |
Step-by-step React-SPA → Hono conversion playbook |
All import paths and patterns in these files were verified against hono.dev on 2026-06-04.
1---2name: hono3description: >-4 Build, structure, and ship web apps/sites with the Hono framework — the5 the user default for new sites and for converting React SPAs to SSR. Use when6 the user wants to create a Hono app, convert/remake a site in Hono, add SSR +7 islands on Cloudflare Workers, set up Hono routing/middleware/JSX, choose8 between hono/jsx (server) and hono/jsx/dom (client), wire the Vite build, or9 asks "is this Hono", "make it Hono", "Hono SSR", "Hono on Workers", "@hono/jsx",10 "islands", "hono framework". Loads current, verified Hono API patterns so you11 leverage the framework fully instead of guessing from memory.12---1314# Hono Framework — Full-Leverage Skill1516Hono is a small, ultrafast web framework built on **Web Standards** (Request/Response,17`fetch`). It runs on Cloudflare Workers, Deno, Bun, Node, and the edge. For the user's18stack it is the **default for new sites** and the **target for React-SPA → SSR conversions**19(global rule: "Default to Hono SSR for new sites", 2026-05-15).2021> **Ground-truth note:** every API in this skill was verified against `hono.dev` docs via22> Context7 (`/websites/hono_dev`) on 2026-06-04. Before asserting a Hono API that is NOT in23> these references, re-verify against `hono.dev` — do not invent middleware names, import24> paths, or hook names from memory. Hono's API surface is small; if you can't find it in the25> references or live docs, it probably doesn't exist under that name.2627## When to use this skill2829- Creating a new website or web app on Cloudflare Workers (or any edge runtime)30- Converting/remaking an existing React/Vue/SPA site into Hono SSR + islands31- Adding server-rendered HTML, routing, middleware, or an API to an existing Hono worker32- Deciding `hono/jsx` (server SSR) vs `hono/jsx/dom` (client hydration/islands)33- Wiring the Vite build for a Hono Worker with interactive client islands34- Any "make it Hono / is this Hono / Hono SSR / Hono on Workers" request3536## The 90-second mental model37381. **One `app = new Hono()`.** Routes are `app.get/post/...('/path', handler)`. The handler39 gets a single **Context** `c` and returns a `Response` (via `c.text`, `c.json`, `c.html`,40 `c.redirect`, or a raw `Response`).412. **Server JSX is just a function that returns HTML.** `import { ... } from 'hono/jsx'`,42 write components, `return c.html(<Page/>)`. No virtual DOM on the server — it renders to a43 string. This is your SSR.443. **Interactivity = islands.** A small client bundle built with `hono/jsx/dom` mounts into a45 placeholder via `render(<Widget/>, el)`. `useState`/`useEffect` from `hono/jsx` work there.46 A counter island is **2.8 KB brotli vs 47.8 KB for React** — that's the whole point.474. **`c.env` is your bindings**, typed via `new Hono<{ Bindings }>()`. No `process.env` on48 Workers.495. **Static assets** are served by the platform (`assets` in `wrangler.jsonc`), not by route50 handlers. The Worker handles dynamic routes; the SPA/asset fallback handles the rest.5152## Core decision: server vs client JSX (do not mix import sources)5354| | Server (SSR) | Client (island) |55|---|---|---|56| Import | `hono/jsx` | `hono/jsx/dom` |57| Renders to | HTML string (`c.html`) | live DOM (`render(node, el)`) |58| `jsxImportSource` | `hono/jsx` (default) | `hono/jsx/dom` (set per client build) |59| Hooks | for structure only | `useState`, `useEffect`, `useRef`, etc. are live |60| Ship size | 0 KB to the browser | tiny (counter ≈ 2.8 KB brotli) |6162The classic mistake is rendering an interactive component with `hono/jsx` and expecting clicks63to work — server JSX has no event loop in the browser. Interactive = build it into the **client64entry** with `hono/jsx/dom` and mount it.6566## Workflow for any Hono task67681. **Identify the runtime + build.** Cloudflare Workers? Then `wrangler.jsonc` `main` =69 server entry, `assets` for static, and a Vite config that emits BOTH a server bundle and a70 client island bundle. See `references/cloudflare-workers-vite.md`.712. **Lay out routes + SSR pages** with `hono/jsx`. Shared `<Layout>` via the `html` helper or72 the `jsxRenderer` middleware. See `references/ssr-jsx.md`.733. **Carve out islands** — only the genuinely interactive pieces (maps, charts, modals,74 toggles). Each island = a client entry that `render()`s into an SSR'd `<div id="...">`.75 See `references/client-islands.md`.764. **Add middleware** (logger, CORS, cache, secureHeaders, etc.) with `app.use()`. See77 `references/core-routing-middleware.md`.785. **Verify**: `c.html` returns 200 with real markup (curl it), island bundle loads and79 hydrates (check the DOM updates), no `hono/jsx` import in client code and no80 `hono/jsx/dom` in server code.8182## Converting a React SPA → Hono (the big one)8384Read `references/react-spa-to-hono-migration.md`. The short version:8586- **Pages → SSR JSX.** Each React Router route becomes a Hono route returning `c.html(<Page/>)`.87 Static content (text, layout, data-driven markup) renders on the server — instant, SEO-friendly,88 great Lighthouse.89- **Interactive components → islands.** `react-leaflet` map, `d3` force graph, modals, anything90 with `useState`/event handlers → a `hono/jsx/dom` island OR keep the underlying lib91 (Leaflet/D3 are framework-agnostic) and mount it from a client entry. D3 and Leaflet don't92 need React at all.93- **Icons:** `lucide-react` is React-specific. On the server use inline SVG (or `lucide`'s94 framework-agnostic SVG strings); don't import `lucide-react` into `hono/jsx`.95- **Router:** delete `react-router`; routing is server-side in Hono. Client nav can be plain96 `<a href>` (full SSR navigation) or a tiny island if you want SPA-like transitions.97- **Data modules** (plain `.ts` exports) port over unchanged — import them into server pages.98- **Don't big-bang it blindly.** Keep the build green at every step; convert page-by-page and99 curl each route. A working SSR page with a missing island beats a broken全-rewrite.100101## 🛑 "Using Hono" means SSR — a Hono ROUTER in front of static HTML is NOT the default (2026-08-31)102103**The most likely way to think you built a Hono site and not have one:** `new Hono()` for the104API, `secureHeaders()`, typed `Bindings` — all correct — and then the actual page is a105hand-written `public/index.html` served through the `ASSETS` binding. Every Hono symbol is106present. Zero HTML is server-rendered. That is **Hono-as-router**, and it satisfies none of the107reasons the Hono default exists (real markup to crawlers/curl/Wayback, no CSR shell, FCP).108109It passes every check you would naively run: `tsc` clean, `wrangler deploy --dry-run` rc=0,110`import { Hono } from "hono"` right there at the top of the file.111112**The one-command test — run it before claiming a site is Hono:**113```bash114grep -rn 'c\.html(' src/ | grep -v node_modules # ZERO hits => you have a router, not SSR115grep -rln 'hono/jsx' src/ # ZERO files => no server JSX exists116```117Any page a human reads must reach the browser via `c.html(<Page/>)`. `ASSETS.fetch` is for118**assets** — `.js`, `.css`, images, fonts — never for the document.119120**Reference incident (2026-08-31, improvecortland).** Built the Worker in Hono, wrote the portal121as `public/index.html`, and reported the site as built on Hono. The user asked *"did you build122this on hono framework btw like /carmack says to"* — the honest answer was *partially*. The123conversion afterwards was ~30 minutes and had one non-obvious constraint worth stealing:124125**Converting static-HTML → SSR when tests execute the shipped page.** The harnesses regex-126extracted JS out of `public/index.html` and ran it, so deleting that file would have broken127them. The fix is better than the original on both axes: move the executable blocks into a real128`public/foil-core.js`, `<script src>` it from the SSR page, and point the tests at that file.129One source, no HTML parsing in the test, and the tests still execute **the shipped bytes**.130Deleting the old `index.html` then doubles as a free negative control — the suites would fail if131they were still reading it. Keep DOM-touching wiring in a separate `public/ui.js`, because the132harnesses run the core in plain Node with no `document`.133134Also: JSX in a `.ts` file is `error TS1005: '>' expected`. Rename to `.tsx` **and** update135`main` in `wrangler.toml` — the rename alone leaves the config pointing at a file that is gone.136137## Hard rules138139- **A page is not "in Hono" until it renders through `c.html(<Page/>)`.** A Hono router serving140 static HTML via `ASSETS` is the anti-pattern above — grep `c.html(` before you claim SSR.141- **Never mix `hono/jsx` and `hono/jsx/dom` in the same module.** Pick by where the code runs.142- **No `process.env` on Workers** — use `c.env` (typed `Bindings`) or `env(c)` from `hono/adapter`.143- **Don't hand-serve static files from route handlers** when the platform has an `assets` binding.144- **Verify against live docs before using an unfamiliar Hono API** — the framework is small and145 memory-invented middleware/hooks are the #1 failure mode.146- **Client islands must be code-split** so SSR pages ship 0 JS except the island that page needs.147148## Reference files149150| File | Use when |151|---|---|152| `references/core-routing-middleware.md` | Routing, Context (`c`), middleware, bindings, error handling |153| `references/ssr-jsx.md` | Server JSX, `c.html`, `html` helper, `jsxRenderer`, layouts, streaming |154| `references/client-islands.md` | `hono/jsx/dom`, `render()`, hooks, hydration, mounting D3/Leaflet |155| `references/cloudflare-workers-vite.md` | `wrangler.jsonc`, dual client/server Vite build, assets, deploy |156| `references/react-spa-to-hono-migration.md` | Step-by-step React-SPA → Hono conversion playbook |157158All import paths and patterns in these files were verified against `hono.dev` on 2026-06-04.