Loading...
Loading...
Use when building UI for a Hono app with hono/jsx — pages, layouts, forms, and interactive parts — server-rendered with jsxRenderer, developed and built with Vite, with client components via hono/jsx/dom. TRIGGER when tsconfig has jsxImportSource "hono/jsx", code imports 'hono/jsx', 'hono/jsx/dom', 'hono/jsx-renderer' or 'vite-ssr-components', or the user asks for a page, screen, layout, or form in a Hono project. Never reach for React.
npx skill4agent add honojs/skills hono-jsxhono/jsxhonoc.html()hono/jsxreactreact-dom@types/react@vitejs/plugin-reactreacttsconfig.json"jsx": "react-jsx""jsxImportSource": "hono/jsx"react-jsxasyncawaithono/jsx/domclassclassNamevite-ssr-componentsvite-ssr-components/hono/reactcreate-honocloudflare-workers+vitesrc/
index.tsx # Hono app, routes
renderer.tsx # jsxRenderer layout: <html>, <head>, Vite assets
client.ts # optional: browser-side code
style.css
public/ # static files served as-is
vite.config.ts
wrangler.jsonc// package.json (scripts)
{
"dev": "vite",
"build": "vite build",
"preview": "$npm_execpath run build && vite preview",
"deploy": "$npm_execpath run build && wrangler deploy",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
}// vite.config.ts
import { cloudflare } from '@cloudflare/vite-plugin'
import { defineConfig } from 'vite'
import ssrPlugin from 'vite-ssr-components/plugin'
export default defineConfig({
plugins: [cloudflare(), ssrPlugin()],
})// src/renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { Link, Script, ViteClient } from 'vite-ssr-components/hono'
export const renderer = jsxRenderer(({ children }) => {
return (
<html>
<head>
<ViteClient />
<Link href="/src/style.css" rel="stylesheet" />
<Script src="/src/client.ts" />
</head>
<body>{children}</body>
</html>
)
})// src/index.tsx
import { Hono } from 'hono'
import { renderer } from './renderer'
const app = new Hono<{ Bindings: CloudflareBindings }>()
app.use(renderer)
app.get('/', (c) => {
return c.render(<h1>Hello!</h1>)
})
export default appvite-ssr-components<ViteClient /><Script><Link>rollupOptions.input<Script><Link>componentsnpm run devc.envnpm run cf-typegenjsxRendererc.render()<title><meta><link><head><title>ContextRendererc.render()declare module 'hono' {
interface ContextRenderer {
(content: string | Promise<string>, props: { title: string }): Response
}
}
export const renderer = jsxRenderer(({ children, title }) => (
<html>
<head>
<title>{title}</title>
</head>
<body>{children}</body>
</html>
))
app.get('/about', (c) => c.render(<About />, { title: 'About' }))jsxRendererLayoutconst blog = new Hono()
blog.use(
jsxRenderer(({ children, Layout }) => (
<Layout>
<nav>Blog</nav>
{children}
</Layout>
))
)
app.route('/blog', blog)useRequestContext()hono/jsx-renderercc.reqc.envc.varimport { useRequestContext } from 'hono/jsx-renderer'
const UserMenu = async () => {
const c = useRequestContext<{ Bindings: CloudflareBindings }>()
const user = await c.env.KV.get('user')
return <span>{user}</span>
}src/components/FC<Props>PropsWithChildren<Props>hono/jsxapp.get('/todos', async (c) => {
const todos = await listTodos(c.env)
return c.render(
<>
<form method="post" action="/todos">
<input name="title" required />
<button type="submit">Add</button>
</form>
<ul>{todos.map((t) => <li>{t.title}</li>)}</ul>
</>
)
})
app.post('/todos', async (c) => {
const body = await c.req.parseBody()
await addTodo(c.env, String(body.title))
return c.redirect('/todos')
})validator('form', ...)honohono/jsx/domsrc/client.ts<Script src="/src/client.ts" />// src/client.ts
/// <reference lib="dom" />
document.querySelector('#menu-toggle')?.addEventListener('click', () => {
document.querySelector('#menu')?.classList.toggle('open')
})hono/jsx/domrender()// src/client.tsx
/// <reference lib="dom" />
import { useState } from 'hono/jsx'
import { render } from 'hono/jsx/dom'
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
render(<Counter />, document.getElementById('counter')!)useStateuseEffectuseRefhono/jsxfetchhchono/clienttsconfig.jsonlib: ["ESNext"]DOMdocumenttsctsconfig.json/// <reference lib="dom" />libtscsrc/style.css<Link href="/src/style.css" rel="stylesheet" />hono/csscss<Style /><head>import { css, Style } from 'hono/css'
const card = css`
padding: 1rem;
&:hover { background: #eee; }
`
// <head><Style /></head> ... <div class={card}>...</div>stream: trueSuspenseimport { Suspense } from 'hono/jsx/streaming'
export const renderer = jsxRenderer(({ children }) => <html><body>{children}</body></html>, { stream: true })
app.get('/', (c) =>
c.render(
<Suspense fallback={<p>Loading...</p>}>
<SlowList />
</Suspense>
)
)SuspenseErrorBoundarydangerouslySetInnerHTML={{ __html }}raw()hono/html@hono/vite-dev-server@hono/vite-build/<runtime>// vite.config.ts
import build from '@hono/vite-build/node' // or /bun, /cloudflare-workers
import devServer from '@hono/vite-dev-server'
import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => {
if (mode === 'client') {
return {
build: { rollupOptions: { input: './src/client.tsx', output: { entryFileNames: 'static/client.js' } } },
}
}
return {
plugins: [
// staticRoot is a Node adapter option: it serves dist/static from the built server
build({ entry: 'src/index.tsx', staticRoot: './dist' }),
devServer({ entry: 'src/index.tsx' }),
],
}
})// renderer: pick the script path by mode
{import.meta.env.PROD ? (
<script type="module" src="/static/client.js" />
) : (
<script type="module" src="/src/client.tsx" />
)}vite build --mode client && vite buildnode dist/index.js@hono/node-server@hono/vite-dev-server/cloudflare/node/bunAccept: text/markdownhonojsxImportSourcehono/jsxclassjsxRendererc.render()<title>vite-ssr-componentsViteClientScriptLink<script>npm run cf-typegen