# Обзор портала (/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`. | <Callout type="warn" title="Синхронизация с содержимым"> `products[]` описывает карточки на главной странице, а выпадающий список строится из каталогов с `"root": true`. Это два независимых источника — добавляя продукт, обновите оба: каталог в `content/docs/` и запись в конфиге. </Callout> ## Слой 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) ## Три уровня иерархии [#три-уровня-иерархии] <Mermaid chart="graph LR ROOT["content/docs<br/>meta.json"] --> P1["Продукт A<br/>root: true"] ROOT --> P2["Продукт B<br/>root: true"] P1 --> S1["Раздел 1<br/>meta.json"] P1 --> S2["Раздел 2<br/>meta.json"] S1 --> PG1["страница.mdx"] S1 --> PG2["страница.mdx"] S2 --> PG3["страница.mdx"]" /> ### Продукт [#продукт] Каталог первого уровня внутри `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` формируют содержимое пункта списка. <Callout title="Один продукт — один пункт дропдауна"> Пока пользователь находится внутри продукта, боковое меню показывает только его разделы. Переход к другому продукту выполняется через выпадающий список. </Callout> ### Раздел [#раздел] Вложенный каталог **без** `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` | да | Заголовок страницы, пункт бокового меню, `<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. <Callout type="warn" title="Типичная ошибка"> Файл создан, но не добавлен в `pages` — страница «пропадает» из бокового меню. Всегда перечисляйте страницы явно. </Callout> ## Иконки [#иконки] Значение `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`-файлы в типизированную коллекцию, а единственный динамический маршрут рендерит любую страницу документации. ## Общая схема [#общая-схема] <Mermaid chart="graph TD subgraph Source [Исходники] MDX["content/docs/**/*.mdx"] META["content/docs/**/meta.json"] CFG["lib/portal.config.ts"] end subgraph Build [Сборка] FMDX["fumadocs-mdx<br/>(.source/)"] LOADER["lib/source.ts<br/>loader()"] end subgraph Runtime [Маршруты Next.js] TREE["app/docs/layout.tsx<br/>дерево + дропдаун продуктов"] PAGE["app/docs/[[...slug]]/page.tsx<br/>рендер страницы"] SEARCH["app/api/search"] LLM["/llms.txt, /llms-full.txt"] OG["/og/docs/*"] end MDX --> FMDX META --> FMDX FMDX --> LOADER LOADER --> TREE LOADER --> PAGE LOADER --> SEARCH LOADER --> LLM LOADER --> OG CFG --> TREE CFG --> PAGE" /> ## Ключевые решения [#ключевые-решения] | Решение | Обоснование | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Контент в репозитории, а не в CMS | Документация проходит тот же review, что и код; версия документации совпадает с версией продукта. | | Один динамический маршрут `[[...slug]]` | Новая страница не требует изменений в коде — достаточно `.mdx`-файла. | | Структура из `meta.json`, а не из имен файлов | Порядок и названия разделов управляются явно, переименование файла не ломает навигацию. | | Продукт = каталог с `"root": true` | Fumadocs автоматически строит выпадающий список продуктов; добавление продукта — операция над содержимым, не над кодом. | | Вся настройка в `lib/portal.config.ts` | Развертывание нового портала не требует правок в компонентах. | | Брендовые токены в CSS-переменных | Fumadocs UI читает `--color-fd-*`; переопределение палитры брендирует все встроенные компоненты сразу. | ## Подробнее [#подробнее] <Cards> <Card href="/docs/doc-portal/architecture/content-model" title="Модель контента" description="Продукты, разделы, страницы и правила формирования URL." /> <Card href="/docs/doc-portal/architecture/build-pipeline" title="Конвейер сборки" description="От MDX до статических страниц: fumadocs-mdx, loader, маршруты." /> <Card href="/docs/doc-portal/architecture/configuration-layers" title="Слои конфигурации" description="Что где настраивается и какой файл править для чего." /> </Cards> # Ассеты (/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'; // Полный логотип с надписью <Logo variant="horizontal" className="h-10" priority /> // Только знак R/E <Logo variant="mark" className="h-6" /> ``` | Свойство | Значение | Назначение | | ----------- | ---------------------- | ------------------------------------------------------------------- | | `variant` | `horizontal` \| `mark` | Полный логотип или только знак. По умолчанию `horizontal`. | | `className` | классы Tailwind | Задавайте только высоту (`h-*`) — ширина вычисляется автоматически. | | `priority` | `boolean` | `true` для логотипа выше первого экрана. | <Callout type="warn" title="Не задавайте ширину"> Компонент использует `w-auto object-contain`. Фиксация ширины исказит пропорции логотипа. </Callout> ## Паттерн в качестве фона [#паттерн-в-качестве-фона] Готовая утилита из `app/global.css`: ```tsx <div className="re-pattern-panel relative overflow-hidden rounded-[2rem] p-8"> <div className="absolute inset-0 bg-gradient-to-r from-re-navy via-re-navy/95 to-re-navy/55" /> <div className="relative">{/* содержимое */}</div> </div> ``` Градиентный оверлей поверх паттерна обязателен: он гасит геометрию под текстом и обеспечивает читаемость. Без него паттерн конкурирует с содержимым. ## Иконка продукта [#иконка-продукта] `components/brand/brand-icon.tsx` помещает иконку Lucide в плашку с мягким брендовым градиентом. Иконки разрешаются по той же карте имен, что использует Fumadocs для `meta.json`, поэтому карточка продукта и пункт выпадающего списка получают один и тот же глиф. ```tsx import { BookOpen } from 'lucide-react'; import { BrandIcon } from '@/components/brand/brand-icon'; <BrandIcon name="CreditCard" fallback={BookOpen} /> ``` ## Favicon [#favicon] `app/icon.png` — знак R/E на фирменном градиенте, 512×512. Next.js обрабатывает файл по соглашению App Router и подставляет нужные размеры автоматически. ## Замена ассетов под другой бренд [#замена-ассетов-под-другой-бренд] <Steps> <Step> ### Подготовить файлы [#подготовить-файлы] Нужны четыре варианта логотипа: горизонтальный и знак, каждый в темном и светлом исполнении, PNG с прозрачным фоном. </Step> <Step> ### Заменить файлы в public/brand/ [#заменить-файлы-в-publicbrand] Сохраните имена файлов — тогда правки в коде не потребуются. При других именах обновите `brandAssets` в `lib/brand.ts`. </Step> <Step> ### Обновить favicon [#обновить-favicon] Замените `app/icon.png` изображением 512×512. </Step> <Step> ### Проверить обе темы [#проверить-обе-темы] Логотип в навбаре, hero-блок главной страницы, подвал, страницы-заглушки. </Step> </Steps> # Цвета и градиент (/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 <Link href="/docs" className="re-gradient-surface rounded-full px-6 py-3 font-semibold"> Открыть документацию </Link> ``` <Callout title="Контраст на градиенте"> Градиент светлый, поэтому текст на нем — темно-синий (`--color-re-navy`), а не белый. `.re-gradient-surface` задает такой цвет текста по умолчанию. </Callout> ## Переопределение темы 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` при этом используются без изменений там, где они работают как плашка или обводка, а не как цвет текста: градиенты, кольца фокуса, подсветка при наведении. ## Изменение палитры под другой бренд [#изменение-палитры-под-другой-бренд] <Steps> <Step> ### Обновить константы [#обновить-константы] ```ts title="lib/brand.ts" export const brandColors = { green: '#0DE644', cyan: '#45E1FF', navy: '#000D2B', // ... } as const; ``` </Step> <Step> ### Обновить токены и палитру Fumadocs [#обновить-токены-и-палитру-fumadocs] В `app/global.css` — блок `@theme`, переменные `--re-gradient` и наборы `--color-fd-*` для `:root` и `.dark`. </Step> <Step> ### Проверить контраст [#проверить-контраст] Проверьте светлую и темную тему: основной текст, ссылки, состояния наведения и фокуса, выделенные блоки `Callout`. </Step> </Steps> # Бренд и стили (/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 на градиенте. | <Callout type="warn" title="Два источника одних значений"> Палитра продублирована в `lib/brand.ts` и `app/global.css`: первый нужен React (Open Graph-изображения, инлайновые стили), второй — CSS и Fumadocs UI. При изменении цвета правьте оба файла. </Callout> ## Подробнее [#подробнее] <Cards> <Card href="/docs/doc-portal/brand-and-styles/colors" title="Цвета и градиент" description="Палитра, фирменный градиент и переопределение темы Fumadocs." /> <Card href="/docs/doc-portal/brand-and-styles/typography" title="Типографика" description="Ubuntu и Nunito Sans, доступные начертания и шкала заголовков." /> <Card href="/docs/doc-portal/brand-and-styles/assets" title="Ассеты" description="Логотипы, паттерны, favicon и правила их использования." /> </Cards> # Типографика (/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; ``` Системные шрифты в конце стека — страховка на случай, если веб-шрифт не загрузился. ## Доступные начертания [#доступные-начертания] <Callout type="warn" title="Ubuntu — не переменный шрифт"> В Ubuntu существуют только начертания **300, 400, 500 и 700**. Запрос промежуточного веса (например, `font-semibold`, то есть 600) заставит браузер синтезировать начертание — буквы получатся искусственно утолщёнными. Nunito Sans переменный, для него ограничений нет. </Callout> ```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 <h2 className="font-display text-2xl font-medium tracking-tight"> Продукты </h2> ``` Обратите внимание на `font-medium` (500), а не `font-semibold` (600) — по причине, описанной выше. ## Шкала заголовков [#шкала-заголовков] | Элемент | Начертание | Трекинг | | ------------------------------- | ---------- | --------- | | `h1`, `.re-display` | 700 | `-0.01em` | | `h2`, `h3` | 500 | `-0.01em` | | Заголовок страницы документации | 700 | `-0.02em` | | Основной текст | 400 | обычный | ## Замена шрифтов [#замена-шрифтов] <Steps> <Step> ### Проверить наличие кириллицы [#проверить-наличие-кириллицы] Прежде чем выбрать гарнитуру, убедитесь, что в Google Fonts у неё есть набор `cyrillic`: ```bash node -e "const d=require('next/dist/compiled/@next/font/dist/google/font-data.json'); console.log(d['Ubuntu'])" ``` Если набора нет, добавьте кириллическую гарнитуру следующим элементом стека — браузер подбирает шрифт отдельно для каждого глифа. </Step> <Step> ### Подключить гарнитуры [#подключить-гарнитуры] ```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` элемента `<html>`. Для непеременных гарнитур укажите массив `weight`. </Step> <Step> ### Обновить стек и начертания [#обновить-стек-и-начертания] ```css title="app/global.css" @theme { --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif; } ``` Проверьте, что веса заголовков в `app/global.css` и классы `font-*` в компонентах входят в набор, доступный у новой гарнитуры. </Step> <Step> ### Обновить константы [#обновить-константы] `lib/brand.ts` — `brandFonts.body`, `brandFonts.display` и `brandFonts.displayWeights` — единственный источник названий гарнитур для документации и вспомогательных скриптов. </Step> </Steps> # Быстрый старт (/docs/doc-portal/getting-started) Раздел описывает минимальный путь от клонирования репозитория до работающего портала с собственным содержимым. ## Порядок действий [#порядок-действий] <Steps> <Step> ### Подготовить окружение [#подготовить-окружение] Установить Node.js 24 и pnpm 11.7, склонировать репозиторий и установить зависимости — см. [Установка и запуск](/docs/doc-portal/getting-started/installation). </Step> <Step> ### Настроить портал [#настроить-портал] Заполнить `lib/portal.config.ts`: название, описание, координаты репозитория и список продуктов — см. [Настройка портала](/docs/doc-portal/how-to/configure-portal). </Step> <Step> ### Наполнить документацию [#наполнить-документацию] Создать каталог продукта в `content/docs/` и разделы внутри него — см. [Добавление продукта](/docs/doc-portal/how-to/add-product). </Step> <Step> ### Опубликовать [#опубликовать] Собрать Docker-образ и развернуть через Dokploy или compose — см. [Публикация портала](/docs/doc-portal/how-to/deploy). </Step> </Steps> ## Что дальше [#что-дальше] <Cards> <Card href="/docs/doc-portal/getting-started/installation" title="Установка и запуск" description="Требования, команды разработки и проверки." /> <Card href="/docs/doc-portal/getting-started/project-structure" title="Структура репозитория" description="Назначение каждого каталога и ключевого файла." /> </Cards> # Установка и запуск (/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`), поэтому локальный прогон экономит цикл сборки. <Callout type="warn" title="Каталоги, которые нельзя править вручную"> `.source/`, `.next/`, `node_modules/` и `tsconfig.tsbuildinfo` генерируются инструментами. Они исключены из репозитория через `.gitignore`. </Callout> ## Запуск в 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) ## Дерево репозитория [#дерево-репозитория] <Files> <Folder name="app"> <File name="layout.tsx" /> <File name="page.tsx" /> <File name="global.css" /> <File name="icon.png" /> <Folder name="docs"> <File name="layout.tsx" /> <File name="[[...slug]]/page.tsx" /> </Folder> <Folder name="api/search"> <File name="route.ts" /> </Folder> <Folder name="og/docs/[...slug]"> <File name="route.tsx" /> </Folder> </Folder> <Folder name="components"> <Folder name="brand"> <File name="logo.tsx" /> <File name="brand-icon.tsx" /> </Folder> <Folder name="mdx"> <File name="mermaid.tsx" /> </Folder> <File name="mdx.tsx" /> <File name="placeholder-page.tsx" /> </Folder> <Folder name="content/docs"> <File name="meta.json" /> <File name="index.mdx" /> <Folder name="doc-portal" /> </Folder> <Folder name="lib"> <File name="portal.config.ts" /> <File name="brand.ts" /> <File name="shared.ts" /> <File name="layout.shared.tsx" /> <File name="source.ts" /> <File name="cn.ts" /> </Folder> <Folder name="docs" /> <Folder name="public/brand" /> <File name="AGENT.md" /> <File name="README.md" /> <File name="DEPLOYMENT.md" /> <File name="source.config.ts" /> <File name="next.config.mjs" /> <File name="proxy.ts" /> <File name="Dockerfile" /> </Files> ## Маршруты приложения [#маршруты-приложения] | Путь | Назначение | | -------------------------------------------- | -------------------------------------------------------------------------- | | `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) Продукт — верхний уровень навигации. Каждый продукт становится отдельным пунктом выпадающего списка над боковым меню, а под ним отображаются только его разделы и страницы. <Steps> <Step> ### Создать каталог продукта [#создать-каталог-продукта] Имя каталога попадает в публичный URL — латиница, kebab-case. ```text content/docs/payment-gateway/ ``` </Step> <Step> ### Описать продукт в 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` | Порядок разделов и страниц внутри продукта. | </Step> <Step> ### Создать обзорную страницу [#создать-обзорную-страницу] ```mdx title="content/docs/payment-gateway/index.mdx" --- title: Payment Gateway description: Платежный шлюз — назначение, состав и порядок подключения. icon: CreditCard --- Краткое описание продукта и карточки его разделов. <Cards> <Card href="/docs/payment-gateway/getting-started" title="Быстрый старт" description="Подключение и первый платеж." /> </Cards> ``` </Step> <Step> ### Добавить продукт в корневой meta.json [#добавить-продукт-в-корневой-metajson] ```json title="content/docs/meta.json" { "title": "Документация", "pages": ["index", "doc-portal", "payment-gateway"] } ``` Порядок элементов определяет порядок продуктов в выпадающем списке. </Step> <Step> ### Добавить карточку на главную страницу [#добавить-карточку-на-главную-страницу] Выпадающий список строится из каталогов автоматически, а карточки на главной странице берутся из конфигурации — это отдельный источник. ```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`, чтобы карточка и пункт списка выглядели согласованно. </Step> <Step> ### Наполнить разделы [#наполнить-разделы] Далее — по инструкции [Добавить раздел и страницу](/docs/doc-portal/how-to/add-section-and-page). </Step> <Step> ### Проверить [#проверить] ```bash pnpm types:check ``` ```bash pnpm build ``` Убедитесь, что продукт появился в выпадающем списке, а его разделы — в боковом меню. </Step> </Steps> ## Итоговая структура [#итоговая-структура] <Files> <Folder name="content/docs"> <File name="meta.json" /> <File name="index.mdx" /> <Folder name="doc-portal" /> <Folder name="payment-gateway"> <File name="meta.json" /> <File name="index.mdx" /> <Folder name="getting-started" /> <Folder name="api" /> <Folder name="operations" /> </Folder> </Folder> </Files> <Callout type="warn" title="Частая ошибка"> Забытый `"root": true` — каталог отображается как обычный раздел внутри предыдущего продукта и не появляется в выпадающем списке. </Callout> # Добавить раздел и страницу (/docs/doc-portal/how-to/add-section-and-page) ## Добавить раздел [#добавить-раздел] <Steps> <Step> ### Создать каталог внутри продукта [#создать-каталог-внутри-продукта] ```text content/docs/payment-gateway/api/ ``` </Step> <Step> ### Описать раздел [#описать-раздел] Без `"root": true` — иначе раздел станет отдельным продуктом. ```json title="content/docs/payment-gateway/api/meta.json" { "title": "API", "description": "Спецификации операций и коды ошибок", "icon": "Braces", "pages": ["index", "authentication", "payments", "errors"] } ``` </Step> <Step> ### Добавить раздел в meta.json продукта [#добавить-раздел-в-metajson-продукта] ```json title="content/docs/payment-gateway/meta.json" { "pages": ["index", "getting-started", "api", "operations"] } ``` </Step> </Steps> ## Добавить страницу [#добавить-страницу] <Steps> <Step> ### Создать .mdx-файл [#создать-mdx-файл] Имя файла становится последним сегментом URL. ```text content/docs/payment-gateway/api/authentication.mdx ``` </Step> <Step> ### Заполнить frontmatter [#заполнить-frontmatter] ```mdx --- title: Аутентификация description: Получение токена и подпись запросов к платежному шлюзу. icon: KeyRound --- ## Получение токена Содержимое страницы... ``` </Step> <Step> ### Добавить страницу в meta.json раздела [#добавить-страницу-в-metajson-раздела] ```json { "pages": ["index", "authentication", "payments", "errors"] } ``` Страница доступна по адресу `/docs/payment-gateway/api/authentication`. </Step> </Steps> ## Управление порядком и группировкой [#управление-порядком-и-группировкой] Массив `pages` поддерживает несколько специальных элементов. ```json { "pages": [ "index", "---Основное---", "authentication", "payments", "---Справочник---", "errors", "..." ] } ``` | Элемент | Действие | | ------------------- | -------------------------------------------- | | `"index"` | Обзорная страница раздела. | | `"имя-файла"` | Страница без расширения `.mdx`. | | `"имя-каталога"` | Вложенный раздел. | | `"---Заголовок---"` | Визуальный разделитель с подписью. | | `"..."` | Все неперечисленные элементы в конец списка. | ## Доступные компоненты в MDX [#доступные-компоненты-в-mdx] Зарегистрированы в `components/mdx.tsx` и не требуют импорта на странице. | Компонент | Назначение | | ----------------------------------------------------------- | -------------------------------------------------- | | `<Callout type="info \| warn \| error \| success \| idea">` | Выделенный блок с примечанием или предупреждением. | | `<Cards>`, `<Card href title description>` | Сетка ссылок-карточек. | | `<Steps>`, `<Step>` | Пошаговая инструкция. | | `<Tabs>`, `<Tab>` | Варианты одной инструкции. | | `<Files>`, `<Folder>`, `<File>` | Дерево файлов. | | `<Accordions>`, `<Accordion>` | Сворачиваемые блоки. | | Блок кода с языком `mermaid` | Диаграмма Mermaid. | ### Пример: варианты команды [#пример-варианты-команды] ````mdx <Tabs items={['pnpm', 'npm']}> <Tab value="pnpm"> ```bash pnpm install ``` </Tab> <Tab value="npm"> ```bash npm install ``` </Tab> </Tabs> ```` ### Пример: диаграмма [#пример-диаграмма] ````mdx ```mermaid graph LR Client --> Gateway --> Provider ``` ```` ## Заголовок блока кода [#заголовок-блока-кода] Атрибут `title` в информационной строке блока подписывает фрагмент именем файла: ````mdx ```json title="content/docs/payment-gateway/meta.json" { "title": "Payment Gateway" } ``` ```` ## Перемещение и переименование [#перемещение-и-переименование] <Callout type="warn" title="Проверьте ссылки"> При переименовании или переносе `.mdx`-файла обновите `meta.json` и внутренние ссылки на страницу. Старый URL перестает существовать — при необходимости добавьте редирект в `next.config.mjs`. </Callout> # Настроить портал (/docs/doc-portal/how-to/configure-portal) Настройка нового портала сводится к правке `lib/portal.config.ts`. Компоненты и layout читают значения оттуда. ## Обязательный минимум [#обязательный-минимум] <Steps> <Step> ### Название и описание [#название-и-описание] ```ts title="lib/portal.config.ts" export const portalConfig = { name: 'Payment Gateway Documentation', shortName: 'PG Docs', tagline: 'Документация платежного шлюза', description: 'Подключение, спецификации API и эксплуатационные материалы.', // ... }; ``` | Поле | Где отображается | | ------------- | ------------------------------------------------------ | | `name` | Шаблон `<title>` всех страниц, Open Graph-изображения. | | `shortName` | Подпись рядом с логотипом в навбаре. | | `tagline` | Заголовок hero-блока главной страницы. | | `description` | Подзаголовок hero-блока и `meta description`. | </Step> <Step> ### Координаты репозитория [#координаты-репозитория] ```ts git: { user: 'ReactiveEngineering', repo: 'payment-gateway-docs', branch: 'main', }, ``` Отсюда формируются ссылка на репозиторий в навбаре и кнопка «View on GitHub» на каждой странице документации. <Callout type="warn" title="Проверьте имя репозитория"> Несовпадение `repo` с фактическим именем репозитория даёт битые ссылки на каждой странице документации — внешне портал при этом работает нормально. </Callout> </Step> <Step> ### Список продуктов [#список-продуктов] ```ts products: [ { slug: 'payment-gateway', title: 'Payment Gateway', description: 'Платежный шлюз: подключение, API и эксплуатация.', icon: 'CreditCard', }, ], ``` `slug` — имя каталога в `content/docs/`. Подробнее: [Добавить продукт](/docs/doc-portal/how-to/add-product). </Step> <Step> ### Адрес портала [#адрес-портала] ```ini title=".env" NEXT_PUBLIC_SITE_URL=https://docs.example.com ``` Значение попадает в `metadataBase` и делает Open Graph-ссылки абсолютными. </Step> </Steps> ## Связанные порталы и навигация [#связанные-порталы-и-навигация] Массив `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"` | Рисует переключатель выпадающим списком над боковым меню. | <Callout title="Как свернуть дерево"> Значение Fumadocs по умолчанию — `0`, все папки свёрнуты. Уменьшите `defaultOpenLevel` до `1`, чтобы открыть только верхний уровень. Поведение отдельной папки переопределяется полем `defaultOpen` в её `meta.json`. </Callout> ### Переключатель продуктов на корневой странице [#переключатель-продуктов-на-корневой-странице] Fumadocs строит вкладки из каталогов с `"root": true`, но рисует выпадающий список **только пока какая-то вкладка активна**. На `/docs` читатель находится вне любого продукта, поэтому переключатель исчезал именно там, где он нужнее всего. Поэтому список вкладок собирается вручную, и первым элементом идёт обзорная страница: ```tsx title="app/docs/layout.tsx" const tabs: LayoutTab[] = [ { title: 'Обзор портала', description: 'Точка входа и правила навигации по порталу', url: docsRoute, icon: <House />, }, ...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 <html lang="ru" suppressHydrationWarning> ``` Подписи навигации находятся в `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`. <Steps> <Step> ### Подключить репозиторий [#подключить-репозиторий] В разделе `Git` создайте или установите Dokploy GitHub App и выдайте доступ к репозиторию портала. </Step> <Step> ### Создать приложение [#создать-приложение] | Параметр | Значение | | ----------------- | ------------------- | | Provider | `GitHub` | | Repository | репозиторий портала | | Branch | `main` | | Build path | `/` | | Build type | `Dockerfile` | | Dockerfile path | `Dockerfile` | | Port | `3000` | | Health check path | `/docs` | </Step> <Step> ### Задать переменные окружения [#задать-переменные-окружения] ```ini HOSTNAME=0.0.0.0 PORT=3000 NEXT_PUBLIC_SITE_URL=https://docs.example.com ``` </Step> <Step> ### Настроить домен [#настроить-домен] На вкладке `Domains` добавьте домен, укажите порт сервиса `3000` и включите HTTPS, затем выполните развертывание. </Step> </Steps> Dokploy пересобирает приложение автоматически при появлении новых коммитов в выбранной ветке. ## Обновление на сервере вручную [#обновление-на-сервере-вручную] ```bash git pull && docker compose up -d --build ``` ## Проверка после развертывания [#проверка-после-развертывания] * `/` — главная страница, карточки продуктов; * `/docs` — документация, выпадающий список продуктов, боковое меню; * поиск по документации; * светлая и темная тема; * `/llms.txt` и `/llms-full.txt`; * Open Graph-изображение любой страницы: `/og/docs/<путь>/image.png`. <Callout title="Health check"> В качестве проверки готовности используется `/docs`, а не `/` — этот маршрут зависит от коллекции документации и падает, если сборка контента не удалась. </Callout> # Руководства (/docs/doc-portal/how-to) <Cards> <Card href="/docs/doc-portal/how-to/add-product" title="Добавить продукт" description="Новый каталог с root: true — новый пункт выпадающего списка продуктов." /> <Card href="/docs/doc-portal/how-to/add-section-and-page" title="Добавить раздел и страницу" description="Структура внутри продукта, frontmatter и порядок в боковом меню." /> <Card href="/docs/doc-portal/how-to/configure-portal" title="Настроить портал" description="Название, репозиторий, навигация и страницы-заглушки." /> <Card href="/docs/doc-portal/how-to/deploy" title="Опубликовать портал" description="Docker, GitHub Actions и развертывание через Dokploy." /> </Cards> ## Проверка после любых изменений [#проверка-после-любых-изменений] ```bash pnpm types:check ``` ```bash pnpm build ``` Дополнительно проверьте вручную: * главную страницу `/` и карточки продуктов; * выпадающий список продуктов и положение измененной страницы в боковом меню; * поиск по документации; * светлую и темную тему; * внутренние ссылки и оглавление; * отображение навигации на мобильном разрешении.