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: Build, structure, and ship web apps/sites with the Hono framework — the the user default for new sites and for converting React SPAs to SSR. Use when the user wants to create a Hono app, convert/remake a site in Hono, add SSR + islands on Cloudflare Workers, set up Hono routing/middleware/JSX, choose between hono/jsx (server) and hono/jsx/dom (client), wire the Vite build, or asks "is this Hono", "make it Hono", "Hono SSR", "Hono on Workers", "@hono/jsx", "islands", "hono framework". Loads current, verified Hono API patterns so you leverage the framework fully instead of guessing from memory.4---56# Hono Framework — Full-Leverage Skill78Hono is a small, ultrafast web framework built on **Web Standards** (Request/Response,9`fetch`). It runs on Cloudflare Workers, Deno, Bun, Node, and the edge. For the user's10stack it is the **default for new sites** and the **target for React-SPA → SSR conversions**11(global rule: "Default to Hono SSR for new sites", 2026-05-15).1213> **Ground-truth note:** every API in this skill was verified against `hono.dev` docs via14> Context7 (`/websites/hono_dev`) on 2026-06-04. Before asserting a Hono API that is NOT in15> these references, re-verify against `hono.dev` — do not invent middleware names, import16> paths, or hook names from memory. Hono's API surface is small; if you can't find it in the17> references or live docs, it probably doesn't exist under that name.1819## When to use this skill2021- Creating a new website or web app on Cloudflare Workers (or any edge runtime)22- Converting/remaking an existing React/Vue/SPA site into Hono SSR + islands23- Adding server-rendered HTML, routing, middleware, or an API to an existing Hono worker24- Deciding `hono/jsx` (server SSR) vs `hono/jsx/dom` (client hydration/islands)25- Wiring the Vite build for a Hono Worker with interactive client islands26- Any "make it Hono / is this Hono / Hono SSR / Hono on Workers" request2728## The 90-second mental model29301. **One `app = new Hono()`.** Routes are `app.get/post/...('/path', handler)`. The handler31 gets a single **Context** `c` and returns a `Response` (via `c.text`, `c.json`, `c.html`,32 `c.redirect`, or a raw `Response`).332. **Server JSX is just a function that returns HTML.** `import { ... } from 'hono/jsx'`,34 write components, `return c.html(<Page/>)`. No virtual DOM on the server — it renders to a35 string. This is your SSR.363. **Interactivity = islands.** A small client bundle built with `hono/jsx/dom` mounts into a37 placeholder via `render(<Widget/>, el)`. `useState`/`useEffect` from `hono/jsx` work there.38 A counter island is **2.8 KB brotli vs 47.8 KB for React** — that's the whole point.394. **`c.env` is your bindings**, typed via `new Hono<{ Bindings }>()`. No `process.env` on40 Workers.415. **Static assets** are served by the platform (`assets` in `wrangler.jsonc`), not by route42 handlers. The Worker handles dynamic routes; the SPA/asset fallback handles the rest.4344## Core decision: server vs client JSX (do not mix import sources)4546| | Server (SSR) | Client (island) |47|---|---|---|48| Import | `hono/jsx` | `hono/jsx/dom` |49| Renders to | HTML string (`c.html`) | live DOM (`render(node, el)`) |50| `jsxImportSource` | `hono/jsx` (default) | `hono/jsx/dom` (set per client build) |51| Hooks | for structure only | `useState`, `useEffect`, `useRef`, etc. are live |52| Ship size | 0 KB to the browser | tiny (counter ≈ 2.8 KB brotli) |5354The classic mistake is rendering an interactive component with `hono/jsx` and expecting clicks55to work — server JSX has no event loop in the browser. Interactive = build it into the **client56entry** with `hono/jsx/dom` and mount it.5758## Workflow for any Hono task59601. **Identify the runtime + build.** Cloudflare Workers? Then `wrangler.jsonc` `main` =61 server entry, `assets` for static, and a Vite config that emits BOTH a server bundle and a62 client island bundle. See `references/cloudflare-workers-vite.md`.632. **Lay out routes + SSR pages** with `hono/jsx`. Shared `<Layout>` via the `html` helper or64 the `jsxRenderer` middleware. See `references/ssr-jsx.md`.653. **Carve out islands** — only the genuinely interactive pieces (maps, charts, modals,66 toggles). Each island = a client entry that `render()`s into an SSR'd `<div id="...">`.67 See `references/client-islands.md`.684. **Add middleware** (logger, CORS, cache, secureHeaders, etc.) with `app.use()`. See69 `references/core-routing-middleware.md`.705. **Verify**: `c.html` returns 200 with real markup (curl it), island bundle loads and71 hydrates (check the DOM updates), no `hono/jsx` import in client code and no72 `hono/jsx/dom` in server code.7374## Converting a React SPA → Hono (the big one)7576Read `references/react-spa-to-hono-migration.md`. The short version:7778- **Pages → SSR JSX.** Each React Router route becomes a Hono route returning `c.html(<Page/>)`.79 Static content (text, layout, data-driven markup) renders on the server — instant, SEO-friendly,80 great Lighthouse.81- **Interactive components → islands.** `react-leaflet` map, `d3` force graph, modals, anything82 with `useState`/event handlers → a `hono/jsx/dom` island OR keep the underlying lib83 (Leaflet/D3 are framework-agnostic) and mount it from a client entry. D3 and Leaflet don't84 need React at all.85- **Icons:** `lucide-react` is React-specific. On the server use inline SVG (or `lucide`'s86 framework-agnostic SVG strings); don't import `lucide-react` into `hono/jsx`.87- **Router:** delete `react-router`; routing is server-side in Hono. Client nav can be plain88 `<a href>` (full SSR navigation) or a tiny island if you want SPA-like transitions.89- **Data modules** (plain `.ts` exports) port over unchanged — import them into server pages.90- **Don't big-bang it blindly.** Keep the build green at every step; convert page-by-page and91 curl each route. A working SSR page with a missing island beats a broken全-rewrite.9293## 🛑 "Using Hono" means SSR — a Hono ROUTER in front of static HTML is NOT the default (2026-08-31)9495**The most likely way to think you built a Hono site and not have one:** `new Hono()` for the96API, `secureHeaders()`, typed `Bindings` — all correct — and then the actual page is a97hand-written `public/index.html` served through the `ASSETS` binding. Every Hono symbol is98present. Zero HTML is server-rendered. That is **Hono-as-router**, and it satisfies none of the99reasons the Hono default exists (real markup to crawlers/curl/Wayback, no CSR shell, FCP).100101It passes every check you would naively run: `tsc` clean, `wrangler deploy --dry-run` rc=0,102`import { Hono } from "hono"` right there at the top of the file.103104**The one-command test — run it before claiming a site is Hono:**105```bash106grep -rn 'c\.html(' src/ | grep -v node_modules # ZERO hits => you have a router, not SSR107grep -rln 'hono/jsx' src/ # ZERO files => no server JSX exists108```109Any page a human reads must reach the browser via `c.html(<Page/>)`. `ASSETS.fetch` is for110**assets** — `.js`, `.css`, images, fonts — never for the document.111112**Reference incident (2026-08-31, improvecortland).** Built the Worker in Hono, wrote the portal113as `public/index.html`, and reported the site as built on Hono. The user asked *"did you build114this on hono framework btw like /carmack says to"* — the honest answer was *partially*. The115conversion afterwards was ~30 minutes and had one non-obvious constraint worth stealing:116117**Converting static-HTML → SSR when tests execute the shipped page.** The harnesses regex-118extracted JS out of `public/index.html` and ran it, so deleting that file would have broken119them. The fix is better than the original on both axes: move the executable blocks into a real120`public/foil-core.js`, `<script src>` it from the SSR page, and point the tests at that file.121One source, no HTML parsing in the test, and the tests still execute **the shipped bytes**.122Deleting the old `index.html` then doubles as a free negative control — the suites would fail if123they were still reading it. Keep DOM-touching wiring in a separate `public/ui.js`, because the124harnesses run the core in plain Node with no `document`.125126Also: JSX in a `.ts` file is `error TS1005: '>' expected`. Rename to `.tsx` **and** update127`main` in `wrangler.toml` — the rename alone leaves the config pointing at a file that is gone.128129## Hard rules130131- **A page is not "in Hono" until it renders through `c.html(<Page/>)`.** A Hono router serving132 static HTML via `ASSETS` is the anti-pattern above — grep `c.html(` before you claim SSR.133- **Never mix `hono/jsx` and `hono/jsx/dom` in the same module.** Pick by where the code runs.134- **No `process.env` on Workers** — use `c.env` (typed `Bindings`) or `env(c)` from `hono/adapter`.135- **Don't hand-serve static files from route handlers** when the platform has an `assets` binding.136- **Verify against live docs before using an unfamiliar Hono API** — the framework is small and137 memory-invented middleware/hooks are the #1 failure mode.138- **Client islands must be code-split** so SSR pages ship 0 JS except the island that page needs.139140## Reference files141142| File | Use when |143|---|---|144| `references/core-routing-middleware.md` | Routing, Context (`c`), middleware, bindings, error handling |145| `references/ssr-jsx.md` | Server JSX, `c.html`, `html` helper, `jsxRenderer`, layouts, streaming |146| `references/client-islands.md` | `hono/jsx/dom`, `render()`, hooks, hydration, mounting D3/Leaflet |147| `references/cloudflare-workers-vite.md` | `wrangler.jsonc`, dual client/server Vite build, assets, deploy |148| `references/react-spa-to-hono-migration.md` | Step-by-step React-SPA → Hono conversion playbook |149150All import paths and patterns in these files were verified against `hono.dev` on 2026-06-04.