A shared page layout
Every page of a site has the same wrapper: the html and head tags, a header with a menu. There is no need to repeat it in every route. It goes into a layout component, and the jsxRenderer middleware applies the layout automatically.
A component with children
Create src/components/Layout.tsx:
// src/components/Layout.tsx
import type { FC } from 'hono/jsx'
export const Layout: FC = ({ children }) => {
return (
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Blog</title>
</head>
<body>
<header>
<a href="/">Home</a> | <a href="/posts">Posts</a>
</header>
<main>{children}</main>
</body>
</html>
)
}children is everything written between the component's opening and closing tags. It is used like this:
// src/routes/pages.tsx
import { Layout } from '../components/Layout.js'
pages.get('/about', (c) => {
return c.html(
<Layout>
<h1>About the blog</h1>
<p>Notes on web development.</p>
</Layout>,
)
})The page http://localhost:3000/about shows the menu and the text about the blog.
jsxRenderer
To avoid wrapping every page by hand, attach the middleware at the top of src/routes/pages.tsx, right after new Hono():
// src/routes/pages.tsx
import { jsxRenderer } from 'hono/jsx-renderer'
import { Layout } from '../components/Layout.js'
pages.use(jsxRenderer(({ children }) => <Layout>{children}</Layout>))
pages.get('/about', (c) => {
return c.render(<h1>About the blog</h1>)
})The c.render() method takes the page content and puts it into the layout. The result is the same, but the route now contains only the page's own markup. The middleware adds the <!DOCTYPE html> line itself.
A page title of its own
The second argument of c.render() passes data to the layout, such as the tab title. First tell TypeScript what data c.render() accepts:
// src/renderer.d.ts
import 'hono'
declare module 'hono' {
interface ContextRenderer {
(content: string | Promise<string>, props: { title: string }): Response
}
}Then accept title in the layout and pass it from the route:
// src/routes/pages.tsx
pages.use(
jsxRenderer(({ children, title }) => (
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>{children}</body>
</html>
)),
)
pages.get('/about', (c) => c.render(<h1>About the blog</h1>, { title: 'About the blog' }))The browser tab shows "About the blog". Once the type is declared, title is required in every c.render() call, and TypeScript reminds you if you forget it.
Which to choose
| Approach | When it fits |
|---|---|
<Layout> in every route | a few pages with different layouts |
jsxRenderer and c.render() | one layout for a group of pages |
Common mistake
Attaching jsxRenderer but still calling c.html() in a route. The page is sent without the layout: c.html() knows nothing about it. Inside a group with jsxRenderer, return pages with c.render().