Общий макет страниц
У всех страниц сайта одинаковая обёртка: теги html, head, шапка с меню. Повторять её в каждом маршруте не нужно. Её выносят в компонент-макет, а middleware jsxRenderer подставляет макет автоматически.
Компонент с children
Создайте src/components/Layout.tsx:
// src/components/Layout.tsx
import type { FC } from 'hono/jsx'
export const Layout: FC = ({ children }) => {
return (
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>Блог</title>
</head>
<body>
<header>
<a href="/">Главная</a> | <a href="/posts">Статьи</a>
</header>
<main>{children}</main>
</body>
</html>
)
}children — всё, что написано между открывающим и закрывающим тегом компонента. Используется он так:
// src/routes/pages.tsx
import { Layout } from '../components/Layout.js'
pages.get('/about', (c) => {
return c.html(
<Layout>
<h1>О блоге</h1>
<p>Заметки о веб-разработке.</p>
</Layout>,
)
})Страница http://localhost:3000/about покажет меню и текст о блоге.
jsxRenderer
Чтобы не оборачивать каждую страницу вручную, подключите middleware в начале src/routes/pages.tsx, сразу после 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>О блоге</h1>)
})Метод c.render() берёт содержимое страницы и вставляет его в макет. Результат тот же, но в маршруте осталась только разметка самой страницы. Строку <!DOCTYPE html> middleware добавит само.
Свой заголовок страницы
Вторым аргументом c.render() можно передать данные в макет, например заголовок вкладки. Сначала сообщите TypeScript, какие данные принимает c.render():
// src/renderer.d.ts
import 'hono'
declare module 'hono' {
interface ContextRenderer {
(content: string | Promise<string>, props: { title: string }): Response
}
}Затем примите title в макете и передайте его из маршрута:
// src/routes/pages.tsx
pages.use(
jsxRenderer(({ children, title }) => (
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>{title}</title>
</head>
<body>{children}</body>
</html>
)),
)
pages.get('/about', (c) => c.render(<h1>О блоге</h1>, { title: 'О блоге' }))Во вкладке браузера появится «О блоге». После объявления типа title обязателен в каждом вызове c.render(), и TypeScript напомнит, если его забыть.
Что выбрать
| Способ | Когда удобен |
|---|---|
<Layout> в каждом маршруте | несколько страниц с разными макетами |
jsxRenderer и c.render() | один макет для группы страниц |
Частая ошибка
Подключают jsxRenderer, но в маршруте по-прежнему вызывают c.html(). Страница отдаётся без макета: c.html() ничего не знает о нём. Внутри группы с jsxRenderer возвращайте страницы через c.render().