# Обзор портала (/docs)
Портал собирает техническую документацию продуктов Reactive Engineering в одном
интерфейсе: архитектуру, руководства по подключению, эксплуатационные материалы и
стандарты разработки.
## Как устроена навигация [#как-устроена-навигация]
Документация разделена на три уровня.
| Уровень | Что это | Где управляется |
| -------- | ----------------------------------------------------------------- | --------------------------------------- |
| Продукт | Верхний уровень. Выбирается в выпадающем списке над боковым меню. | Каталог с `"root": true` в `meta.json` |
| Раздел | Тематическая группа страниц внутри продукта. | Вложенный каталог с обычным `meta.json` |
| Страница | Отдельный документ. | Файл `.mdx` |
Выпадающий список в верхней части бокового меню показывает все продукты
портала. Один продукт — один пункт списка; под ним отображаются только его
разделы и страницы.
## Продукты [#продукты]
## Дополнительные представления [#дополнительные-представления]
Помимо веб-интерфейса документация доступна в машиночитаемых форматах.
* `/<путь-страницы>.md` — Markdown-версия любой страницы документации;
* [`/llms.txt`](/llms.txt) — краткий индекс всех страниц для LLM;
* [`/llms-full.txt`](/llms-full.txt) — полное содержимое портала одним файлом.
# Doc-Portal (/docs/doc-portal)
**Doc-Portal** — шаблон для быстрого развертывания порталов документации
Reactive Engineering. Из репозитория разворачивается готовый сайт: брендированный
интерфейс, поиск, машиночитаемые представления страниц, CI и Docker-сборка.
Остается наполнить его содержимым.
## Что входит в шаблон [#что-входит-в-шаблон]
## Технологический стек [#технологический-стек]
| Слой | Решение |
| ------------------- | ------------------------------------------------------------------ |
| Фреймворк | Next.js 16, App Router, React 19 |
| Движок документации | Fumadocs 16 (`fumadocs-core`, `fumadocs-mdx`, `@fumadocs/base-ui`) |
| Формат контента | MDX с поддержкой Mermaid |
| Стили | Tailwind CSS 4, брендовые токены Reactive Engineering |
| Шрифты | Nunito Sans (текст), Ubuntu (заголовки и выделения) |
| Сборка и публикация | Docker (standalone-режим Next.js), GitHub Actions, Dokploy |
## Принцип наполнения [#принцип-наполнения]
Один продукт — один каталог верхнего уровня в `content/docs/` с признаком
`"root": true`. Такой каталог становится отдельным пунктом выпадающего списка над
боковым меню, а его вложенные каталоги — разделами документации этого продукта.
```text
content/docs/
├── meta.json порядок продуктов
├── index.mdx обзорная страница /docs
└── doc-portal/ продукт: "root": true → пункт дропдауна
├── meta.json
├── index.mdx
├── getting-started/ раздел
├── architecture/ раздел
├── how-to/ раздел
└── brand-and-styles/ раздел
```
Репозиторная документация в каталоге `docs/` и страницы этого раздела описывают
одно и то же. При изменении шаблона обновляйте оба места — правило
зафиксировано в `AGENT.md`.
# Конвейер сборки (/docs/doc-portal/architecture/build-pipeline)
## Этапы [#этапы]
### Генерация коллекции [#генерация-коллекции]
`fumadocs-mdx` читает `source.config.ts`, обходит `content/docs/`, валидирует
frontmatter и `meta.json` по Zod-схемам и создает каталог `.source/` с
типизированной коллекцией и виртуальным модулем `collections/server`.
Запускается скриптом `postinstall`, а также первым шагом `pnpm types:check`.
### Загрузчик [#загрузчик]
`lib/source.ts` вызывает `loader()` из `fumadocs-core/source`: строит дерево
страниц, назначает базовый URL `/docs` и подключает плагин иконок.
```ts title="lib/source.ts"
export const source = loader({
baseUrl: docsRoute,
source: docs.toFumadocsSource(),
plugins: [lucideIconsPlugin()],
});
```
### Рендеринг [#рендеринг]
`app/docs/[[...slug]]/page.tsx` получает страницу по slug, рендерит её тело
через `getMDXComponents()` и собирает оглавление, кнопки Markdown и GitHub,
Open Graph-метаданные.
`generateStaticParams()` перечисляет все страницы, поэтому документация
предрендерится на этапе сборки.
## Обработка MDX [#обработка-mdx]
`source.config.ts` описывает коллекцию и remark-плагины:
```ts title="source.config.ts"
export const docs = defineDocs({
dir: 'content/docs',
docs: {
schema: pageSchema,
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
export default defineConfig({
mdxOptions: {
remarkPlugins: [remarkMdxMermaid],
},
});
```
* `includeProcessedMarkdown` сохраняет обработанный Markdown каждой страницы —
на нем построены `/llms.txt`, `/llms-full.txt` и кнопка «Copy Markdown»;
* `remarkMdxMermaid` превращает блоки ` ```mermaid ` в компонент ``,
зарегистрированный в `components/mdx.tsx`.
## Диаграммы Mermaid [#диаграммы-mermaid]
`components/mdx/mermaid.tsx` — клиентский компонент. Библиотека `mermaid`
загружается динамически и кешируется по паре «текст диаграммы + тема», тема
диаграммы синхронизирована со темой портала через `next-themes`.
## Дополнительные представления [#дополнительные-представления]
| Маршрут | Что отдает |
| ---------------------------------- | ------------------------------------------------------- |
| `/api/search` | Индекс полнотекстового поиска Fumadocs. |
| `/llms.txt` | Краткий индекс: заголовки, описания и URL всех страниц. |
| `/llms-full.txt` | Полный обработанный Markdown всех страниц одним файлом. |
| `/llms.mdx/docs/<путь>/content.md` | Markdown отдельной страницы. |
| `/og/docs/<путь>/image.png` | Open Graph-изображение в брендовых цветах. |
### Content negotiation [#content-negotiation]
`proxy.ts` перехватывает запросы к документации и отдает Markdown вместо HTML,
если клиент явно его просит:
* суффикс `.md` в адресе — `/docs/doc-portal/architecture.md`;
* заголовок `Accept`, в котором Markdown приоритетнее HTML (типично для
LLM-агентов и CLI-клиентов).
Оба случая переписываются на `/llms.mdx/docs/<путь>/content.md` без редиректа.
## Продукционная сборка [#продукционная-сборка]
`next.config.mjs` включает `output: 'standalone'` — Next.js собирает
самодостаточный `server.js` вместе с минимальным набором зависимостей.
`Dockerfile` копирует именно этот результат, поэтому образ не содержит
`node_modules` целиком.
# Слои конфигурации (/docs/doc-portal/architecture/configuration-layers)
Настройки шаблона разделены по слоям: чем выше слой, тем чаще его правят при
развертывании нового портала.
## Слой 1 — портал [#слой-1--портал]
`lib/portal.config.ts` — единственный файл, обязательный к правке при
развертывании нового портала.
| Поле | Влияет на |
| ------------------------------------ | ----------------------------------------------------------------------------- |
| `name`, `shortName` | Навбар, шаблон `
`, Open Graph-изображения. |
| `tagline`, `description` | Hero-блок главной страницы и `meta description`. |
| `docsRoute` | Базовый путь документации, content negotiation в `proxy.ts`. |
| `git.user`, `git.repo`, `git.branch` | Ссылка на репозиторий в навбаре и кнопка «View on GitHub» на каждой странице. |
| `products[]` | Карточки продуктов на главной странице. |
| `externalPortals[]` | Пункты навигации и карточки связанных порталов. |
| `placeholders` | Тексты и целевые адреса страниц `/swagger` и `/flows`. |
`products[]` описывает карточки на главной странице, а выпадающий список
строится из каталогов с `"root": true`. Это два независимых источника —
добавляя продукт, обновите оба: каталог в `content/docs/` и запись в конфиге.
## Слой 2 — бренд [#слой-2--бренд]
| Файл | Содержит |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `lib/brand.ts` | Палитра, градиент, пути к ассетам и названия шрифтов как TypeScript-константы. Используется в React (Open Graph, инлайновые стили). |
| `app/global.css` | Те же значения как CSS-переменные плюс переопределение палитры Fumadocs (`--color-fd-*`) для светлой и темной темы. |
| `app/layout.tsx` | Подключение шрифтов через `next/font/google`. |
| `public/brand/` | Логотипы и паттерны из брендбука. |
Подробности — в разделе [Бренд и стили](/docs/doc-portal/brand-and-styles).
## Слой 3 — структура документации [#слой-3--структура-документации]
`content/docs/**/meta.json` и frontmatter `.mdx`-файлов. Код не затрагивается:
добавление продукта, раздела или страницы — операция над содержимым.
## Слой 4 — конвейер сборки [#слой-4--конвейер-сборки]
Правится редко, при изменении самого шаблона.
| Файл | Когда править |
| ----------------------- | --------------------------------------------------------------- |
| `source.config.ts` | Расширение схем frontmatter, добавление remark/rehype-плагинов. |
| `next.config.mjs` | Редиректы, заголовки, режим сборки. |
| `proxy.ts` | Правила content negotiation. |
| `components/mdx.tsx` | Регистрация новых компонентов, доступных в MDX. |
| `lib/layout.shared.tsx` | Изменение состава верхней навигации. |
| `app/docs/layout.tsx` | Поведение бокового меню и выпадающего списка продуктов. |
## Переменные окружения [#переменные-окружения]
Значения, зависящие от среды развертывания, а не от кода.
| Переменная | Назначение |
| ---------------------- | ------------------------------------------------------------------------ |
| `NEXT_PUBLIC_SITE_URL` | Базовый адрес портала для `metadataBase` и абсолютных Open Graph-ссылок. |
| `PORT` | Порт HTTP-сервера. |
| `HOSTNAME` | Интерфейс прослушивания; в контейнере `0.0.0.0`. |
# Модель контента (/docs/doc-portal/architecture/content-model)
## Три уровня иерархии [#три-уровня-иерархии]
### Продукт [#продукт]
Каталог первого уровня внутри `content/docs/`, в `meta.json` которого указано
`"root": true`.
```json title="content/docs/doc-portal/meta.json"
{
"title": "Doc-Portal",
"description": "Шаблон портала документации Reactive Engineering",
"icon": "BookMarked",
"root": true,
"pages": ["index", "getting-started", "architecture", "how-to", "brand-and-styles"]
}
```
Признак `root` заставляет Fumadocs выделить каталог в отдельный корень дерева
страниц. Layout документации получает такие корни как «tabs» и при
`tabMode="auto"` рисует их выпадающим списком над боковым меню. `title`,
`description` и `icon` формируют содержимое пункта списка.
Пока пользователь находится внутри продукта, боковое меню показывает только его
разделы. Переход к другому продукту выполняется через выпадающий список.
### Раздел [#раздел]
Вложенный каталог **без** `root`. Его `meta.json` задает название, иконку и
порядок страниц:
```json title="content/docs/doc-portal/architecture/meta.json"
{
"title": "Архитектура",
"description": "Модель контента, конвейер сборки и слои конфигурации",
"icon": "Waypoints",
"pages": ["index", "content-model", "build-pipeline", "configuration-layers"]
}
```
### Страница [#страница]
Файл `.mdx` с frontmatter:
```mdx
---
title: Модель контента
description: Продукты, разделы и страницы.
icon: Layers
---
Содержимое страницы...
```
| Поле | Обязательное | Назначение |
| ------------- | ------------ | ---------------------------------------------------------------- |
| `title` | да | Заголовок страницы, пункт бокового меню, `` документа. |
| `description` | нет | Подзаголовок на странице, `meta description`, описание в поиске. |
| `icon` | нет | Имя иконки Lucide, отображается в боковом меню. |
| `full` | нет | `true` — страница на всю ширину, без области оглавления. |
## Формирование URL [#формирование-url]
URL собирается из пути файла относительно `content/docs/`, база — `/docs`.
| Файл | URL |
| -------------------------------------------------------- | --------------------------------------------- |
| `content/docs/index.mdx` | `/docs` |
| `content/docs/doc-portal/index.mdx` | `/docs/doc-portal` |
| `content/docs/doc-portal/architecture/index.mdx` | `/docs/doc-portal/architecture` |
| `content/docs/doc-portal/architecture/content-model.mdx` | `/docs/doc-portal/architecture/content-model` |
`index.mdx` представляет корень своего каталога. Имя каталога попадает в
публичный адрес, поэтому названия каталогов задаются латиницей в kebab-case.
## Правила поля `pages` [#правила-поля-pages]
Массив `pages` перечисляет элементы явно и определяет их порядок в боковом меню.
```json
{
"pages": ["index", "installation", "project-structure"]
}
```
* элементы указываются без расширения `.mdx`;
* каталоги указываются по имени каталога;
* `"..."` добавляет все неперечисленные элементы в конец;
* `"---Название---"` вставляет разделитель между группами;
* элемент, не указанный в `pages` и без `"..."`, **не попадает в навигацию**,
хотя страница остается доступной по прямому URL.
Файл создан, но не добавлен в `pages` — страница «пропадает» из бокового меню.
Всегда перечисляйте страницы явно.
## Иконки [#иконки]
Значение `icon` разрешается плагином `lucideIconsPlugin` в `lib/source.ts` по
карте имен библиотеки `lucide-react`. Имя указывается в PascalCase:
`Rocket`, `Waypoints`, `BookMarked`, `Terminal`, `Palette`.
Неизвестное имя не ломает сборку — плагин выводит предупреждение
`[lucide-icons-plugin] Unknown icon detected` и не рисует иконку.
# Архитектура (/docs/doc-portal/architecture)
Портал — это приложение Next.js с файловой маршрутизацией, где содержимое
документации хранится в репозитории рядом с кодом. Сборка превращает `.mdx`-файлы
в типизированную коллекцию, а единственный динамический маршрут рендерит любую
страницу документации.
## Общая схема [#общая-схема]
## Ключевые решения [#ключевые-решения]
| Решение | Обоснование |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Контент в репозитории, а не в CMS | Документация проходит тот же review, что и код; версия документации совпадает с версией продукта. |
| Один динамический маршрут `[[...slug]]` | Новая страница не требует изменений в коде — достаточно `.mdx`-файла. |
| Структура из `meta.json`, а не из имен файлов | Порядок и названия разделов управляются явно, переименование файла не ломает навигацию. |
| Продукт = каталог с `"root": true` | Fumadocs автоматически строит выпадающий список продуктов; добавление продукта — операция над содержимым, не над кодом. |
| Вся настройка в `lib/portal.config.ts` | Развертывание нового портала не требует правок в компонентах. |
| Брендовые токены в CSS-переменных | Fumadocs UI читает `--color-fd-*`; переопределение палитры брендирует все встроенные компоненты сразу. |
## Подробнее [#подробнее]
# Ассеты (/docs/doc-portal/brand-and-styles/assets)
## Состав каталога [#состав-каталога]
Ассеты лежат в `public/brand/` и доступны по абсолютным путям. Источник —
официальный набор `RE Brand Assets`.
| Файл | Содержимое | Применение |
| --------------------------- | ------------------------------------------ | --------------------------- |
| `logo-horizontal-ink.png` | Горизонтальный логотип, темно-синий | Светлая тема |
| `logo-horizontal-white.png` | Горизонтальный логотип, белый | Темная тема и темные панели |
| `logo-mark-ink.png` | Знак R/E, темно-синий | Светлая тема, малые размеры |
| `logo-mark-white.png` | Знак R/E, белый | Темная тема, малые размеры |
| `pattern-hero.png` | Геометрический паттерн на темно-синем фоне | Фон hero-блоков |
| `pattern-blocks.png` | Полноцветный геометрический паттерн | Акцентные панели |
| `app/icon.png` | Знак R/E на градиенте, 512×512 | Favicon портала |
Пути к ассетам объявлены в `lib/brand.ts` как `brandAssets` — используйте
константы вместо строковых литералов.
## Компонент логотипа [#компонент-логотипа]
`components/brand/logo.tsx` рендерит оба цветовых варианта и переключает их через
CSS (`dark:hidden` / `hidden dark:block`). Это оставляет компонент серверным и
исключает мелькание неверного варианта при гидратации.
```tsx
import { Logo } from '@/components/brand/logo';
// Полный логотип с надписью
// Только знак R/E
```
| Свойство | Значение | Назначение |
| ----------- | ---------------------- | ------------------------------------------------------------------- |
| `variant` | `horizontal` \| `mark` | Полный логотип или только знак. По умолчанию `horizontal`. |
| `className` | классы Tailwind | Задавайте только высоту (`h-*`) — ширина вычисляется автоматически. |
| `priority` | `boolean` | `true` для логотипа выше первого экрана. |
Компонент использует `w-auto object-contain`. Фиксация ширины исказит пропорции
логотипа.
## Паттерн в качестве фона [#паттерн-в-качестве-фона]
Готовая утилита из `app/global.css`:
```tsx
{/* содержимое */}
```
Градиентный оверлей поверх паттерна обязателен: он гасит геометрию под текстом и
обеспечивает читаемость. Без него паттерн конкурирует с содержимым.
## Иконка продукта [#иконка-продукта]
`components/brand/brand-icon.tsx` помещает иконку Lucide в плашку с мягким
брендовым градиентом. Иконки разрешаются по той же карте имен, что использует
Fumadocs для `meta.json`, поэтому карточка продукта и пункт выпадающего списка
получают один и тот же глиф.
```tsx
import { BookOpen } from 'lucide-react';
import { BrandIcon } from '@/components/brand/brand-icon';
```
## Favicon [#favicon]
`app/icon.png` — знак R/E на фирменном градиенте, 512×512. Next.js обрабатывает
файл по соглашению App Router и подставляет нужные размеры автоматически.
## Замена ассетов под другой бренд [#замена-ассетов-под-другой-бренд]
### Подготовить файлы [#подготовить-файлы]
Нужны четыре варианта логотипа: горизонтальный и знак, каждый в темном и светлом
исполнении, PNG с прозрачным фоном.
### Заменить файлы в public/brand/ [#заменить-файлы-в-publicbrand]
Сохраните имена файлов — тогда правки в коде не потребуются. При других именах
обновите `brandAssets` в `lib/brand.ts`.
### Обновить favicon [#обновить-favicon]
Замените `app/icon.png` изображением 512×512.
### Проверить обе темы [#проверить-обе-темы]
Логотип в навбаре, hero-блок главной страницы, подвал, страницы-заглушки.
# Цвета и градиент (/docs/doc-portal/brand-and-styles/colors)
## Палитра [#палитра]
Значения сняты с официальных ассетов брендбука Reactive Engineering.
| Роль | HEX | Токен CSS | Константа |
| ---------------------------- | --------- | ------------------------ | ------------------------ |
| Основной акцент — зеленый | `#0DE644` | `--color-re-green` | `brandColors.green` |
| Дополнительный акцент — циан | `#45E1FF` | `--color-re-cyan` | `brandColors.cyan` |
| Основной темный тон | `#000D2B` | `--color-re-navy` | `brandColors.navy` |
| Темный тон под логотипом | `#001029` | `--color-re-navy-raised` | `brandColors.navyRaised` |
| Светлая нейтраль | `#E8EDF3` | `--color-re-mist` | `brandColors.mist` |
| Приглушенный стальной | `#406E8E` | `--color-re-steel` | `brandColors.steel` |
Токены объявлены в блоке `@theme` файла `app/global.css`, поэтому доступны как
классы Tailwind: `bg-re-navy`, `text-re-cyan`, `border-re-green`.
## Фирменный градиент [#фирменный-градиент]
Зеленый переходит в циан слева направо под углом 100°.
```css
--re-gradient: linear-gradient(100deg, #0de644 0%, #45e1ff 100%);
```
Дополнительно объявлен полупрозрачный вариант `--re-gradient-soft` — для плашек
иконок и мягких подложек.
### Готовые утилиты [#готовые-утилиты]
| Класс | Применение |
| ---------------------- | -------------------------------------------------------- |
| `.re-gradient-text` | Градиентная заливка текста — акцентные слова заголовков. |
| `.re-gradient-surface` | Градиентная плашка — основные кнопки, бейджи. |
| `.re-gradient-rule` | Градиентная разделительная линия толщиной 2px. |
| `.re-pattern-panel` | Темная панель с геометрическим паттерном — hero-блоки. |
```tsx
Открыть документацию
```
Градиент светлый, поэтому текст на нем — темно-синий (`--color-re-navy`), а не
белый. `.re-gradient-surface` задает такой цвет текста по умолчанию.
## Переопределение темы Fumadocs [#переопределение-темы-fumadocs]
Fumadocs UI читает палитру из переменных `--color-fd-*`. Шаблон переопределяет их
брендовыми значениями, поэтому все встроенные компоненты — боковое меню, поиск,
карточки, выделенные блоки — оформлены в фирменном стиле без правки самих
компонентов.
```css title="app/global.css"
:root {
--color-fd-background: #ffffff;
--color-fd-foreground: #000d2b;
--color-fd-primary: #0aa835;
--color-fd-ring: #0de644;
/* ... */
}
.dark {
--color-fd-background: #000d2b;
--color-fd-foreground: #e8edf3;
--color-fd-primary: #45e1ff;
--color-fd-ring: #45e1ff;
/* ... */
}
```
### Почему акценты различаются по темам [#почему-акценты-различаются-по-темам]
| Тема | Акцент | Причина |
| ------- | ------------------------------- | ---------------------------------------------------------------------------- |
| Светлая | `#0AA835` — затемненный зеленый | Чистый `#0DE644` на белом фоне не проходит по контрасту для текста и ссылок. |
| Темная | `#45E1FF` — циан | На темно-синем фоне циан читается лучше зеленого. |
Фирменные `#0DE644` и `#45E1FF` при этом используются без изменений там, где они
работают как плашка или обводка, а не как цвет текста: градиенты, кольца фокуса,
подсветка при наведении.
## Изменение палитры под другой бренд [#изменение-палитры-под-другой-бренд]
### Обновить константы [#обновить-константы]
```ts title="lib/brand.ts"
export const brandColors = {
green: '#0DE644',
cyan: '#45E1FF',
navy: '#000D2B',
// ...
} as const;
```
### Обновить токены и палитру Fumadocs [#обновить-токены-и-палитру-fumadocs]
В `app/global.css` — блок `@theme`, переменные `--re-gradient` и наборы
`--color-fd-*` для `:root` и `.dark`.
### Проверить контраст [#проверить-контраст]
Проверьте светлую и темную тему: основной текст, ссылки, состояния наведения и
фокуса, выделенные блоки `Callout`.
# Бренд и стили (/docs/doc-portal/brand-and-styles)
Оформление портала построено на фирменном стиле Reactive Engineering: зелено-циановый
градиент, темно-синий как основной тёмный тон, геометрический паттерн и связка
шрифтов Ubuntu и Nunito Sans.
## Где что задано [#где-что-задано]
| Файл | Содержит |
| --------------------------------- | ---------------------------------------------------------------------------------------- |
| `lib/brand.ts` | Палитра, градиент, пути к ассетам и названия шрифтов как TypeScript-константы. |
| `app/global.css` | Те же значения как CSS-переменные, переопределение палитры Fumadocs и брендовые утилиты. |
| `app/layout.tsx` | Подключение шрифтов через `next/font/google`. |
| `components/brand/logo.tsx` | Логотип с автоматическим выбором варианта под тему. |
| `components/brand/brand-icon.tsx` | Иконка продукта в градиентной плашке. |
| `public/brand/` | Логотипы и паттерны из брендбука. |
| `app/icon.png` | Favicon: знак R/E на градиенте. |
Палитра продублирована в `lib/brand.ts` и `app/global.css`: первый нужен React
(Open Graph-изображения, инлайновые стили), второй — CSS и Fumadocs UI. При
изменении цвета правьте оба файла.
## Подробнее [#подробнее]
# Типографика (/docs/doc-portal/brand-and-styles/typography)
## Связка шрифтов [#связка-шрифтов]
| Роль | Шрифт | CSS-переменная |
| -------------------------- | --------------- | --------------- |
| Основной текст и интерфейс | **Nunito Sans** | `--font-nunito` |
| Заголовки, H1, выделения | **Ubuntu** | `--font-ubuntu` |
Шрифты подключаются через `next/font/google` в `app/layout.tsx` — файлы
self-hosted, внешних запросов к Google Fonts в рантайме нет.
Оба шрифта содержат кириллический набор (`cyrillic`, `cyrillic-ext`), поэтому
дополнительные гарнитуры-компаньоны не требуются:
```css title="app/global.css"
--font-sans: var(--font-nunito), ui-sans-serif, system-ui, sans-serif;
--font-display: var(--font-ubuntu), var(--font-nunito), ui-sans-serif, sans-serif;
```
Системные шрифты в конце стека — страховка на случай, если веб-шрифт не
загрузился.
## Доступные начертания [#доступные-начертания]
В Ubuntu существуют только начертания **300, 400, 500 и 700**. Запрос
промежуточного веса (например, `font-semibold`, то есть 600) заставит браузер
синтезировать начертание — буквы получатся искусственно утолщёнными.
Nunito Sans переменный, для него ограничений нет.
```tsx title="app/layout.tsx"
const ubuntu = Ubuntu({
subsets: ['latin', 'latin-ext', 'cyrillic'],
weight: ['400', '500', '700'],
display: 'swap',
variable: '--font-ubuntu',
});
```
Список доступных весов зафиксирован в `lib/brand.ts` как
`brandFonts.displayWeights`.
## Применение [#применение]
`h1`, `h2`, `h3` получают display-гарнитуру автоматически через `app/global.css`.
Для остальных элементов используйте утилиты:
| Класс | Результат |
| -------------- | ------------------------------------------------------------------------ |
| `font-display` | Ubuntu |
| `font-sans` | Nunito Sans (значение по умолчанию для `body`) |
| `re-display` | Ubuntu с начертанием 700 — для крупных акцентных надписей вне заголовков |
```tsx
Продукты
```
Обратите внимание на `font-medium` (500), а не `font-semibold` (600) — по причине,
описанной выше.
## Шкала заголовков [#шкала-заголовков]
| Элемент | Начертание | Трекинг |
| ------------------------------- | ---------- | --------- |
| `h1`, `.re-display` | 700 | `-0.01em` |
| `h2`, `h3` | 500 | `-0.01em` |
| Заголовок страницы документации | 700 | `-0.02em` |
| Основной текст | 400 | обычный |
## Замена шрифтов [#замена-шрифтов]
### Проверить наличие кириллицы [#проверить-наличие-кириллицы]
Прежде чем выбрать гарнитуру, убедитесь, что в Google Fonts у неё есть набор
`cyrillic`:
```bash
node -e "const d=require('next/dist/compiled/@next/font/dist/google/font-data.json'); console.log(d['Ubuntu'])"
```
Если набора нет, добавьте кириллическую гарнитуру следующим элементом стека —
браузер подбирает шрифт отдельно для каждого глифа.
### Подключить гарнитуры [#подключить-гарнитуры]
```tsx title="app/layout.tsx"
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin', 'latin-ext', 'cyrillic'],
display: 'swap',
variable: '--font-inter',
});
```
Добавьте переменную шрифта в `className` элемента ``. Для непеременных
гарнитур укажите массив `weight`.
### Обновить стек и начертания [#обновить-стек-и-начертания]
```css title="app/global.css"
@theme {
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
}
```
Проверьте, что веса заголовков в `app/global.css` и классы `font-*` в
компонентах входят в набор, доступный у новой гарнитуры.
### Обновить константы [#обновить-константы]
`lib/brand.ts` — `brandFonts.body`, `brandFonts.display` и
`brandFonts.displayWeights` — единственный источник названий гарнитур для
документации и вспомогательных скриптов.
# Быстрый старт (/docs/doc-portal/getting-started)
Раздел описывает минимальный путь от клонирования репозитория до работающего
портала с собственным содержимым.
## Порядок действий [#порядок-действий]
### Подготовить окружение [#подготовить-окружение]
Установить Node.js 24 и pnpm 11.7, склонировать репозиторий и установить
зависимости — см. [Установка и запуск](/docs/doc-portal/getting-started/installation).
### Настроить портал [#настроить-портал]
Заполнить `lib/portal.config.ts`: название, описание, координаты репозитория и
список продуктов — см. [Настройка портала](/docs/doc-portal/how-to/configure-portal).
### Наполнить документацию [#наполнить-документацию]
Создать каталог продукта в `content/docs/` и разделы внутри него —
см. [Добавление продукта](/docs/doc-portal/how-to/add-product).
### Опубликовать [#опубликовать]
Собрать Docker-образ и развернуть через Dokploy или compose —
см. [Публикация портала](/docs/doc-portal/how-to/deploy).
## Что дальше [#что-дальше]
# Установка и запуск (/docs/doc-portal/getting-started/installation)
## Требования [#требования]
| Инструмент | Версия | Где зафиксировано |
| ---------- | ---------------- | ------------------------------------ |
| Node.js | 24 или новее | `package.json` → `engines`, `.nvmrc` |
| pnpm | 11.7 или новее | `package.json` → `packageManager` |
| Docker | любая актуальная | опционально, для публикации |
## Установка [#установка]
```bash
pnpm install
```
Скрипт `postinstall` запускает `fumadocs-mdx` и генерирует каталог `.source/` —
типы и коллекции документации. Без него сборка не найдет модуль
`collections/server`.
## Разработка [#разработка]
```bash
pnpm dev
```
* портал — `http://localhost:3000`;
* документация — `http://localhost:3000/docs`.
Изменения в `.mdx`-файлах и `meta.json` подхватываются без перезапуска сервера.
## Проверки перед коммитом [#проверки-перед-коммитом]
```bash
pnpm types:check
```
```bash
pnpm build
```
`types:check` последовательно выполняет генерацию коллекций Fumadocs, генерацию
типов маршрутов Next.js и проверку TypeScript. Обе команды выполняются в CI
(`.github/workflows/ci.yml`), поэтому локальный прогон экономит цикл сборки.
`.source/`, `.next/`, `node_modules/` и `tsconfig.tsbuildinfo` генерируются
инструментами. Они исключены из репозитория через `.gitignore`.
## Запуск в Docker [#запуск-в-docker]
```bash
docker compose up --build
```
Compose собирает production-образ по `Dockerfile` и публикует порт `3000`.
Переменные окружения перечислены в `.env.example`.
## Переменные окружения [#переменные-окружения]
| Переменная | Назначение | Значение по умолчанию |
| ---------------------- | -------------------------------------------------------------------------- | ----------------------- |
| `NEXT_PUBLIC_SITE_URL` | Базовый адрес портала. Используется в `metadataBase` и Open Graph-ссылках. | `http://localhost:3000` |
| `PORT` | Порт HTTP-сервера Next.js. | `3000` |
| `HOSTNAME` | Интерфейс прослушивания. В контейнере обязателен `0.0.0.0`. | `0.0.0.0` |
# Структура репозитория (/docs/doc-portal/getting-started/project-structure)
## Дерево репозитория [#дерево-репозитория]
## Маршруты приложения [#маршруты-приложения]
| Путь | Назначение |
| -------------------------------------------- | -------------------------------------------------------------------------- |
| `app/layout.tsx` | Корневой layout: подключение шрифтов, глобальные metadata, провайдер темы. |
| `app/page.tsx` | Главная страница `/`: hero-блок, карточки продуктов и связанных порталов. |
| `app/global.css` | Брендовые токены, переопределение палитры Fumadocs, типографика, утилиты. |
| `app/icon.png` | Favicon портала: знак R/E на брендовом градиенте. |
| `app/docs/layout.tsx` | Layout документации: дерево страниц и выпадающий список продуктов. |
| `app/docs/[[...slug]]/page.tsx` | Рендерер всех MDX-страниц: заголовок, TOC, кнопки Markdown и GitHub, SEO. |
| `app/api/search/route.ts` | Endpoint полнотекстового поиска. |
| `app/og/docs/[...slug]/route.tsx` | Генерация Open Graph-изображений в брендовых цветах. |
| `app/llms.txt/route.ts` | Краткий индекс документации для LLM. |
| `app/llms-full.txt/route.ts` | Полное содержимое всех страниц одним файлом. |
| `app/llms.mdx/docs/[[...slug]]/route.ts` | Markdown-представление отдельной страницы. |
| `app/swagger/page.tsx`, `app/flows/page.tsx` | Страницы-заглушки связанных порталов. |
## Конфигурация [#конфигурация]
| Файл | Назначение |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lib/portal.config.ts` | **Единственный файл, который нужно править при развертывании нового портала:** название, описание, репозиторий, список продуктов, связанные порталы. |
| `lib/brand.ts` | Брендовые константы Reactive Engineering: палитра, градиент, пути к ассетам, шрифты. |
| `lib/shared.ts` | Плоские экспорты, производные от `portal.config.ts`, и построение GitHub-ссылок. |
| `lib/layout.shared.tsx` | Общая верхняя навигация с логотипом. |
| `lib/source.ts` | Загрузчик коллекции Fumadocs, дерево страниц, вспомогательные URL. |
| `source.config.ts` | Описание MDX-коллекции, схемы frontmatter и `meta.json`, remark-плагины. |
| `next.config.mjs` | Конфигурация Next.js: MDX и standalone-сборка. |
| `proxy.ts` | Content negotiation: отдает Markdown для `/docs/*.md` и клиентов, предпочитающих Markdown. |
## Документация о самом шаблоне [#документация-о-самом-шаблоне]
| Расположение | Аудитория |
| ---------------------------- | -------------------------------------------------------------- |
| `docs/*.md` | Разработчики, работающие с репозиторием. Читается прямо в Git. |
| `content/docs/doc-portal/**` | Пользователи портала. Публикуется на сайте. |
| `AGENT.md` | Контекст проекта для AI-агентов и новых участников команды. |
| `README.md` | Обзор проекта, быстрый старт и карта документации. |
# Добавить продукт (/docs/doc-portal/how-to/add-product)
Продукт — верхний уровень навигации. Каждый продукт становится отдельным пунктом
выпадающего списка над боковым меню, а под ним отображаются только его разделы и
страницы.
### Создать каталог продукта [#создать-каталог-продукта]
Имя каталога попадает в публичный URL — латиница, kebab-case.
```text
content/docs/payment-gateway/
```
### Описать продукт в meta.json [#описать-продукт-в-metajson]
Признак `"root": true` — то, что превращает каталог в пункт выпадающего списка.
```json title="content/docs/payment-gateway/meta.json"
{
"title": "Payment Gateway",
"description": "Платежный шлюз: подключение, API и эксплуатация",
"icon": "CreditCard",
"root": true,
"pages": ["index", "getting-started", "api", "operations"]
}
```
| Поле | Назначение |
| ------------- | --------------------------------------------------------------- |
| `title` | Название в выпадающем списке и в заголовке бокового меню. |
| `description` | Пояснение под названием в выпадающем списке. |
| `icon` | Имя иконки [Lucide](https://lucide.dev/icons) в PascalCase. |
| `root` | **Обязательно `true`** — иначе каталог станет обычным разделом. |
| `pages` | Порядок разделов и страниц внутри продукта. |
### Создать обзорную страницу [#создать-обзорную-страницу]
```mdx title="content/docs/payment-gateway/index.mdx"
---
title: Payment Gateway
description: Платежный шлюз — назначение, состав и порядок подключения.
icon: CreditCard
---
Краткое описание продукта и карточки его разделов.
```
### Добавить продукт в корневой meta.json [#добавить-продукт-в-корневой-metajson]
```json title="content/docs/meta.json"
{
"title": "Документация",
"pages": ["index", "doc-portal", "payment-gateway"]
}
```
Порядок элементов определяет порядок продуктов в выпадающем списке.
### Добавить карточку на главную страницу [#добавить-карточку-на-главную-страницу]
Выпадающий список строится из каталогов автоматически, а карточки на главной
странице берутся из конфигурации — это отдельный источник.
```ts title="lib/portal.config.ts"
products: [
{
slug: 'doc-portal',
title: 'Doc-Portal',
description: 'Шаблон портала документации...',
icon: 'BookMarked',
},
{
slug: 'payment-gateway',
title: 'Payment Gateway',
description: 'Платежный шлюз: подключение, API и эксплуатация.',
icon: 'CreditCard',
},
],
```
`slug` должен совпадать с именем каталога, `icon` — с иконкой из `meta.json`,
чтобы карточка и пункт списка выглядели согласованно.
### Наполнить разделы [#наполнить-разделы]
Далее — по инструкции
[Добавить раздел и страницу](/docs/doc-portal/how-to/add-section-and-page).
### Проверить [#проверить]
```bash
pnpm types:check
```
```bash
pnpm build
```
Убедитесь, что продукт появился в выпадающем списке, а его разделы — в боковом
меню.
## Итоговая структура [#итоговая-структура]
Забытый `"root": true` — каталог отображается как обычный раздел внутри
предыдущего продукта и не появляется в выпадающем списке.
# Добавить раздел и страницу (/docs/doc-portal/how-to/add-section-and-page)
## Добавить раздел [#добавить-раздел]
### Создать каталог внутри продукта [#создать-каталог-внутри-продукта]
```text
content/docs/payment-gateway/api/
```
### Описать раздел [#описать-раздел]
Без `"root": true` — иначе раздел станет отдельным продуктом.
```json title="content/docs/payment-gateway/api/meta.json"
{
"title": "API",
"description": "Спецификации операций и коды ошибок",
"icon": "Braces",
"pages": ["index", "authentication", "payments", "errors"]
}
```
### Добавить раздел в meta.json продукта [#добавить-раздел-в-metajson-продукта]
```json title="content/docs/payment-gateway/meta.json"
{
"pages": ["index", "getting-started", "api", "operations"]
}
```
## Добавить страницу [#добавить-страницу]
### Создать .mdx-файл [#создать-mdx-файл]
Имя файла становится последним сегментом URL.
```text
content/docs/payment-gateway/api/authentication.mdx
```
### Заполнить frontmatter [#заполнить-frontmatter]
```mdx
---
title: Аутентификация
description: Получение токена и подпись запросов к платежному шлюзу.
icon: KeyRound
---
## Получение токена
Содержимое страницы...
```
### Добавить страницу в meta.json раздела [#добавить-страницу-в-metajson-раздела]
```json
{
"pages": ["index", "authentication", "payments", "errors"]
}
```
Страница доступна по адресу
`/docs/payment-gateway/api/authentication`.
## Управление порядком и группировкой [#управление-порядком-и-группировкой]
Массив `pages` поддерживает несколько специальных элементов.
```json
{
"pages": [
"index",
"---Основное---",
"authentication",
"payments",
"---Справочник---",
"errors",
"..."
]
}
```
| Элемент | Действие |
| ------------------- | -------------------------------------------- |
| `"index"` | Обзорная страница раздела. |
| `"имя-файла"` | Страница без расширения `.mdx`. |
| `"имя-каталога"` | Вложенный раздел. |
| `"---Заголовок---"` | Визуальный разделитель с подписью. |
| `"..."` | Все неперечисленные элементы в конец списка. |
## Доступные компоненты в MDX [#доступные-компоненты-в-mdx]
Зарегистрированы в `components/mdx.tsx` и не требуют импорта на странице.
| Компонент | Назначение |
| ----------------------------------------------------------- | -------------------------------------------------- |
| `` | Выделенный блок с примечанием или предупреждением. |
| ``, `` | Сетка ссылок-карточек. |
| ``, `` | Пошаговая инструкция. |
| ``, `` | Варианты одной инструкции. |
| ``, ``, `` | Дерево файлов. |
| ``, `` | Сворачиваемые блоки. |
| Блок кода с языком `mermaid` | Диаграмма Mermaid. |
### Пример: варианты команды [#пример-варианты-команды]
````mdx
```bash
pnpm install
```
```bash
npm install
```
````
### Пример: диаграмма [#пример-диаграмма]
````mdx
```mermaid
graph LR
Client --> Gateway --> Provider
```
````
## Заголовок блока кода [#заголовок-блока-кода]
Атрибут `title` в информационной строке блока подписывает фрагмент именем файла:
````mdx
```json title="content/docs/payment-gateway/meta.json"
{ "title": "Payment Gateway" }
```
````
## Перемещение и переименование [#перемещение-и-переименование]
При переименовании или переносе `.mdx`-файла обновите `meta.json` и внутренние
ссылки на страницу. Старый URL перестает существовать — при необходимости
добавьте редирект в `next.config.mjs`.
# Настроить портал (/docs/doc-portal/how-to/configure-portal)
Настройка нового портала сводится к правке `lib/portal.config.ts`. Компоненты и
layout читают значения оттуда.
## Обязательный минимум [#обязательный-минимум]
### Название и описание [#название-и-описание]
```ts title="lib/portal.config.ts"
export const portalConfig = {
name: 'Payment Gateway Documentation',
shortName: 'PG Docs',
tagline: 'Документация платежного шлюза',
description: 'Подключение, спецификации API и эксплуатационные материалы.',
// ...
};
```
| Поле | Где отображается |
| ------------- | ------------------------------------------------------ |
| `name` | Шаблон `` всех страниц, Open Graph-изображения. |
| `shortName` | Подпись рядом с логотипом в навбаре. |
| `tagline` | Заголовок hero-блока главной страницы. |
| `description` | Подзаголовок hero-блока и `meta description`. |
### Координаты репозитория [#координаты-репозитория]
```ts
git: {
user: 'ReactiveEngineering',
repo: 'payment-gateway-docs',
branch: 'main',
},
```
Отсюда формируются ссылка на репозиторий в навбаре и кнопка «View on GitHub» на
каждой странице документации.
Несовпадение `repo` с фактическим именем репозитория даёт битые ссылки на
каждой странице документации — внешне портал при этом работает нормально.
### Список продуктов [#список-продуктов]
```ts
products: [
{
slug: 'payment-gateway',
title: 'Payment Gateway',
description: 'Платежный шлюз: подключение, API и эксплуатация.',
icon: 'CreditCard',
},
],
```
`slug` — имя каталога в `content/docs/`. Подробнее:
[Добавить продукт](/docs/doc-portal/how-to/add-product).
### Адрес портала [#адрес-портала]
```ini title=".env"
NEXT_PUBLIC_SITE_URL=https://docs.example.com
```
Значение попадает в `metadataBase` и делает Open Graph-ссылки абсолютными.
## Связанные порталы и навигация [#связанные-порталы-и-навигация]
Массив `externalPortals` формирует и пункты верхней навигации, и карточки в
нижней части главной страницы.
```ts
externalPortals: [
{
title: 'Swagger',
description: 'Интерактивные спецификации OpenAPI.',
href: 'https://swagger.example.com',
external: true,
},
],
```
| Поле | Назначение |
| ---------- | -------------------------------------------- |
| `href` | Внутренний путь или полный внешний адрес. |
| `external` | `true` — ссылка открывается в новой вкладке. |
Чтобы убрать блок связанных порталов целиком, оставьте массив пустым — раздел на
главной странице не отрендерится.
Эта навигация отображается **только на главной странице и на заглушках**. В
разделе документации она скрыта: боковое меню там отведено под структуру
документации, а возврат на `/` выполняется по логотипу в его заголовке.
## Поведение бокового меню [#поведение-бокового-меню]
```tsx title="app/docs/layout.tsx"
links={[]}
sidebar={{ defaultOpenLevel: EXPAND_ALL_LEVELS }}
tabMode="auto"
```
| Параметр | Действие |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `links={[]}` | Убирает навигацию портала из бокового меню. |
| `sidebar.defaultOpenLevel` | Разворачивает разделы при первой отрисовке: папка открыта, если `defaultOpenLevel >= depth`. Константа `EXPAND_ALL_LEVELS` заведомо больше любой реальной вложенности, поэтому раскрыто всё дерево — включая разделы, добавленные позже. |
| `tabs` | Список продуктов для переключателя, см. ниже. |
| `tabMode="auto"` | Рисует переключатель выпадающим списком над боковым меню. |
Значение Fumadocs по умолчанию — `0`, все папки свёрнуты. Уменьшите
`defaultOpenLevel` до `1`, чтобы открыть только верхний уровень. Поведение
отдельной папки переопределяется полем `defaultOpen` в её `meta.json`.
### Переключатель продуктов на корневой странице [#переключатель-продуктов-на-корневой-странице]
Fumadocs строит вкладки из каталогов с `"root": true`, но рисует выпадающий
список **только пока какая-то вкладка активна**. На `/docs` читатель находится
вне любого продукта, поэтому переключатель исчезал именно там, где он нужнее
всего.
Поэтому список вкладок собирается вручную, и первым элементом идёт обзорная
страница:
```tsx title="app/docs/layout.tsx"
const tabs: LayoutTab[] = [
{
title: 'Обзор портала',
description: 'Точка входа и правила навигации по порталу',
url: docsRoute,
icon: ,
},
...getLayoutTabs(tree),
];
```
Вкладка без привязки к дереву страниц (`$folder`) активна по вложенному URL,
поэтому `/docs` делает её активной — как и любая страница внутри. Выпадающий
список выбирает **последнюю** подходящую вкладку, отсюда порядок: внутри
продукта побеждает его собственная вкладка, на корне остаётся только обзор.
Обзорная запись попадает и в сам список — это одновременно путь назад к `/docs`.
## Страницы-заглушки [#страницы-заглушки]
`/swagger` и `/flows` по умолчанию показывают страницу «раздел в подготовке».
Тексты и планируемый адрес задаются в конфигурации:
```ts
placeholders: {
swagger: {
title: 'Swagger-портал',
eyebrow: 'Раздел в подготовке',
description: 'Здесь появится интерактивная спецификация OpenAPI.',
target: 'https://swagger.example.com',
},
},
```
### Замена заглушки редиректом [#замена-заглушки-редиректом]
```tsx title="app/swagger/page.tsx"
import { redirect } from 'next/navigation';
import { portalConfig } from '@/lib/portal.config';
export default function SwaggerPage() {
redirect(portalConfig.placeholders.swagger.target);
}
```
Альтернатива без страницы — редирект на уровне конфигурации Next.js:
```js title="next.config.mjs"
const config = {
reactStrictMode: true,
output: 'standalone',
async redirects() {
return [
{
source: '/swagger',
destination: 'https://swagger.example.com',
permanent: false,
},
];
},
};
```
### Удаление раздела [#удаление-раздела]
Удалите каталог `app/swagger/` и соответствующую запись из `externalPortals` и
`placeholders`.
## Смена языка интерфейса [#смена-языка-интерфейса]
Атрибут `lang` задан в `app/layout.tsx`:
```tsx
```
Подписи навигации находятся в `lib/layout.shared.tsx`, тексты главной страницы —
в `app/page.tsx` и `lib/portal.config.ts`.
## Брендинг [#брендинг]
Палитра, шрифты и логотипы описаны в разделе
[Бренд и стили](/docs/doc-portal/brand-and-styles).
# Опубликовать портал (/docs/doc-portal/how-to/deploy)
## Непрерывная интеграция [#непрерывная-интеграция]
`.github/workflows/ci.yml` запускается на push в `main` и на каждый pull request:
```bash
pnpm install --frozen-lockfile
pnpm types:check
pnpm build
```
Сборка фиксирует Node.js 24 и pnpm 11.7.0 — те же версии, что в `package.json`.
## Docker [#docker]
Сборка образа:
```bash
docker build -t re-doc-portal:latest .
```
Запуск:
```bash
docker run -d --name re-doc-portal --restart unless-stopped -p 3000:3000 -e HOSTNAME=0.0.0.0 -e PORT=3000 -e NEXT_PUBLIC_SITE_URL=https://docs.example.com re-doc-portal:latest
```
Локальная сборка и запуск через compose:
```bash
docker compose up -d --build
```
`Dockerfile` использует standalone-режим Next.js: в финальный образ попадают
только `server.js`, необходимые зависимости и статика.
## Dokploy [#dokploy]
Рекомендуемый способ развертывания — приложение Dokploy на основе имеющегося
`Dockerfile`.
### Подключить репозиторий [#подключить-репозиторий]
В разделе `Git` создайте или установите Dokploy GitHub App и выдайте доступ к
репозиторию портала.
### Создать приложение [#создать-приложение]
| Параметр | Значение |
| ----------------- | ------------------- |
| Provider | `GitHub` |
| Repository | репозиторий портала |
| Branch | `main` |
| Build path | `/` |
| Build type | `Dockerfile` |
| Dockerfile path | `Dockerfile` |
| Port | `3000` |
| Health check path | `/docs` |
### Задать переменные окружения [#задать-переменные-окружения]
```ini
HOSTNAME=0.0.0.0
PORT=3000
NEXT_PUBLIC_SITE_URL=https://docs.example.com
```
### Настроить домен [#настроить-домен]
На вкладке `Domains` добавьте домен, укажите порт сервиса `3000` и включите
HTTPS, затем выполните развертывание.
Dokploy пересобирает приложение автоматически при появлении новых коммитов в
выбранной ветке.
## Обновление на сервере вручную [#обновление-на-сервере-вручную]
```bash
git pull && docker compose up -d --build
```
## Проверка после развертывания [#проверка-после-развертывания]
* `/` — главная страница, карточки продуктов;
* `/docs` — документация, выпадающий список продуктов, боковое меню;
* поиск по документации;
* светлая и темная тема;
* `/llms.txt` и `/llms-full.txt`;
* Open Graph-изображение любой страницы: `/og/docs/<путь>/image.png`.
В качестве проверки готовности используется `/docs`, а не `/` — этот маршрут
зависит от коллекции документации и падает, если сборка контента не удалась.
# Руководства (/docs/doc-portal/how-to)
## Проверка после любых изменений [#проверка-после-любых-изменений]
```bash
pnpm types:check
```
```bash
pnpm build
```
Дополнительно проверьте вручную:
* главную страницу `/` и карточки продуктов;
* выпадающий список продуктов и положение измененной страницы в боковом меню;
* поиск по документации;
* светлую и темную тему;
* внутренние ссылки и оглавление;
* отображение навигации на мобильном разрешении.