Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Общий макет страниц

У всех страниц сайта одинаковая обёртка: теги 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().

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.