Compare commits

..

18 Commits

Author SHA1 Message Date
028a69f3ac docs: переработать заголовки и описания разделов
All checks were successful
CI/CD Pipeline / docker (push) Successful in 51s
CI/CD Pipeline / deploy (push) Successful in 9s
- Зафиксированы новые правила в CONTRIBUTING.md: заголовок и описание
  как навигационный маркер, запреты, тесты и подходящие формы
- Описания во всех 26 разделах переписаны как короткий ответ на
  вопрос «когда сюда нужно?»
- Заголовки приведены к самодостаточному виду: SLM Design, SVG-спрайты,
  VS Code, Чистая установка Next.js, Источники данных, Создание
  проекта вручную, Стили, Алиасы импортов
- Описания добавлены в frontmatter (description) для согласованности
  с llms.txt и SEO
- Удалены навигационные ссылки сразу после описания
- Добавлен docs-overview.md — сводный обзор всех страниц
- Обновлён сайдбар: ссылка «Алиасы» → «Алиасы импортов»
2026-04-28 15:35:10 +03:00
a9ea898220 sync (черновик) 2026-04-28 10:52:09 +03:00
7965ec2a96 docs: актуализировать структуру разделов и заблокировать пустые
All checks were successful
CI/CD Pipeline / docker (push) Successful in 47s
CI/CD Pipeline / deploy (push) Successful in 7s
- sidebar: «Данные» поднят сразу после «Страницы (App Router)»
- sidebar: пустые разделы (Изображения, Видео, Stores, Хуки, Шрифты,
  Локализация) оставлены без link с пометкой «· в разработке»
- index.md приведён к актуальной структуре sidebar (Workflow, Базовые
  правила, Установка и настройка, Использование)
- README.md перегенерирован
2026-04-27 12:04:34 +03:00
7c224fed99 fix(generate-llms): корректные ссылки в корневом README
- ссылки /docs/... преобразованы в относительные пути docs/docs/*.md
- корневые ресурсы (/llms.txt, *.zip и т.п.) переписаны на абсолютный SITE_URL
- после описания добавлена строка с URL сайта
2026-04-27 12:04:31 +03:00
74cbd43a23 feat: чистые URL без .html
All checks were successful
CI/CD Pipeline / docker (push) Successful in 46s
CI/CD Pipeline / deploy (push) Successful in 7s
- VitePress: включён cleanUrls
- Caddyfile: try_files пробует .html для запросов без расширения
- Caddyfile: 301-редирект /foo.html → /foo для совместимости
- sitemap.xml: канонические URL без .html
2026-04-27 10:12:30 +03:00
ef58a02609 fix: унифицировать ссылки на лендинге для краулеров
All checks were successful
CI/CD Pipeline / docker (push) Successful in 53s
CI/CD Pipeline / deploy (push) Successful in 7s
- относительные пути (./docs/, ./nextjs-style-guide.zip) заменены на абсолютные
- убраны target="_blank" и rel="noopener" у llms-кнопок: некоторые краулеры считают такие ссылки внешними и не следуют по ним
2026-04-27 09:54:37 +03:00
e265799c26 fix: устранить проблемы доступности llms.txt для AI-агентов
All checks were successful
CI/CD Pipeline / docker (push) Successful in 51s
CI/CD Pipeline / deploy (push) Successful in 8s
- Caddyfile: text/plain применяется только к существующим .txt/.md
- Caddyfile: несуществующие .txt/.md отдают 404 вместо HTML-фолбэка
- Caddyfile: добавлены редиректы /docs/llms*.txt → /llms*.txt
- Caddyfile: добавлен HTTP-заголовок Link с rel="llms" (RFC 8288)
- лендинг: относительные ссылки на llms.txt заменены на абсолютные
- лендинг: добавлено явное упоминание путей в тексте карточки
- config: добавлены <link rel="alternate"> на llms.txt в <head>
- generate-llms: добавлена генерация robots.txt и sitemap.xml
2026-04-27 09:37:23 +03:00
781efc52f1 docs: добавить разделы установки проекта, Next.js, стилей и шаблонов
All checks were successful
CI/CD Pipeline / docker (push) Successful in 45s
CI/CD Pipeline / deploy (push) Successful in 5s
- Создание проекта: «Из шаблона» (clone через tiged) и «Вручную» (пошаговая сборка) — оформлены подгруппой
- Next.js: `create-next-app` с каноническими флагами, очистка демо-шаблона, минимальный layout
- Стили: базовая инфраструктура `variables.css` / `media.css` / `global.css` в `src/shared/styles/` без PostCSS
- Шаблоны генерации: установка `.templates/` через tiged + создание шаблона вручную
- Сайдбар «Установка и настройка» расширен новыми пунктами в порядке зависимости установки
2026-04-27 01:27:43 +03:00
ec01ae2e1c fix: рекурсивный обход sidebar для вложенных групп
- flattenSidebar поддерживал максимум два уровня вложенности — группы глубже теряли страницы в llms.txt и архиве
- Переписан на рекурсивный обход с накоплением префикса групп через `: `
- Раздел «Данные» (4 уровня вложенности) теперь полностью попадает в карту документации
2026-04-27 01:27:35 +03:00
3d93efd90a docs: добавить раздел «Данные» и реорганизовать документацию
All checks were successful
CI/CD Pipeline / docker (push) Successful in 44s
CI/CD Pipeline / deploy (push) Successful in 6s
- Добавлен раздел «Данные»: REST (автоматическая и ручная генерация клиентов, получение данных в server и client компонентах с инкапсуляцией SWR в хуках), Realtime, введение
- Прикладные разделы переименованы в «Использование», папка перенесена в `docs/docs/usage/`
- Создана группа «Установка и настройка» с папкой `docs/docs/setup/` — туда вынесены PostCSS, Biome, VS Code, алиасы и установка SVG-спрайтов
- Подгруппы «Стили» и «SVG-спрайты» в сайдбаре упразднены — страницы установки и использования разнесены по верхнеуровневым группам
- Удалён устаревший раздел `applied/api.md`
- Перекрёстные ссылки в workflow-разделах и внутри новых страниц синхронизированы с новыми путями
- CONTRIBUTING.md обновлён под новую структуру папок
2026-04-27 00:54:26 +03:00
e5e4ace91a docs: добавить раздел «Алиасы»
All checks were successful
CI/CD Pipeline / docker (push) Successful in 46s
CI/CD Pipeline / deploy (push) Successful in 6s
- Добавлен прикладной раздел с конфигом tsconfig.paths и правилами использования
- Зафиксирован отказ от префикса @/
- Пункт «Алиасы» добавлен в сайдбар после «Структуры проекта»
2026-04-26 23:00:00 +03:00
5a773a5b4f docs: добавить разделы biome, svg-спрайтов и сгруппировать стили
- Добавлен раздел «Biome» в прикладные разделы
- Раздел «SVG-спрайты» преобразован в группу: установка, использование
- Раздел «Стили» преобразован в группу: PostCSS, использование
- Обновлён сайдбар VitePress с новыми группами
- Перегенерирован README из главной страницы документации
2026-04-26 22:50:51 +03:00
f645b2ad40 refactor: удалить английскую локаль и упростить структуру
All checks were successful
CI/CD Pipeline / docker (push) Successful in 48s
CI/CD Pipeline / deploy (push) Successful in 7s
- Удалена английская версия документации (docs/en/) и артефакты en
- Контент перенесён docs/ru/ → docs/docs/, URL /ru/ заменён на /docs/
- Из .vitepress/config.ts убраны locales и enSidebar, оставлен один sidebar
- Из лендинга удалён переключатель языка ru/en и en-словарь
- generate-llms.ts переписан без параметра lang; llms.txt, llms-full.txt
  и nextjs-style-guide.zip генерируются в корень docs/public/
- README_RU.md занял место корневого README.md
- Обновлены CONTRIBUTING.md, custom.css, комментарий в Dockerfile
2026-04-26 15:04:10 +03:00
90bf360c06 docs: добавить раздел «Использование» на главных локалей
All checks were successful
CI/CD Pipeline / docker (push) Successful in 58s
CI/CD Pipeline / deploy (push) Successful in 7s
- блок поднят над «Структура документации» — это первое что видит
  читатель после описания
- разделён на две аудитории:
  * «Для AI-агентов»: llms.txt (карта) и llms-full.txt (полный текст)
  * «Для проекта»: ZIP-архив с распаковкой в ./ai/nextjs-style-guide/
- прямые URL на nextjs-style-guide.gromlab.ru
- README.md и README_RU.md (генерируются из docs/{ru,en}/index.md)
  обновлены автоматически
2026-04-25 21:26:45 +03:00
5cf0f0f8ba fix: переключатель языка и иконка репозитория в шапке
- восстановлены link для локалей: ru → /ru/, en → /en/ (были сломаны на /)
- добавлена иконка GitHub в socialLinks шапки VitePress для ru и en
  со ссылкой на https://gromlab.ru/docs/nextjs-style-guide
- root-локаль скрыта из переключателя языка через CSS (display: none
  для href="/"): в дропдауне теперь только Русский и English
- pill-кнопка «Репозиторий» добавлена в блок controls лендинга
  с иконкой GitHub, открывается в новой вкладке
- мобильная вёрстка лендинга переработана: контролы стопкой,
  репозиторий на ≤480px сжимается до иконки 36x36, увеличены
  отступы между блоками (hero / controls / cards) для разделения
2026-04-25 21:14:17 +03:00
464c709859 docs: убрать «воду» из вводных абзацев разделов
All checks were successful
CI/CD Pipeline / docker (push) Successful in 54s
CI/CD Pipeline / deploy (push) Successful in 6s
- удалены обороты «Раздел описывает», «Этот раздел описывает» из
  10 файлов docs/ru
- вводные абзацы переписаны в формате «тема: категории/области»
  без перечисления конкретного содержимого раздела
- удалён frontmatter description из basics/architecture/index.md
  (подтягивается первый абзац после h1 — про SLM Design)
- в CONTRIBUTING.md добавлен раздел «Вводный абзац» с правилами
  и блоками «Хорошо/Плохо»: что делать, чего избегать, проверка
  на излишнюю конкретику
2026-04-25 20:15:10 +03:00
64db18917b fix: синхронизировать EN-лендинг с русским
All checks were successful
CI/CD Pipeline / docker (push) Successful in 54s
CI/CD Pipeline / deploy (push) Successful in 6s
- обновлён EN tagline на лендинге под новый стиль (RU уже был обновлён)
- английская карточка «For Assistant» приведена к структуре русской:
  две кнопки llms.txt и llms-full.txt с заглушечными ссылками (#)
  и бейджем «in development»
- шаблон карточки теперь поддерживает badge при наличии buttons:
  карточка приглушается, кнопки не кликаются
2026-04-25 20:00:52 +03:00
ae103e962e feat: llms-full.txt, README архива и доработка лендинга
All checks were successful
CI/CD Pipeline / docker (push) Successful in 46s
CI/CD Pipeline / deploy (push) Successful in 8s
- добавлен генератор llms-full.txt: вся документация локали в одном
  файле с мета-якорями, порядок повторяет sidebar
- архив теперь содержит README.md как точку входа: карта документации
  с относительными ссылками, описаниями и метаинфо сборки
- ссылки /ru/... в .md-файлах архива преобразуются в относительные
  пути (через path.relative) — внутренняя навигация работает локально
- веб-index.md удаляется из архива (его роль выполняет README.md)
- llms-full.txt добавлен в архив для одноразового чтения LLM
- в sidebar добавлен пункт «Главная» / «Home» со ссылкой на корень локали
- карточка «Ассистенту» на лендинге: две кнопки llms.txt и llms-full.txt
  с открытием в новой вкладке
- активирована карточка «Скачать правила» (ru) с ссылкой на zip-архив
- удалён устаревший блок «Для ассистентов» из docs/{ru,en}/index.md
- обновлены описания на главных локалей и заменён FSD на SLM в EN
- в манифесте появилось поле llmsFull рядом с llms
2026-04-25 19:56:44 +03:00
90 changed files with 3102 additions and 1123 deletions

View File

@@ -1,93 +1,100 @@
import { defineConfig } from 'vitepress'; import { defineConfig } from 'vitepress';
const ruSidebar = [ const sidebar = [
{ {
text: 'Workflow', text: 'Главная',
link: '/ru/workflow', link: '/docs/',
},
{
text: 'Подсказки',
link: '/docs/workflow',
}, },
{ {
text: 'Базовые правила', text: 'Базовые правила',
items: [ items: [
{ text: 'Технологии и библиотеки', link: '/ru/basics/tech-stack' }, { text: 'Технологии и библиотеки', link: '/docs/basics/tech-stack' },
{ text: 'Именование', link: '/ru/basics/naming' }, { text: 'Именование', link: '/docs/basics/naming' },
{ {
text: 'Архитектура', text: 'Архитектура',
collapsed: true, collapsed: true,
items: [ items: [
{ text: 'Обзор', link: '/ru/basics/architecture/' }, { text: 'Обзор', link: '/docs/basics/architecture/' },
{ text: 'Слои', link: '/ru/basics/architecture/reference/layers' }, { text: 'Слои', link: '/docs/basics/architecture/reference/layers' },
{ text: 'Модули', link: '/ru/basics/architecture/reference/modules' }, { text: 'Модули', link: '/docs/basics/architecture/reference/modules' },
{ text: 'Сегменты', link: '/ru/basics/architecture/reference/segments' }, { text: 'Сегменты', link: '/docs/basics/architecture/reference/segments' },
], ],
}, },
{ text: 'Стиль кода', link: '/ru/basics/code-style' }, { text: 'Стиль кода', link: '/docs/basics/code-style' },
{ text: 'Документирование', link: '/ru/basics/documentation' }, { text: 'Документирование', link: '/docs/basics/documentation' },
{ text: 'Типизация', link: '/ru/basics/typing' }, { text: 'Типизация', link: '/docs/basics/typing' },
], ],
}, },
{ {
text: 'Прикладные разделы', text: 'Создание проекта',
items: [ items: [
{ text: 'Структура проекта', link: '/ru/applied/project-structure' }, { text: 'Из шаблона', link: '/docs/creating-project/from-template' },
{ text: 'Компоненты', link: '/ru/applied/components' }, { text: 'По гайду вручную', link: '/docs/creating-project/manual' },
{ text: 'Страницы (App Router)', link: '/ru/applied/page-level' }, { text: 'Чистый Next.js', link: '/docs/creating-project/nextjs' },
{ text: 'Шаблоны и генерация кода', link: '/ru/applied/templates-generation' },
{ text: 'Стили', link: '/ru/applied/styles' },
{ text: 'Изображения', link: '/ru/applied/images-sprites' },
{ text: 'SVG-спрайты', link: '/ru/applied/svg-sprites' },
{ text: 'Видео', link: '/ru/applied/video' },
{ text: 'API', link: '/ru/applied/api' },
{ text: 'Stores', link: '/ru/applied/stores' },
{ text: 'Хуки', link: '/ru/applied/hooks' },
{ text: 'Шрифты', link: '/ru/applied/fonts' },
{ text: 'Локализация', link: '/ru/applied/localization' },
{ text: 'Настройка VS Code', link: '/ru/applied/vscode' },
],
},
];
const enSidebar = [
{
text: 'Processes',
items: [
{ text: 'Getting Started', link: '/en/workflow/getting-started' },
{ text: 'Creating an App', link: '/en/workflow/creating-app' },
{ text: 'Creating Pages', link: '/en/workflow/creating-pages' },
{ text: 'Creating Components', link: '/en/workflow/creating-components' },
{ text: 'Styling', link: '/en/workflow/styling' },
{ text: 'Data Fetching', link: '/en/workflow/data-fetching' },
{ text: 'State Management', link: '/en/workflow/state-management' },
{ text: 'Localization', link: '/en/workflow/localization' },
], ],
}, },
{ {
text: 'Basic Rules', text: 'Настройка',
items: [ items: [
{ text: 'Tech Stack', link: '/en/basics/tech-stack' }, { text: 'Алиасы импортов', link: '/docs/setup/aliases' },
{ text: 'Architecture', link: '/en/basics/architecture' }, { text: 'Biome', link: '/docs/setup/biome' },
{ text: 'Code Style', link: '/en/basics/code-style' }, { text: 'PostCSS', link: '/docs/setup/postcss' },
{ text: 'Naming', link: '/en/basics/naming' }, { text: 'Стили', link: '/docs/setup/styles' },
{ text: 'Documentation', link: '/en/basics/documentation' }, { text: 'SVG-спрайты', link: '/docs/setup/svg-sprites' },
{ text: 'Typing', link: '/en/basics/typing' }, { text: 'Шаблоны генерации', link: '/docs/setup/templates' },
{ text: 'VS Code', link: '/docs/setup/vscode' },
], ],
}, },
{ {
text: 'Applied Sections', text: 'Использование',
items: [ items: [
{ text: 'VS Code Setup', link: '/en/applied/vscode' }, { text: 'Структура проекта', link: '/docs/usage/project-structure' },
{ text: 'Project Structure', link: '/en/applied/project-structure' }, { text: 'Компоненты', link: '/docs/usage/components' },
{ text: 'Components', link: '/en/applied/components' }, { text: 'Страницы (App Router)', link: '/docs/usage/page-level' },
{ text: 'Page-level Components', link: '/en/applied/page-level' }, { text: 'Шаблоны и генерация кода', link: '/docs/usage/templates-generation' },
{ text: 'Templates & Code Generation', link: '/en/applied/templates-generation' }, { text: 'Стили', link: '/docs/usage/styles' },
{ text: 'Styles', link: '/en/applied/styles' }, // Неактивные разделы: страницы существуют, но пока пустые.
{ text: 'Images', link: '/en/applied/images-sprites' }, // Оставляем в sidebar без `link`, чтобы видеть план, но без перехода.
{ text: 'SVG Sprites', link: '/en/applied/svg-sprites' }, { text: 'Изображения · в разработке' },
{ text: 'Video', link: '/en/applied/video' }, { text: 'SVG-спрайты', link: '/docs/usage/svg-sprites' },
{ text: 'API', link: '/en/applied/api' }, { text: 'Видео · в разработке' },
{ text: 'Stores', link: '/en/applied/stores' }, { text: 'Stores · в разработке' },
{ text: 'Hooks', link: '/en/applied/hooks' }, { text: 'Хуки · в разработке' },
{ text: 'Fonts', link: '/en/applied/fonts' }, { text: 'Шрифты · в разработке' },
{ text: 'Localization', link: '/en/applied/localization' }, { text: 'Локализация · в разработке' },
],
},
{
text: 'Данные',
items: [
{ text: 'Введение', link: '/docs/usage/data/' },
{
text: 'REST',
collapsed: true,
items: [
{
text: 'Клиенты',
collapsed: true,
items: [
{ text: 'Автоматическая генерация', link: '/docs/usage/data/rest/clients/auto' },
{ text: 'Ручное создание', link: '/docs/usage/data/rest/clients/manual' },
],
},
{
text: 'Получение данных',
collapsed: true,
items: [
{ text: 'Серверные компоненты', link: '/docs/usage/data/rest/fetching/server' },
{ text: 'Клиентские компоненты', link: '/docs/usage/data/rest/fetching/client' },
],
},
],
},
{ text: 'Realtime', link: '/docs/usage/data/realtime' },
], ],
}, },
]; ];
@@ -115,8 +122,20 @@ export default defineConfig({
// (попадают в корень `dist/` как статика). Исключаем из сканирования // (попадают в корень `dist/` как статика). Исключаем из сканирования
// страниц, иначе VitePress рендерит их как HTML-страницы. // страниц, иначе VitePress рендерит их как HTML-страницы.
srcExclude: ['public/**'], srcExclude: ['public/**'],
lang: 'ru-RU',
title: 'NextJS Style Guide', title: 'NextJS Style Guide',
description: 'Правила и стандарты разработки на NextJS и TypeScript', description: 'Стандарты разработки на Next.js + TypeScript с архитектурой SLM',
// Чистые URL без `.html` — канон для индексации.
// Серверная поддержка реализована в Caddyfile (try_files + редирект).
cleanUrls: true,
// Дублируем указатель на llms.txt в <head> — для агентов,
// которые читают HTML, но не парсят полный DOM/href.
head: [
['link', { rel: 'alternate', type: 'text/plain', href: '/llms.txt', title: 'llms.txt' }],
['link', { rel: 'alternate', type: 'text/plain', href: '/llms-full.txt', title: 'llms-full.txt' }],
],
vite: { vite: {
plugins: [utf8TextPlugin], plugins: [utf8TextPlugin],
@@ -125,38 +144,18 @@ export default defineConfig({
}, },
}, },
locales: { themeConfig: {
root: { sidebar,
label: 'Languages', socialLinks: [
lang: 'en', { icon: 'github', link: 'https://gromlab.ru/docs/nextjs-style-guide' },
}, ],
ru: {
label: 'Русский',
lang: 'ru-RU',
link: '/ru/',
description: 'Стандарты разработки на Next.js + TypeScript с архитектурой SLM',
themeConfig: {
sidebar: ruSidebar,
},
// Расширенный блок описания для llms.txt — даёт LLM полный
// технический контекст: стек, методология, охват тем.
llmsBlockquote:
'Стандарты разработки frontend-приложений на Next.js (App Router) + TypeScript + React с архитектурой SLM (Scoped Layered Module Design — модульная архитектура со слоями ответственности, где каждый модуль содержит всё необходимое: компоненты, хуки, сторы, типы, стили).',
llmsContext:
'Стек: React, TypeScript, Next.js App Router, Mantine UI, SWR, Zustand, i18next, PostCSS Modules, Vitest, clsx.\n\nДокументация покрывает архитектуру SLM (слои, модули, сегменты, направление зависимостей, публичный API), правила оформления кода (именование, форматирование, импорты, типизация, JSDoc), реализацию компонентов и хуков, работу с App Router, кодогенерацию из шаблонов, стилизацию (Mobile First, токены), работу с API и сокетами, управление состоянием через Zustand, локализацию, ассеты (шрифты, изображения, SVG-спрайты) и настройку VS Code.',
},
en: {
label: 'English',
lang: 'en-US',
link: '/en/',
description: 'Next.js + TypeScript development standards with SLM architecture',
themeConfig: {
sidebar: enSidebar,
},
llmsBlockquote:
'Frontend development standards for Next.js (App Router) + TypeScript + React projects with SLM architecture (Scoped Layered Module Design — a modular architecture with responsibility layers, where each module contains everything it needs: components, hooks, stores, types, styles).',
llmsContext:
'Stack: React, TypeScript, Next.js App Router, Mantine UI, SWR, Zustand, i18next, PostCSS Modules, Vitest, clsx.\n\nThe documentation covers SLM architecture (layers, modules, segments, dependency direction, public API), code conventions (naming, formatting, imports, typing, JSDoc), component and hook implementation, App Router usage, code generation from templates, styling (Mobile First, design tokens), API and socket integration, state management via Zustand, localization, assets (fonts, images, SVG sprites), and VS Code setup.',
},
}, },
});
// Расширенный блок описания для llms.txt — даёт LLM полный
// технический контекст: стек, методология, охват тем.
// Используется в generate-llms.ts.
llmsBlockquote:
'Стандарты разработки frontend-приложений на Next.js (App Router) + TypeScript + React с архитектурой SLM (Scoped Layered Module Design — модульная архитектура со слоями ответственности, где каждый модуль содержит всё необходимое: компоненты, хуки, сторы, типы, стили).',
llmsContext:
'Стек: React, TypeScript, Next.js App Router, Mantine UI, SWR, Zustand, i18next, PostCSS Modules, Vitest, clsx.\n\nДокументация покрывает архитектуру SLM (слои, модули, сегменты, направление зависимостей, публичный API), правила оформления кода (именование, форматирование, импорты, типизация, JSDoc), реализацию компонентов и хуков, работу с App Router, кодогенерацию из шаблонов, стилизацию (Mobile First, токены), работу с API и сокетами, управление состоянием через Zustand, локализацию, ассеты (шрифты, изображения, SVG-спрайты) и настройку VS Code.',
} as any);

View File

@@ -15,3 +15,5 @@
max-width: 100%; max-width: 100%;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }

View File

@@ -7,9 +7,8 @@
Документационный сайт с правилами и стандартами фронтенд-разработки на Next.js + TypeScript. Документационный сайт с правилами и стандартами фронтенд-разработки на Next.js + TypeScript.
- Движок: VitePress - Движок: VitePress
- Языки: русский (основной), английский - Язык: русский
- Русская версия: `docs/ru/` - Контент: `docs/docs/`
- Английская версия: `docs/en/`
## Команды ## Команды
@@ -17,51 +16,58 @@
|---------|-----------| |---------|-----------|
| `npm run dev` | Локальный сервер разработки | | `npm run dev` | Локальный сервер разработки |
| `npm run build` | Сборка статического сайта | | `npm run build` | Сборка статического сайта |
| `npm run llms` | Генерация `generated/{lang}/llms.txt` (карта документации для LLM) и README | | `npm run llms` | Генерация `llms.txt` (карта документации для LLM) и README |
## Структура файлов ## Структура файлов
``` ```
docs/ docs/
├── ru/ # Русская версия (основная) ├── index.md # Лендинг (URL `/`)
│ ├── index.md # Главная страница └── docs/ # Контент документации (URL `/docs/...`)
├── basics/ # Базовые правила ├── index.md # Главная страница
│ │ ├── tech-stack.md ├── workflow.md
│ │ ├── architecture.md ├── workflow/ # Процессы разработки
│ │ ├── code-style.md ├── basics/ # Базовые правила
│ ├── naming.md │ ├── tech-stack.md
│ ├── documentation.md │ ├── architecture/
── typing.md ── code-style.md
── applied/ # Прикладные разделы ── naming.md
├── vscode.md ├── documentation.md
── project-structure.md ── typing.md
├── components.md ├── setup/ # Установка: разовая настройка проекта
├── page-level.md ├── aliases.md
├── templates-generation.md ├── biome.md
├── styles.md ├── postcss.md
├── images-sprites.md ├── svg-sprites.md
── svg-sprites.md ── vscode.md
│ ├── video.md └── usage/ # Использование: повседневная работа
├── api.md ├── project-structure.md
├── stores.md ├── components.md
├── hooks.md ├── page-level.md
├── fonts.md ├── templates-generation.md
── localization.md ── styles.md
├── en/ # Английская версия (зеркало ru/) ├── images-sprites.md
├── svg-sprites.md
├── video.md
├── data/
├── stores.md
├── hooks.md
├── fonts.md
└── localization.md
.vitepress/ .vitepress/
── config.ts # Конфигурация VitePress, сайдбары, локали ── config.ts # Конфигурация VitePress, сайдбар
generated/
├── ru/llms.txt # Карта документации для LLM (ru, llmstxt.org)
└── en/llms.txt # Карта документации для LLM (en, llmstxt.org)
generate-llms.ts # Скрипт генерации llms.txt и README generate-llms.ts # Скрипт генерации llms.txt и README
``` ```
Сгенерированные артефакты (`docs/public/`): `llms.txt`, `llms-full.txt`,
`nextjs-style-guide.zip`, `manifest.json`, копии `.md` в `docs/public/docs/`.
### Добавление нового раздела ### Добавление нового раздела
1. Создать `.md`-файл в нужной папке (`basics/` или `applied/`). 1. Создать `.md`-файл в нужной папке (`docs/docs/basics/`, `docs/docs/setup/` или `docs/docs/usage/`).
2. Добавить пункт в сайдбар — `.vitepress/config.ts` (оба языка, если есть перевод). 2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
Сайдбар — единственный источник порядка и группировки для `llms.txt`. Сайдбар — единственный источник порядка и группировки для `llms.txt`.
3. Запустить `npm run llms` для обновления `generated/{lang}/llms.txt`. 3. Запустить `npm run llms` для обновления `llms.txt` и README.
## Два типа документации ## Два типа документации
@@ -92,7 +98,8 @@ generate-llms.ts # Скрипт генерации llms.txt и R
```markdown ```markdown
# {Название} # {Название}
Краткое описание: о чём раздел и какие аспекты работы с областью он охватывает. {Одно-два предложения, по которым читатель за секунду решает, нужен ли ему раздел.
Правила оформления — секция «Заголовок и описание» ниже.}
## Что нужно знать ## Что нужно знать
@@ -177,14 +184,127 @@ title: Название раздела
Значение `title` совпадает с текстом `h1`-заголовка в файле. Значение `title` совпадает с текстом `h1`-заголовка в файле.
### Заголовки ### Заголовок и описание
- Один `h1` на файл — совпадает с `title` из frontmatter. Каждая страница начинается с `h1`-заголовка и абзаца-описания сразу под ним.
- Сразу после `h1` — вводный абзац (одно-два предложения). Эта пара — **навигационный маркер**: попадает в сайдбар, `llms.txt`,
`README.md` архива и должна за секунду давать читателю или LLM понять,
**когда сюда нужно идти**.
#### Структура заголовков
- Один `h1` на файл, совпадает с `title` во frontmatter.
- Сразу после `h1` — описание (одно-два предложения, см. ниже).
- Основные секции — `h2`. - Основные секции — `h2`.
- Подсекции внутри `h2``h3`. - Подсекции внутри `h2``h3`.
- `h4` не используется. - `h4` не используется.
#### Имя `h1` (заголовок страницы)
- Называет предметную область, а не реализацию.
- Без имён пакетов, опций, конфигов и путей.
- Самодостаточен — читается без контекста сайдбара.
- Исключение: имя инструмента допустимо, если оно — единственное
устойчивое имя самой области (`PostCSS`, `Biome`, `VS Code`).
**Хорошо:** «Алиасы импортов», «Структура проекта», «SVG-спрайты».
**Плохо:** «Установка и настройка» (что устанавливаем?),
«Использование» (что используем?), «Введение» (во что?).
#### Описание
Описание — короткий ответ на вопрос «у меня задача X, мне сюда?».
Не аннотация. Не оглавление.
**Запреты:**
- Не упоминать конкретные пакеты, библиотеки, инструменты
(`@gromlab/svg-sprites`, `Mantine`, `Zustand`).
- Не упоминать конкретные файлы и пути
(`postcss.config.mjs`, `.templates/`, `biome.json`).
- Не упоминать конкретные опции, ключи API, имена функций
(`baseUrl`, `cl()`, `useStore`).
- Не начинать с «Раздел описывает», «Этот раздел»,
«В этом разделе», «Здесь рассмотрено».
- Не использовать дежурные префиксы как шаблон
(«Правила работы с...», «Правила написания...») — само то,
что раздел про правила, и так понятно из секции и заголовка.
- Не пересказывать содержимое перечислением подсекций
(«организация, реализация, делегирование, метаданные»).
- Не аргументировать пользу
(«обеспечит единообразие», «упростит поддержку»).
**Требования:**
- 1 предложение, обычно 512 слов.
- Звучит как ответ человека другу, а не как техспек.
- Описание читается **самостоятельно**, без контекста сайдбара.
- Если страница вложена в семантическую группу
(например, `Данные → REST → Клиенты → ...`) и её заголовок
без этой группы теряет смысл — описание явно содержит имя
родительской области, чтобы читалось без сайдбара.
**Подходящие формы:**
- «Как X.»
- «Что такое X.»
- «Из чего состоит X.»
- «Установка X.»
- «Какие X есть и как ими пользоваться.»
Перечисление аспектов через двоеточие — только если без него читатель
не сможет различить раздел от соседнего.
**Тест навигации.** Читатель видит описание — за секунду должен понять
«мне сюда» или «нет, не сюда». Если приходится перечитывать —
описание слишком длинное.
**Тест на изменение.** Если в разделе сменится пакет, переименуется
файл или добавится правило — придётся ли править описание?
Если да — оно слишком конкретное.
**Хорошо:**
```markdown
Какие алиасы импортов есть в проекте и как ими пользоваться.
```
```markdown
Установка и настройка линтера-форматтера в новом проекте.
```
```markdown
Из чего состоит проект и где что лежит.
```
```markdown
Получение REST-данных в серверных компонентах.
```
**Плохо:**
```markdown
Раздел описывает, какие алиасы используются в проекте: их полный список,
где они объявлены и как ими пользоваться между модулями и внутри модуля.
```
_Начинается с «Раздел описывает», пересказывает содержимое._
```markdown
Установка и настройка CSS-процессора PostCSS в проекте: набор плагинов,
конфиг `postcss.config.mjs`. Выполняется один раз при заведении проекта.
```
_Упомянут конкретный файл, перечисление аспектов превратилось в оглавление._
```markdown
Правила работы с React-компонентами: файловая структура,
типизация пропсов, документирование, реализация.
```
ежурный префикс «Правила работы с...» плюс оглавление подсекций._
### Примеры кода ### Примеры кода
- Блоки кода с указанием языка: ` ```tsx `, ` ```css `, ` ```bash `, ` ```text `. - Блоки кода с указанием языка: ` ```tsx `, ` ```css `, ` ```bash `, ` ```text `.

View File

@@ -1,10 +1,44 @@
:8080 { :8080 {
root * /srv root * /srv
# Кириллица в .txt и .md ломается без явного charset # Устаревшие пути llms.txt в подпапках → корень.
@text path *.txt *.md # Без этого опечатка `/docs/llms.txt` уходит в SPA-фолбэк и
header @text Content-Type "text/plain; charset=utf-8" # отдаёт HTML под видом text/plain — агент верит, что получил llms.txt.
redir /docs/llms.txt /llms.txt 301
redir /docs/llms-full.txt /llms-full.txt 301
# Чистые URL: запросы вида `/docs/foo.html` редиректим на `/docs/foo`.
# Канон сайта — без `.html` (cleanUrls в VitePress).
# Не трогаем index.html в корне — он не имеет смысловой пары без расширения.
@legacyHtml {
path_regexp legacyHtml ^(/.+)\.html$
not path /index.html
}
redir @legacyHtml {re.legacyHtml.1} 301
# Подсказка агентам, где лежит карта документации (RFC 8288).
# Позволяет найти llms.txt без парсинга DOM — по HTTP-заголовку.
header Link "</llms.txt>; rel=\"llms\""
# Кириллица в .txt/.md ломается без явного charset.
# Применяем заголовок только к РЕАЛЬНО существующим файлам,
# иначе SPA-фолбэк (HTML) уезжает с Content-Type: text/plain.
@existingText {
path *.txt *.md
file
}
header @existingText Content-Type "text/plain; charset=utf-8"
# Несуществующие .txt/.md → 404, не HTML-фолбэк.
# Это критично для llms.txt: агент должен получить честный 404,
# а не валидный «как бы текст» с лендингом внутри.
@missingText {
path *.txt *.md
not file
}
respond @missingText 404
file_server file_server
try_files {path} /index.html # cleanUrls: пробуем точное совпадение → +.html → каталог → SPA-фолбэк.
try_files {path} {path}.html {path}/ /index.html
} }

View File

@@ -1,6 +1,6 @@
FROM node:24-alpine AS build FROM node:24-alpine AS build
WORKDIR /app WORKDIR /app
# zip нужен для упаковки nextjs-style-guide-{lang}.zip # zip нужен для упаковки nextjs-style-guide.zip
RUN apk add --no-cache zip RUN apk add --no-cache zip
COPY package*.json ./ COPY package*.json ./
RUN npm ci RUN npm ci

110
README.md
View File

@@ -1,58 +1,80 @@
# NextJS Style Guide # NextJS Style Guide
Rules and standards for NextJS and TypeScript development: architecture, typing, styles, components, API, and infrastructure. Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.
## Documentation Structure Сайт: https://nextjs-style-guide.gromlab.ru
### Processes ## Использование
**What to do** in a specific situation — step-by-step instructions. **Для AI-агентов:**
| Section | Answers the question | - [llms.txt](https://nextjs-style-guide.gromlab.ru/llms.txt) — Карта разделов, оглавление со ссылками на разделы.
|---------|---------------------| - [llms-full.txt](https://nextjs-style-guide.gromlab.ru/llms-full.txt) — Вся документация одним файлом.
| Getting Started | What tools to install before starting development? |
| Creating an App | How to create a new project, where to get a template? |
| Creating Pages | How to add a page: routing and screen? |
| Creating Components | How to generate components using templates? |
| Styling | What to use: Mantine, tokens, or PostCSS? |
| Data Fetching | How to fetch data: SWR, codegen, sockets? |
| State Management | When and how to create a store (Zustand)? |
| Localization | How to add translations and work with i18next? |
### Basic Rules **Для проекта:**
**What the code should look like** — standards not tied to a specific technology. - [nextjs-style-guide.zip](https://nextjs-style-guide.gromlab.ru/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
| Section | Answers the question | ## Структура документации
|---------|---------------------|
| Tech Stack | What stack do we use? |
| Architecture | How are FSD layers, dependencies, and public API structured? |
| Code Style | How to format code: indentation, quotes, imports, early return? |
| Naming | How to name files, variables, components, hooks? |
| Documentation | How to write JSDoc: what to document and what not? |
| Typing | How to type: type vs interface, any/unknown? |
### Applied Sections ### Workflow
**How a specific area works** — rules, structure, and code examples for specific technologies and tools. [Workflow](docs/docs/workflow.md) — пошаговая карта типовых задач: с чего начать, как добавить страницу/компонент, как подключить данные, стили, локализацию.
| Section | Answers the question | ### Базовые правила
|---------|---------------------|
| Project Structure | How are folders and files organized by FSD? |
| Components | How is a component structured: files, props, clsx? |
| Page-level Components | How to define layout, page, loading, error, not-found? |
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
| Images | _(not filled)_ |
| SVG Sprites | _(not filled)_ |
| Video | _(not filled)_ |
| API | _(not filled)_ |
| Stores | _(not filled)_ |
| Hooks | _(not filled)_ |
| Fonts | _(not filled)_ |
| Localization | _(not filled)_ |
## For Assistants **Каким должен быть код** — стандарты, не привязанные к конкретной технологии.
Documentation map with links to all sections ([llmstxt.org](https://llmstxt.org) format): | Раздел | Отвечает на вопрос |
https://gromlab.ru/docs/nextjs-style-guide/raw/branch/main/generated/en/llms.txt |--------|-------------------|
| Технологии и библиотеки | Какой стек используем? |
| Именование | Как называть файлы, переменные, компоненты, хуки? |
| Архитектура: Обзор | Что такое SLM и зачем она нужна? |
| Архитектура: Слои | Какие слои есть и как между ними устроены зависимости? |
| Архитектура: Модули | Что такое модуль и как он устроен? |
| Архитектура: Сегменты | Какие сегменты есть внутри модуля? |
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
| Документирование | Как писать JSDoc: что документировать, а что нет? |
| Типизация | Как типизировать: type vs interface, any/unknown? |
### Установка и настройка
**Как поднять и сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Создание проекта: Из шаблона | Как начать проект из готового шаблона? |
| Создание проекта: Вручную | Как поднять проект с нуля без шаблона? |
| Next.js | Как настроить Next.js под проект? |
| Алиасы | Как настроить путевые алиасы импортов? |
| Biome | Как настроить Biome (линтер и форматер)? |
| Стили | Как подключить и настроить стили проекта? |
| PostCSS | Какие плагины PostCSS нужны и как их настроить? |
| SVG-спрайты | Как настроить генерацию SVG-спрайтов? |
| Шаблоны генерации | Как подключить шаблоны для кодогенерации? |
| VS Code | Как настроить редактор для проекта? |
### Использование
**Как это устроено и как этим пользоваться** — структура, примеры и правила для конкретных областей.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Структура проекта | Как организованы папки и файлы по SLM? |
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
| Страницы (App Router) | Как описывать layout, page, loading, error, not-found? |
| Данные: Введение | Как устроена работа с данными в проекте? |
| Данные: REST: Клиенты: Автоматическая генерация | Как сгенерировать REST-клиент автоматически из OpenAPI? |
| Данные: REST: Клиенты: Ручное создание | Как написать REST-клиент вручную? |
| Данные: REST: Получение данных: Серверные компоненты | Как получать данные в серверных компонентах? |
| Данные: REST: Получение данных: Клиентские компоненты | Как получать данные в клиентских компонентах (SWR)? |
| Данные: Realtime | Как работать с realtime-каналами и сокетами? |
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
| Изображения | _(не заполнен)_ |
| SVG-спрайты | Как использовать SVG-спрайты в коде? |
| Видео | _(не заполнен)_ |
| Stores | _(не заполнен)_ |
| Хуки | _(не заполнен)_ |
| Шрифты | _(не заполнен)_ |
| Локализация | _(не заполнен)_ |

View File

@@ -1,61 +0,0 @@
# NextJS Style Guide
Правила и стандарты разработки на NextJS и TypeScript: архитектура, типизация, стили, компоненты, API и инфраструктурные разделы.
## Для ассистентов
Карта документации со ссылками на все разделы (формат [llmstxt.org](https://llmstxt.org)):
https://gromlab.ru/docs/nextjs-style-guide/raw/branch/main/generated/ru/llms.txt
## Структура документации
### Workflow
**Что делать и в каком порядке** — пошаговые инструкции.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Начало работы | Что нужно знать перед началом разработки? |
| Создание проекта | Как начать новый проект? |
| Генерация кода | Какие модули должны генерироваться из шаблонов? |
| Добавление страницы | Как добавить новую страницу в проект? |
| Добавление UI-модуля | Как создать компонент, бизнес-модуль, виджет или layout? |
| Стилизация | Как стилизовать компоненты в проекте? |
| Получение данных | Как получать данные с сервера? |
| Управление состоянием | Как работать с состоянием? |
| Локализация | Как добавлять переводы и подключать локализацию? |
### Базовые правила
**Каким должен быть код** — стандарты, не привязанные к конкретной технологии.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Технологии и библиотеки | Какой стек используем? |
| Архитектура | Как устроены слои SLM, зависимости, публичный API? |
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
| Именование | Как называть файлы, переменные, компоненты, хуки? |
| Документирование | Как писать JSDoc: что документировать, а что нет? |
| Типизация | Как типизировать: type vs interface, any/unknown? |
### Прикладные разделы
**Как это настроить и использовать** — конфигурация, структура и примеры кода для конкретных областей.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Настройка VS Code | Как настроить редактор для проекта? |
| Структура проекта | Как организованы папки и файлы по SLM? |
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
| Изображения | _(не заполнен)_ |
| SVG-спрайты | _(не заполнен)_ |
| Видео | _(не заполнен)_ |
| API | _(не заполнен)_ |
| Stores | _(не заполнен)_ |
| Хуки | _(не заполнен)_ |
| Шрифты | _(не заполнен)_ |
| Локализация | _(не заполнен)_ |

174
docs-overview.md Normal file
View File

@@ -0,0 +1,174 @@
# Обзор страниц документации
Список всех `.md`-страниц в `docs/docs/` в порядке сайдбара (`.vitepress/config.ts`).
Поля: путь к файлу, заголовок (`h1`), описание (абзац под заголовком).
## Главная
### docs/docs/index.md
**Заголовок:** NextJS Style Guide
**Описание:** Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
## Подсказки
### docs/docs/workflow.md
**Заголовок:** Подсказки
**Описание:** Короткие ответы на типовые вопросы и решения для спорных ситуаций.
## Базовые правила
### docs/docs/basics/tech-stack.md
**Заголовок:** Технологии и библиотеки
**Описание:** Какие библиотеки и инструменты используются в проекте.
### docs/docs/basics/naming.md
**Заголовок:** Именование
**Описание:** Как называть переменные, файлы и прочие сущности в коде.
### docs/docs/basics/architecture/index.md
**Заголовок:** SLM Design
**Описание:** Архитектурный подход проекта: что такое SLM и как он устроен.
### docs/docs/basics/architecture/reference/layers.md
**Заголовок:** Слои
**Описание:** Из каких слоёв состоит архитектура и как они связаны.
### docs/docs/basics/architecture/reference/modules.md
**Заголовок:** Модули
**Описание:** Что такое модуль в архитектуре и как он устроен.
### docs/docs/basics/architecture/reference/segments.md
**Заголовок:** Сегменты
**Описание:** Что такое сегмент модуля и какие они бывают.
### docs/docs/basics/code-style.md
**Заголовок:** Стиль кода
**Описание:** Как оформляется код в проекте.
### docs/docs/basics/documentation.md
**Заголовок:** Документирование
**Описание:** Что и как документировать в коде.
### docs/docs/basics/typing.md
**Заголовок:** Типизация
**Описание:** Как типизируется код в проекте.
## Создание проекта
### docs/docs/creating-project/from-template.md
**Заголовок:** Создание проекта из шаблона
**Описание:** Создание нового проекта на основе готового шаблона.
### docs/docs/creating-project/manual.md
**Заголовок:** Создание проекта вручную
**Описание:** Поэтапное создание нового проекта без использования шаблона.
### docs/docs/creating-project/nextjs.md
**Заголовок:** Чистая установка Next.js
**Описание:** Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку.
## Настройка
### docs/docs/setup/aliases.md
**Заголовок:** Алиасы импортов
**Описание:** Какие алиасы импортов есть в проекте и как ими пользоваться.
### docs/docs/setup/biome.md
**Заголовок:** Biome
**Описание:** Установка и настройка линтера-форматтера в новом проекте.
### docs/docs/setup/postcss.md
**Заголовок:** PostCSS
**Описание:** Установка и настройка CSS-процессора в новом проекте.
### docs/docs/setup/styles.md
**Заголовок:** Стили
**Описание:** Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили.
### docs/docs/setup/svg-sprites.md
**Заголовок:** SVG-спрайты
**Описание:** Подключение SVG-спрайтов в новом проекте.
### docs/docs/setup/templates.md
**Заголовок:** Шаблоны генерации
**Описание:** Подключение шаблонов кодогенерации в новом проекте.
### docs/docs/setup/vscode.md
**Заголовок:** VS Code
**Описание:** Единые настройки редактора и расширений для команды.
## Использование
### docs/docs/usage/project-structure.md
**Заголовок:** Структура проекта
**Описание:** Из чего состоит проект и где что лежит.
### docs/docs/usage/components.md
**Заголовок:** Компоненты
**Описание:** Как устроен и пишется React-компонент в проекте.
### docs/docs/usage/page-level.md
**Заголовок:** Файлы роутинга
**Описание:** Что должно лежать в файлах роутинга, а что — в экранах.
### docs/docs/usage/templates-generation.md
**Заголовок:** Шаблоны и генерация кода
**Описание:** Как устроены шаблоны кодогенерации и как ими пользоваться.
### docs/docs/usage/styles.md
**Заголовок:** Стили
**Описание:** Как пишутся стили в проекте.
### docs/docs/usage/images-sprites.md
**Заголовок:** —
**Описание:** _(файл пустой)_
### docs/docs/usage/svg-sprites.md
**Заголовок:** SVG-спрайты
**Описание:** Как добавлять и использовать SVG-иконки в коде.
### docs/docs/usage/video.md
**Заголовок:** —
**Описание:** _(файл пустой)_
### docs/docs/usage/stores.md
**Заголовок:** —
**Описание:** _(файл пустой)_
### docs/docs/usage/hooks.md
**Заголовок:** —
**Описание:** _(файл пустой)_
### docs/docs/usage/fonts.md
**Заголовок:** —
**Описание:** _(файл пустой)_
### docs/docs/usage/localization.md
**Заголовок:** —
**Описание:** _(файл пустой)_
## Данные
### docs/docs/usage/data/index.md
**Заголовок:** Источники данных
**Описание:** Какие источники данных используются в проекте и как с ними работать.
### docs/docs/usage/data/rest/clients/auto.md
**Заголовок:** Автоматическая генерация
**Описание:** Генерация REST-клиента из OpenAPI-спецификации.
### docs/docs/usage/data/rest/clients/manual.md
**Заголовок:** Ручное создание
**Описание:** Создание REST-клиента вручную, когда нет OpenAPI-спецификации.
### docs/docs/usage/data/rest/fetching/server.md
**Заголовок:** Серверные компоненты
**Описание:** Получение REST-данных в серверных компонентах.
### docs/docs/usage/data/rest/fetching/client.md
**Заголовок:** Клиентские компоненты
**Описание:** Получение REST-данных в клиентских компонентах.
### docs/docs/usage/data/realtime.md
**Заголовок:** Realtime
**Описание:** Работа с push-данными от сервера: подписки и события.

View File

@@ -1,10 +1,11 @@
--- ---
title: Архитектура title: SLM Design
description: "Раздел описывает архитектуру проекта: из каких слоёв состоит приложение, как организован код внутри слоёв и какие правила управляют зависимостями." description: "Архитектурный подход проекта: что такое SLM и как он устроен."
--- ---
# SLM Design # SLM Design
Scoped Layered Module Design — модульная архитектура фронтенд-приложений. Код организован по слоям ответственности, а модуль содержит всё, что ему нужно: компоненты, хуки, сторы, типы, стили.
Архитектурный подход проекта: что такое SLM и как он устроен.
## Преимущества ## Преимущества

View File

@@ -1,10 +1,11 @@
--- ---
title: Слои title: Слои
description: Из каких слоёв состоит архитектура и как они связаны.
--- ---
# Слои # Слои
Раздел описывает слои SLM: что такое слой, какие бывают, как между ними направлены зависимости и какие правила действуют на каждом. Из каких слоёв состоит архитектура и как они связаны.
## Определение ## Определение

View File

@@ -1,10 +1,11 @@
--- ---
title: Модули title: Модули
description: Что такое модуль в архитектуре и как он устроен.
--- ---
# Модули # Модули
Раздел описывает модули SLM: что такое модуль, из чего он состоит и как взаимодействует с остальным кодом. Что такое модуль в архитектуре и как он устроен.
## Определение ## Определение
@@ -52,7 +53,7 @@ auth/
└── index.ts # публичный API └── index.ts # публичный API
``` ```
Подробное описание каждого сегмента — в разделе [Сегменты](/ru/basics/architecture/reference/segments). Подробное описание каждого сегмента — в разделе [Сегменты](/docs/basics/architecture/reference/segments).
## Публичный API ## Публичный API

View File

@@ -1,10 +1,11 @@
--- ---
title: Сегменты title: Сегменты
description: Что такое сегмент модуля и какие они бывают.
--- ---
# Сегменты # Сегменты
Раздел описывает сегменты SLM: что такое сегмент, какие бывают и что в каждом из них лежит. Что такое сегмент модуля и какие они бывают.
## Определение ## Определение

View File

@@ -1,10 +1,11 @@
--- ---
title: Стиль кода title: Стиль кода
description: Как оформляется код в проекте.
--- ---
# Стиль кода # Стиль кода
Раздел описывает единые правила оформления кода: отступы, переносы, кавычки, порядок импортов и базовую читаемость. Как оформляется код в проекте.
## Отступы ## Отступы

View File

@@ -1,11 +1,11 @@
--- ---
title: Документирование title: Документирование
description: Что и как документировать в коде.
--- ---
# Документирование # Документирование
Этот раздел описывает правила документирования кода: когда и как писать Что и как документировать в коде.
комментарии к компонентам, функциям, типам и интерфейсам.
## Общие правила ## Общие правила

View File

@@ -1,10 +1,11 @@
--- ---
title: Именование title: Именование
description: Как называть переменные, файлы и прочие сущности в коде.
--- ---
# Именование # Именование
Этот раздел описывает соглашения об именовании в проекте. Единые правила делают код предсказуемым и упрощают навигацию по проекту. Как называть переменные, файлы и прочие сущности в коде.
## Базовые правила ## Базовые правила

View File

@@ -1,10 +1,11 @@
--- ---
title: Технологии и библиотеки title: Технологии и библиотеки
description: Какие библиотеки и инструменты используются в проекте.
--- ---
# Технологии и библиотеки # Технологии и библиотеки
Этот раздел описывает базовый стек технологий и библиотек, принятый в проекте. Какие библиотеки и инструменты используются в проекте.
## Что используем ## Что используем
@@ -13,7 +14,7 @@ title: Технологии и библиотеки
- `Next.js` — для продуктовых сайтов. - `Next.js` — для продуктовых сайтов.
### Архитектура ### Архитектура
- `SLM Design` — собственная модульная архитектура проекта. Подробнее в разделе [Архитектура](/ru/basics/architecture/). - `SLM Design` — собственная модульная архитектура проекта. Подробнее в разделе [Архитектура](/docs/basics/architecture/).
### UI компоненты ### UI компоненты
- `Mantine UI` — базовые UI-компоненты. - `Mantine UI` — базовые UI-компоненты.

View File

@@ -1,10 +1,11 @@
--- ---
title: Типизация title: Типизация
description: Как типизируется код в проекте.
--- ---
# Типизация # Типизация
Этот раздел описывает правила типизации: как типизировать компоненты, функции и работу с `any`/`unknown`. Как типизируется код в проекте.
## Общие правила ## Общие правила

View File

@@ -0,0 +1,51 @@
---
title: Создание проекта из шаблона
description: Создание нового проекта на основе готового шаблона.
keywords: [создать проект из шаблона, шаблон, template, tiged, degit, клонировать шаблон, эталонный шаблон, быстрый старт, scaffold, новый проект]
---
# Создание проекта из шаблона
Создание нового проекта на основе готового шаблона.
## Что внутри
Шаблон — готовый скелет проекта с применёнными правилами стайлгайда:
- **Стек:** Next.js (App Router), TypeScript, React.
- **Архитектура:** структура папок по SLM, алиасы импортов.
- **Качество кода:** Biome (линтер и форматер), настройки VS Code.
- **Стили:** PostCSS Modules с плагинами, токены, медиа-брейкпоинты.
- **Ассеты:** генерация SVG-спрайтов.
- **Кодогенерация:** шаблоны для страниц, компонентов, хуков, сторов.
в
## Установка
1. Склонировать шаблон в родительском каталоге будущего проекта:
```bash
npx tiged git@gromlab.ru:templates/nextjs.git my-app
```
`tiged` копирует снимок репозитория без истории git. Имя каталога (`my-app`) заменяется на нужное.
2. Установить зависимости:
```bash
cd my-app
npm install
```
3. Проверить сборку:
```bash
npm run build
```
Сборка должна завершиться без ошибок.
## Правила
- **Шаблон — источник истины.** Не добавлять, не удалять и не переименовывать файлы шаблона «для приведения к канону»: шаблон уже канонический. Любое несоответствие — баг шаблона, а не проекта.
- **Менеджер пакетов — npm.** Отклонение (pnpm, yarn, bun) — только по явному решению с пониманием, что стайлгайд этого не предусматривает.
- **Не инициализировать git заново** автоматически. `tiged` намеренно не создаёт `.git/` — решение о репозитории принимает разработчик.

View File

@@ -0,0 +1,90 @@
---
title: Создание проекта вручную
description: Поэтапное создание нового проекта без использования шаблона.
keywords: [создать проект, новый проект, с нуля, init, initialize, scaffold, create-next-app, начать проект, поднять проект, эталонный проект, ручная установка]
---
# Создание проекта вручную
Поэтапное создание нового проекта без использования шаблона.
## Состав эталонного проекта
| Компонент | Роль | Раздел |
|-----------|------|--------|
| Next.js | Фреймворк (роутинг, сборка, SSR) | [Next.js](/docs/creating-project/nextjs) |
| Алиасы | Импорты по слоям SLM | [Алиасы](/docs/setup/aliases) |
| Biome | Линтер и форматтер (замена ESLint + Prettier) | [Biome](/docs/setup/biome) |
| Стили | Глобальные токены и breakpoints | [Стили](/docs/setup/styles) |
| PostCSS | CSS-процессор для custom-media и вложенности | [PostCSS](/docs/setup/postcss) |
| SVG-спрайты | Иконки через `<SvgSprite/>`, управление цветом | [SVG-спрайты](/docs/setup/svg-sprites) |
| VS Code | Настройки редактора и расширения | [VS Code](/docs/setup/vscode) |
| Шаблоны генерации | `.templates/` для `@gromlab/create` | [Шаблоны генерации](/docs/setup/templates) |
Убрать компонент из состава — значит согласованно отказаться от части стайлгайда. Частичные проекты возможны (только Next.js, Next.js + стили и т.п.), но не являются эталоном.
## Канон раскладки
В `src/` допустимы только слои SLM: `app/`, `layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`, `shared/`. Любая другая папка в `src/` — нарушение канона ([Структура проекта](/docs/usage/project-structure), [Архитектура](/docs/basics/architecture/)).
В частности: `src/app/` содержит только файлы роутинга Next.js и инициализации, без каталогов `styles/`, `assets/`, `components/`.
## Порядок установки
Подсистемы ставятся в фиксированном порядке — он отражает зависимости между шагами.
### 1. Next.js
Скелет фреймворка — обязательный первый шаг, остальное опирается на него.
См. [Next.js](/docs/creating-project/nextjs). После выполнения проверки этого раздела `npm run build` должен проходить.
### 2. Алиасы
Заменить дефолтный `"@/*"` в `tsconfig.json` на канонический список из восьми слой-префиксов.
См. [Алиасы](/docs/setup/aliases).
### 3. Biome
Линтер и форматтер. Подключается **до** написания кода, иначе в проекте копятся несогласованные правки.
См. [Biome](/docs/setup/biome).
### 4. Стили (базовая инфраструктура)
Файлы `variables.css`, `media.css`, `global.css` в `src/shared/styles/` и подключение `global.css` в `src/app/layout.tsx`. CSS-процессор на этом шаге не ставится.
См. [Стили](/docs/setup/styles).
### 5. PostCSS
CSS-процессор поверх базовых стилей: `@custom-media`, вложенность, autoprefixer. Ставится **только после шага 4** — опирается на `src/shared/styles/media.css`.
См. [PostCSS](/docs/setup/postcss).
### 6. SVG-спрайты
Пакет `@gromlab/svg-sprites`, генерация спрайт-файла и React-компонента `<SvgSprite/>`.
См. [SVG-спрайты](/docs/setup/svg-sprites).
### 7. VS Code
Расширения и настройки редактора. Опирается на установленный Biome (форматирование при сохранении) и PostCSS (ассоциация `*.css`).
См. [VS Code](/docs/setup/vscode).
### 8. Шаблоны генерации
Папка `.templates/` для генератора модулей `@gromlab/create`.
См. [Шаблоны генерации](/docs/setup/templates).
## Правила
- **Порядок шагов фиксирован.** Перестановка ломает зависимости (PostCSS требует базовых стилей, VS Code — установленного Biome).
- **Между шагами обязательна проверка** из соответствующего раздела. Не переходить дальше, пока чеклист текущего шага не пройден.
- **Слои `src/`** (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) не создавать авансом. Появляются по мере первого модуля. Исключения — `src/app/` (создаётся `create-next-app`), `src/shared/styles/` (шаг 1) и `src/shared/sprites/icons/` (шаг 6).
- **Посторонние каталоги в `src/`** (`assets/`, `utils/`, `lib/`, `components/` и т.п.) — запрещены.
- **Подмножество шагов допустимо.** Можно ставить только Next.js и часть инструментов; полный набор — это эталон, а не обязательство.

View File

@@ -0,0 +1,112 @@
---
title: Чистая установка Next.js
description: "Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку."
keywords: [next.js, create-next-app, npx, установка, инициализация, фреймворк, скаффолдинг, app router, typescript]
---
# Чистая установка Next.js
Установка Next.js без лишнего шаблона — голый каркас под дальнейшую сборку.
## Требования
- Node.js 18.18+ (рекомендуется LTS 20+).
- npm 10+.
- Рабочая папка пуста, либо для установки выбрана подпапка (`create-next-app@16+` отказывается ставиться в непустую директорию).
## Установка
### 1. Инициализация через `create-next-app`
Флаги зафиксированы и не согласовываются — это канон стайлгайда:
```bash
npx create-next-app@latest my-app \
--typescript \
--app \
--src-dir \
--import-alias "@/*" \
--no-eslint \
--no-tailwind \
--use-npm
```
| Флаг | Значение | Почему так |
|------|----------|------------|
| `--typescript` | TS включён | Стайлгайд требует TypeScript ([Типизация](/docs/basics/typing)) |
| `--app` | App Router | Pages Router не используется |
| `--src-dir` | Код в `src/` | Архитектура SLM требует `src/` ([Структура проекта](/docs/usage/project-structure)) |
| `--import-alias "@/*"` | Placeholder | Требуется флагом; после установки `paths` полностью переписывается на слой-префиксы (см. [Алиасы](/docs/setup/aliases)) |
| `--no-eslint` | ESLint не ставится | Линтер и форматтер — Biome ([Biome](/docs/setup/biome)) |
| `--no-tailwind` | Tailwind не ставится | Стилизация — PostCSS Modules ([Стили](/docs/usage/styles)) |
| `--use-npm` | Пакетный менеджер — npm | Единый инструмент в проектах |
### 2. Очистить дефолтный шаблон
`create-next-app` генерирует демо-страницу со стилями и ассетами, а Next.js 16+ дополнительно кладёт в корень собственные `AGENTS.md` и `CLAUDE.md` — всё это удаляется.
```bash
rm src/app/page.module.css
rm src/app/globals.css
rm public/next.svg public/vercel.svg public/file.svg public/globe.svg public/window.svg
rm -f AGENTS.md CLAUDE.md
```
Заменить `src/app/page.tsx` на минимальный:
```tsx
// src/app/page.tsx
export default function HomePage() {
return <h1>Home</h1>
}
```
Очистить `src/app/layout.tsx` от импорта шрифтов и `globals.css`:
```tsx
// src/app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'App',
description: '',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ru">
<body>{children}</body>
</html>
)
}
```
### 3. Создать папку `src/shared/styles/`
Глобальные стили в SLM-архитектуре живут в слое `shared`, а не в `src/app/` ([Структура проекта](/docs/usage/project-structure)).
```bash
mkdir -p src/shared/styles
```
Остальные слои (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`) заводятся при появлении первого модуля в них. `src/shared/styles/` — единственный подкаталог `shared/`, который заводится сразу: без него не настроить стили на следующих шагах.
## Правила
- **Конфликт с непустой директорией** — не удалять файлы пользователя автоматически. Ставить в подпапку или временно перенести посторонние файлы.
- **Отклонение от канонических флагов** (pnpm, Tailwind, ESLint и т.п.) — только осознанное, с пониманием, что стайлгайд этого не предусматривает.
- **Слои `src/`** не создавать авансом — появляются при первом модуле. Алиасы прописываются сразу на все восемь слоёв (см. [Алиасы](/docs/setup/aliases)).
- **`AGENTS.md` от Next.js** удаляется в шаге 2. Повторно не создаётся.
- **Biome, стили, PostCSS, SVG-спрайты, VS Code** — отдельные шаги установки, не в этом разделе.
## Проверка установки
- В корне проекта: `next.config.ts`, `tsconfig.json`, `package.json`.
- В `package.json`: Next.js установлен, нет `eslint`, `tailwindcss`.
- В `src/app/` присутствуют минимальные `page.tsx` и `layout.tsx`. `globals.css`, `page.module.css` отсутствуют. Каталогов `styles/`, `assets/`, `providers/`, `components/` в `src/app/` нет.
- Папка `src/shared/styles/` создана (пустая).
- В `src/` из слоёв SLM присутствуют только `app/` и `shared/` (с `styles/`). Посторонних каталогов нет.
- В `public/` удалены `next.svg`, `vercel.svg`, `file.svg`, `globe.svg`, `window.svg`.
- В корне проекта нет `AGENTS.md` и `CLAUDE.md` от Next.js.
- `npm run build` завершается успешно.
- Пакетный менеджер — npm (нет `pnpm-lock.yaml`, `yarn.lock`, `bun.lockb`).

83
docs/docs/index.md Normal file
View File

@@ -0,0 +1,83 @@
---
title: NextJS Style Guide
description: Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
---
# NextJS Style Guide
Стандарты разработки фронтенд-приложений на Next.js и TypeScript.
## Использование
**Для AI-агентов:**
- [llms.txt](/llms.txt) — Карта разделов, оглавление со ссылками на разделы.
- [llms-full.txt](/llms-full.txt) — Вся документация одним файлом.
**Для проекта:**
- [nextjs-style-guide.zip](/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
## Структура документации
### Workflow
[Workflow](/docs/workflow) — пошаговая карта типовых задач: с чего начать, как добавить страницу/компонент, как подключить данные, стили, локализацию.
### Базовые правила
**Каким должен быть код** — стандарты, не привязанные к конкретной технологии.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Технологии и библиотеки | Какой стек используем? |
| Именование | Как называть файлы, переменные, компоненты, хуки? |
| Архитектура: Обзор | Что такое SLM и зачем она нужна? |
| Архитектура: Слои | Какие слои есть и как между ними устроены зависимости? |
| Архитектура: Модули | Что такое модуль и как он устроен? |
| Архитектура: Сегменты | Какие сегменты есть внутри модуля? |
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
| Документирование | Как писать JSDoc: что документировать, а что нет? |
| Типизация | Как типизировать: type vs interface, any/unknown? |
### Установка и настройка
**Как поднять и сконфигурировать проект** — пошаговая настройка инструментов и инфраструктуры.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Создание проекта: Из шаблона | Как начать проект из готового шаблона? |
| Создание проекта: Вручную | Как поднять проект с нуля без шаблона? |
| Next.js | Как настроить Next.js под проект? |
| Алиасы | Как настроить путевые алиасы импортов? |
| Biome | Как настроить Biome (линтер и форматер)? |
| Стили | Как подключить и настроить стили проекта? |
| PostCSS | Какие плагины PostCSS нужны и как их настроить? |
| SVG-спрайты | Как настроить генерацию SVG-спрайтов? |
| Шаблоны генерации | Как подключить шаблоны для кодогенерации? |
| VS Code | Как настроить редактор для проекта? |
### Использование
**Как это устроено и как этим пользоваться** — структура, примеры и правила для конкретных областей.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Структура проекта | Как организованы папки и файлы по SLM? |
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
| Страницы (App Router) | Как описывать layout, page, loading, error, not-found? |
| Данные: Введение | Как устроена работа с данными в проекте? |
| Данные: REST: Клиенты: Автоматическая генерация | Как сгенерировать REST-клиент автоматически из OpenAPI? |
| Данные: REST: Клиенты: Ручное создание | Как написать REST-клиент вручную? |
| Данные: REST: Получение данных: Серверные компоненты | Как получать данные в серверных компонентах? |
| Данные: REST: Получение данных: Клиентские компоненты | Как получать данные в клиентских компонентах (SWR)? |
| Данные: Realtime | Как работать с realtime-каналами и сокетами? |
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
| Изображения | _(не заполнен)_ |
| SVG-спрайты | Как использовать SVG-спрайты в коде? |
| Видео | _(не заполнен)_ |
| Stores | _(не заполнен)_ |
| Хуки | _(не заполнен)_ |
| Шрифты | _(не заполнен)_ |
| Локализация | _(не заполнен)_ |

View File

@@ -0,0 +1,77 @@
---
title: Алиасы импортов
description: Какие алиасы импортов есть в проекте и как ими пользоваться.
keywords: [алиасы, aliases, paths, tsconfig, импорты, baseUrl, app, layouts, screens, widgets, business, infrastructure, ui, shared]
---
# Алиасы импортов
Какие алиасы импортов есть в проекте и как ими пользоваться.
## Конфиг
`tsconfig.json` в корне проекта:
```json
{
"compilerOptions": {
"paths": {
"app/*": ["./src/app/*"],
"layouts/*": ["./src/layouts/*"],
"screens/*": ["./src/screens/*"],
"widgets/*": ["./src/widgets/*"],
"business/*": ["./src/business/*"],
"infrastructure/*": ["./src/infrastructure/*"],
"ui/*": ["./src/ui/*"],
"shared/*": ["./src/shared/*"]
}
}
}
```
Восемь алиасов — ровно по числу слоёв. Других алиасов в проекте нет.
## Правила
- **Каждый импорт между модулями — через алиас слоя.** Относительные пути (`../../`) запрещены за пределами своего модуля.
- **Внутри одного модуля** допустимы относительные импорты (`./model`, `./ui/button`) — это часть инкапсуляции модуля.
- **Префикс `@/` не используется.** Имя слоя — само по себе адрес.
- **Направление импортов** определяется архитектурой, не алиасами. Алиас разрешает импорт технически, но не отменяет правила слоёв (→ [Слои](/docs/basics/architecture/reference/layers)).
**Хорошо**
```ts
import { Button } from 'ui/button'
import { useUser } from 'business/user'
import { formatDate } from 'shared/utils/date'
```
**Плохо**
```ts
// Относительный путь между модулями
import { Button } from '../../../ui/button'
// Префикс @/, которого нет в paths
import { Button } from '@/ui/button'
// Алиас на src — не предусмотрен
import { Button } from 'src/ui/button'
```
## Внутри модуля
Внутри своего модуля — относительные пути:
```ts
// src/ui/button/button.tsx
import styles from './button.module.css'
import { Icon } from './icon'
```
Не использовать алиас на самого себя:
```ts
// Плохо — алиас вместо относительного пути внутри модуля
import { Icon } from 'ui/button/icon'
```

81
docs/docs/setup/biome.md Normal file
View File

@@ -0,0 +1,81 @@
---
title: Biome
description: Установка и настройка линтера-форматтера в новом проекте.
keywords: [biome, линтер, форматтер, lint, format, biome.json, "@biomejs/biome", замена eslint, замена prettier]
---
# Biome
Установка и настройка линтера-форматтера в новом проекте.
## Требования
- Node.js 18+.
- Проект без установленного ESLint и Prettier (они конфликтуют с Biome).
## Установка
1. Установить пакет:
```bash
npm install --save-dev --save-exact @biomejs/biome
```
2. Инициализировать конфиг:
```bash
npx @biomejs/biome init
```
В корне появится `biome.json` с дефолтными настройками.
3. Привести `biome.json` к стандартному виду — добавить override для `*.css` (см. «Стандартный `biome.json`»). Делается сразу после `init`, до первого запуска `lint`/`check`.
4. Добавить скрипты в `package.json`:
```json
{
"scripts": {
"lint": "biome lint .",
"format": "biome format --write .",
"check": "biome check --write ."
}
}
```
| Скрипт | Что делает |
|--------|-----------|
| `lint` | Проверка правил без правок |
| `format` | Автоформатирование всех файлов |
| `check` | Lint + format + organize imports в один проход (основная команда) |
## Стандартный `biome.json`
Дефолтный `biome.json`, созданный `biome init`, кастомизируется ровно одним блоком — `overrides` для `*.css` с отключённым правилом `suspicious/noUnknownAtRules`. Этот override **обязателен по умолчанию во всех проектах**, независимо от того, подключены ли уже стили: проектный CSS-стек использует `@custom-media` и другие нестандартные at-правила, которые Biome не распознаёт; без override `npm run lint` падает.
Фрагмент, который добавляется в `biome.json`:
```jsonc
{
"overrides": [
{
"includes": ["**/*.css"],
"linter": {
"rules": {
"suspicious": {
"noUnknownAtRules": "off"
}
}
}
}
]
}
```
Если в `biome.json` уже есть массив `overrides` — добавить элемент в него; не дублировать массив.
Прочая настройка правил Biome — отдельная задача, не входит в стандартный канон.
## Интеграция с VS Code
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [Настройка VS Code](/docs/setup/vscode).

View File

@@ -0,0 +1,70 @@
---
title: PostCSS
description: Установка и настройка CSS-процессора в новом проекте.
keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, autoprefixer, postcss-global-data, csstools, "@custom-media", "@nest", css-процессор]
---
# PostCSS
Установка и настройка CSS-процессора в новом проекте.
## Зачем PostCSS
Подключаем ради двух вещей:
- **Вложенность** — `&:hover`, `&::before`, `&._active` и `@media` внутри селектора. Без процессора нативный CSS не покрывает всех нужных кейсов вложенности.
- **`@custom-media`** — единые breakpoints проекта (`@media (--md)`) вместо магических `min-width`. Определяются в одном месте, переиспользуются везде.
Autoprefixer и `@csstools/postcss-global-data` идут довеском под эти две задачи.
## Требования
- Next.js 14+ (App Router).
- Node.js 18+.
CSS Modules поддерживаются Next.js из коробки — отдельной установки не требуют.
## Установка
1. Установить PostCSS-плагины как devDependencies:
```bash
npm install -D postcss-custom-media postcss-nesting autoprefixer @csstools/postcss-global-data
```
2. Создать `postcss.config.mjs` в корне проекта (см. «Конфиг»).
## Конфиг
Файл `postcss.config.mjs` в корне проекта.
```js
// postcss.config.mjs
export default {
plugins: {
'@csstools/postcss-global-data': {
files: ['src/shared/styles/media.css'],
},
'postcss-custom-media': {},
'postcss-nesting': {},
autoprefixer: {},
},
}
```
### Разбор плагинов
| Плагин | Назначение |
|--------|------------|
| `@csstools/postcss-global-data` | Подгружает определения `@custom-media` из `src/shared/styles/media.css` перед обработкой каждого CSS-модуля. Семантика — «глобальный файл определений, который не импортируется в исходники» |
| `postcss-custom-media` | Поддержка `@custom-media --md (...)` и использования `@media (--md) {}`. Определения берутся из файла, который подгрузил `postcss-global-data` |
| `postcss-nesting` | Нативная CSS-вложенность: `&:hover`, `&::before`, `&._active` |
| `autoprefixer` | Добавление вендорных префиксов по browserslist |
### Почему внешний файл с `@custom-media`, а не `@import`
`@custom-media` — глобальные определения, одинаковые для всего проекта. Держим их в `src/shared/styles/media.css`. `@csstools/postcss-global-data` подгружает этот файл перед каждым модулем, а `postcss-custom-media` заменяет `@media (--md)` на конкретные `@media (min-width: ...)` на этапе сборки. Сами определения в бандл не попадают.
Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`.
Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование](/docs/usage/styles), раздел «Импорт стилей»).

177
docs/docs/setup/styles.md Normal file
View File

@@ -0,0 +1,177 @@
---
title: Стили
description: "Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили."
keywords: [variables.css, media.css, global.css, shared/styles, токены, переменные, custom-media, breakpoints, подключение стилей, базовые стили, инициализация]
---
# Стили
Подготовка стилевой основы проекта: токены, медиа-запросы, глобальные стили.
## Требования
- Структура `src/` соответствует SLM ([Структура проекта](/docs/usage/project-structure)). Глобальные стили живут в `src/shared/styles/`, не в `src/app/`.
- В проекте нет `globals.css` с кастомным содержимым и не установлен `tailwindcss`.
## Файлы
Состав глобальных стилей — три файла:
| Файл | Роль |
|------|------|
| `variables.css` | Токены проекта (цвета, отступы, радиусы) |
| `media.css` | Custom media queries (брейкпоинты по ширине и высоте) |
| `global.css` | Точка сборки глобальных стилей: через `@import` тянет все остальные глобалы, импортируется в приложение один раз |
Правила подключения:
- В приложение импортируется **только** `global.css`.
- `variables.css` и будущие глобальные файлы (резеты, темы, типографика) подключаются в `global.css` через `@import`.
- `media.css` **не импортируется** — ни в `global.css`, ни в компоненты, ни в точку инициализации. Его читает CSS-процессор на этапе сборки (см. [PostCSS](/docs/setup/postcss)).
## Корневой `font-size`
Базовая единица `rem` в проекте привязана к **16px**: корневой `font-size` не переопределяется. `html { font-size: ... }` писать запрещено — пользовательская настройка размера шрифта в браузере должна работать (a11y). Все `rem`-значения в `media.css` и других стилях трактуются как `1rem = 16px по умолчанию`.
Reset браузерных дефолтов (`box-sizing`, сброс `margin`, типографика) каноном не задаётся — каждый проект решает сам. Если заводится — подключается через `global.css`.
## Установка
### 1. Создать файлы
```bash
mkdir -p src/shared/styles
touch src/shared/styles/variables.css src/shared/styles/media.css src/shared/styles/global.css
```
### 2. Заполнить `media.css`
Файл `src/shared/styles/media.css`. Стандартный набор брейкпоинтов проекта; редактировать только при согласованном изменении шкалы.
Единица — `rem` (реагирует на корневой `font-size`). Перевод исходит из дефолтного `html { font-size: 16px }`, т.е. `1rem = 16px`.
```css
/* src/shared/styles/media.css */
/* Ширина — Mobile First (min-width), кроме --xs (max-width) */
@custom-media --xs (max-width: 35.9375rem); /* 575px — до sm */
@custom-media --sm (min-width: 36rem); /* 576px — телефон альбом / малый планшет */
@custom-media --md (min-width: 48rem); /* 768px — планшет */
@custom-media --lg (min-width: 62rem); /* 992px — малый десктоп */
@custom-media --xl (min-width: 75rem); /* 1200px — десктоп */
@custom-media --2xl (min-width: 88rem); /* 1408px — широкий десктоп */
@custom-media --3xl (min-width: 120rem); /* 1920px — full HD+ */
/* Высота — min-height */
@custom-media --h-xs (min-height: 41.6875rem); /* 667px — iPhone SE портрет */
@custom-media --h-sm (min-height: 43.875rem); /* 702px */
@custom-media --h-md (min-height: 50.625rem); /* 810px — iPad портрет */
@custom-media --h-lg (min-height: 56.25rem); /* 900px */
@custom-media --h-xl (min-height: 62.5rem); /* 1000px */
@custom-media --h-2xl (min-height: 68.75rem); /* 1100px */
@custom-media --h-3xl (min-height: 75rem); /* 1200px */
```
Правила:
- только `@custom-media` на верхнем уровне;
- имена короткие, по шкале (`--xs``--3xl`); высотные — с префиксом `--h-`;
- единица — `rem`, не `em`/`px`; пиксельное значение указывается комментарием;
- значения ширины — `min-width` (Mobile First), исключение `--xs``max-width` (блок «строго меньше `--sm`»);
- значения высоты — `min-height`.
### 3. Заполнить `variables.css`
Файл `src/shared/styles/variables.css`. Набор токенов под проект расширяется по мере роста дизайн-системы.
```css
/* src/shared/styles/variables.css */
:root {
/* Цвета */
--color-primary: #3b82f6;
--color-bg: #ffffff;
--color-bg-hover: #f5f5f5;
--color-text: #1a1a1a;
/* Отступы */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
/* Скругления */
--radius-1: 4px;
--radius-2: 8px;
}
```
Правила:
- все токены определяются в `:root` — без вложенных селекторов;
- именование — `kebab-case` по ролям: `--color-*`, `--space-*`, `--radius-*`;
- `px` — основная единица для пространственных токенов;
- темы накладываются поверх через `[data-theme="..."] { ... }` — в отдельном файле темы или здесь же.
`variables.css` напрямую в приложение не импортируется — только через `global.css`.
### 4. Заполнить `global.css`
Файл `src/shared/styles/global.css`. Единственный глобальный файл, импортируемый в точку инициализации приложения. Внутри — `@import` остальных глобалов относительным путём.
```css
/* src/shared/styles/global.css */
@import './variables.css';
/* Сюда же подключаются будущие глобалы через @import:
* @import './reset.css';
* @import './typography.css';
* @import './themes.css';
* media.css НЕ импортируется — он работает через PostCSS.
*/
```
Правила:
- пути в `@import` — относительные (`./variables.css`), не через алиасы; нативный CSS `@import` не понимает tsconfig-paths;
- `media.css` в `global.css` **не импортируется**;
- собственные глобальные правила (`html { ... }`, `body { ... }`) писать **не здесь**, а в отдельных файлах рядом (`reset.css`, `typography.css`) и подключать через `@import`. `global.css` — только точка сборки;
- порядок `@import` определяет порядок каскада: токены первыми, дальше резеты / темы / типографика.
### 5. Подключить `global.css` в layout
Импорт делается **один раз** — в корневом layout приложения:
```tsx
// src/app/layout.tsx
import 'shared/styles/global.css'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'App',
description: '',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ru">
<body>{children}</body>
</html>
)
}
```
`variables.css` и `media.css` в layout **не импортируются напрямую** — только через `global.css` (variables) или через PostCSS на сборке (media).
## Проверка установки
- В `src/shared/styles/` присутствуют три файла: `variables.css`, `media.css`, `global.css`. В `src/app/` папки `styles/` нет.
- В `src/app/layout.tsx` есть `import 'shared/styles/global.css'`. Импортов `variables.css` и `media.css` там нет.
- В проекте **не появились** PostCSS-пакеты и `postcss.config.*` — этот раздел их не ставит.
- `npm run build` завершается успешно.
## Дальше
- [PostCSS](/docs/setup/postcss) — подключить процессор, чтобы заработали `@media (--md)` и вложенность.
- [Стили: использование](/docs/usage/styles) — правила написания CSS в компонентах.
- [SVG-спрайты](/docs/setup/svg-sprites) — стили иконок отдельно от глобальных.

View File

@@ -0,0 +1,107 @@
---
title: SVG-спрайты
description: Подключение SVG-спрайтов в новом проекте.
keywords: [svg-sprites, установка, настройка, config, пакет, "@gromlab/svg-sprites", svg-sprites.config.ts]
---
# SVG-спрайты
Подключение SVG-спрайтов в новом проекте.
## Требования
- Node.js 18+
- React 18+
## Установка
1. Установить пакет:
```bash
npm install @gromlab/svg-sprites
```
2. Создать `svg-sprites.config.ts` в корне проекта (см. «Стандартный конфиг»).
3. Создать папку входа для SVG-файлов в слое `shared`:
```bash
mkdir -p src/shared/sprites/icons
```
Источники спрайтов живут в `src/shared/sprites/<group>/` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/usage/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим.
4. Добавить скрипты в `package.json`:
```json
{
"scripts": {
"sprite": "svg-sprites",
"predev": "svg-sprites",
"prebuild": "svg-sprites"
}
}
```
Хуки `predev` и `prebuild` гарантируют, что спрайты и типы всегда актуальны перед запуском и сборкой.
5. Добавить сгенерированные артефакты в `.gitignore`:
```text
# Сгенерированные спрайты и React-компонент
/public/sprites/
/src/ui/svg-sprite/
```
6. Выполнить первую генерацию:
```bash
npm run sprite
```
## Стандартный конфиг
Файл `svg-sprites.config.ts` в корне проекта. Это канон — отклонения только по явной причине.
```ts
// svg-sprites.config.ts
import { defineConfig } from '@gromlab/svg-sprites'
export default defineConfig({
output: 'public/sprites',
publicPath: '/sprites',
react: 'src/ui/svg-sprite',
sprites: [
{ name: 'icons', input: 'src/shared/sprites/icons' },
],
})
```
### Фиксированные значения
| Опция | Значение | Почему так |
|-------|----------|------------|
| `output` | `public/sprites` | Единая папка статики Next.js |
| `publicPath` | `/sprites` | URL-путь без `public/` (Next.js раздаёт `public/` как `/`) |
| `react` | `src/ui/svg-sprite` | Слой `ui/` из архитектуры проекта (→ [Архитектура](/docs/basics/architecture/)) |
| `sprites[0].name` | `icons` | Основной спрайт всегда называется `icons` |
### Трансформации
Все значения по умолчанию оставлять включёнными:
```ts
transform: {
removeSize: true,
replaceColors: true,
addTransition: true,
}
```
Явно прописывать блок `transform` не нужно — пакет применяет эти значения по умолчанию.
Отключать `replaceColors` — только для отдельного спрайта с фиксированной палитрой (например, брендовые логотипы). Делать это на уровне спрайта, не глобально.
### Режим
По умолчанию `mode: 'stack'` — не указывать явно. Переход на `symbol` требует обоснования: превью и примеры в пакете оптимизированы под `stack`.

View File

@@ -0,0 +1,105 @@
---
title: Шаблоны генерации
description: Подключение шаблонов кодогенерации в новом проекте.
keywords: [шаблоны, templates, .templates, tiged, generator, генератор шаблонов, добавить шаблон, скачать шаблоны, scaffold]
---
# Шаблоны генерации
Подключение шаблонов кодогенерации в новом проекте.
## Требования
- Доступен `npx` (ставится вместе с npm).
- Доступ к `gromlab.ru` (SSH-ключ добавлен) — для скачивания эталонного набора.
## Развилка
Сценарий зависит от состояния проекта:
| Состояние | Что делать |
|-----------|------------|
| В корне проекта **нет** `.templates/` | [Скачать стандартный набор](#скачать-стандартный-набор) |
| `.templates/` **есть**, нужен новый специфический шаблон | [Создать шаблон вручную](#создать-шаблон-вручную) |
| `.templates/` **есть**, нужно обновить до эталона | Скачать заново с подтверждением перезаписи |
## Скачать стандартный набор
### Установка
1. Проверить, что `.templates/` отсутствует (или согласовать перезапись, если папка уже есть).
2. Скачать папку из эталонного репозитория:
```bash
npx tiged git@gromlab.ru:templates/nextjs-template.git/.templates .templates
```
3. Если `tiged` падает в default-режиме (HTTP-tarball у Gitea) — повторить с явным git-режимом:
```bash
npx tiged --mode=git git@gromlab.ru:templates/nextjs-template.git/.templates .templates
```
4. Проверить генерацию:
```bash
npx @gromlab/create component test-widget src/ui
```
После проверки — удалить тестовый модуль.
### Правила
- **Скачанные файлы не править.** Источник истины — репозиторий `templates/nextjs-template`. Изменения стандартных шаблонов делаются в нём, не в отдельных проектах.
- **Расположение — только `.templates/` в корне проекта.** Это требование расширения VS Code и CLI ([Шаблоны и генерация кода](/docs/usage/templates-generation)).
- **Если доступа к `gromlab.ru` нет** — не восстанавливать содержимое из памяти: разойдётся с каноном. Запросить шаблоны иным путём.
## Создать шаблон вручную
Если стандартного набора недостаточно и нужен специфический шаблон под проект.
### Установка
1. Прочитать [Шаблоны и генерация кода](/docs/usage/templates-generation) — секции «Структура шаблонов», «Синтаксис шаблонов», «Как создать новый шаблон».
2. Определить:
- имя шаблона (папка внутри `.templates/`);
- состав файлов;
- слой SLM предполагаемых потребителей ([Архитектура: слои](/docs/basics/architecture/reference/layers));
- минимальное содержимое каждого файла.
3. Проверить, что имя шаблона не конфликтует с существующей папкой в `.templates/`.
4. Создать структуру `.templates/{name}/` по канону из [Шаблоны и генерация кода](/docs/usage/templates-generation) — синтаксис переменных, правила именования файлов и содержимого.
5. Проверить генерацию:
```bash
npx @gromlab/create {name} test-entity {целевой путь}
```
После проверки — удалить тестовый артефакт.
### Правила
- **Если запрос покрыт стандартными шаблонами** (`component`, `widget`, `store`, `layout`, `screen`, `business`) — не создавать копию.
- **Стандартные шаблоны не трогать.** Правка стандарта — задача репозитория `templates/nextjs-template`, не отдельного проекта.
- **Синтаксис переменных, правила регистра, минимальный boilerplate** — в [Шаблоны и генерация кода](/docs/usage/templates-generation). Здесь не дублируется.
## Общие правила
- VS Code-расширение `gromlab.vscode-templateFileGenerator` устанавливается разово на машину разработчика, а не через этот раздел ([Шаблоны и генерация кода](/docs/usage/templates-generation)).
- CLI `@gromlab/create` вызывается через `npx`, в `package.json` отдельно не добавляется.
- Менеджер пакетов (npm/pnpm/yarn/bun) не влияет: `tiged` и `@gromlab/create` запускаются через `npx`.
## Проверка установки
- В корне проекта есть папка `.templates/`.
- Внутри `.templates/` присутствуют стандартные шаблоны (или согласованный кастомный набор).
- В корне проекта нет каталогов `.git/` и `.github/` из репозитория-шаблона.
- Пробная генерация через `npx @gromlab/create ...` отрабатывает без ошибок.
## Дальше
- [Шаблоны и генерация кода](/docs/usage/templates-generation) — синтаксис, использование, создание новых шаблонов.

View File

@@ -1,10 +1,11 @@
--- ---
title: Настройка VS Code title: VS Code
description: Единые настройки редактора и расширений для команды.
--- ---
# Настройка VS Code # VS Code
Каждый проект содержит папку `.vscode/` с конфигурацией редактора. Это гарантирует, что все участники команды работают с одинаковыми настройками форматирования, линтинга и расширениями. Единые настройки редактора и расширений для команды.
## Структура `.vscode/` ## Структура `.vscode/`

View File

@@ -1,13 +1,11 @@
--- ---
title: Компоненты title: Компоненты
description: Как устроен и пишется React-компонент в проекте.
--- ---
# Компоненты # Компоненты
Правила написания React-компонентов: файловая структура модуля, типизация пропсов, документирование и реализация. Раздел охватывает компоненты всех слоёв — от `shared/ui` до `screens`. Как устроен и пишется React-компонент в проекте.
Архитектурные слои и их назначение описаны в разделе [Архитектура](/ru/basics/architecture/).
## Правила организации ## Правила организации
@@ -43,7 +41,7 @@ container/
- **`type` вместо `interface`** — гибче для пропсов: поддерживает union, intersection, mapped types. Declaration merging пропсам не нужно. - **`type` вместо `interface`** — гибче для пропсов: поддерживает union, intersection, mapped types. Declaration merging пропсам не нужно.
- **Без `FC`** — неявно добавляет `children`, усложняет дженерики, не даёт преимуществ перед аннотацией параметра. - **Без `FC`** — неявно добавляет `children`, усложняет дженерики, не даёт преимуществ перед аннотацией параметра.
- **Типы в `types/`, а не в `.tsx`** — предотвращает циклические зависимости (компонент импортирует хук, хук импортирует тип из компонента) и разделяет ответственность: `.tsx` для рендера, `.type.ts` для данных. - **Типы в `types/`, а не в `.tsx`** — предотвращает циклические зависимости (компонент импортирует хук, хук импортирует тип из компонента) и разделяет ответственность: `.tsx` для рендера, `.type.ts` для данных.
- **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/ru/basics/typing). - **Без возвращаемого типа** — TypeScript выводит из JSX. Осознанное исключение из [базового правила](/docs/basics/typing).
## Реализация ## Реализация

View File

@@ -0,0 +1,51 @@
---
title: Источники данных
description: Какие источники данных используются в проекте и как с ними работать.
keywords: [данные, api, rest, realtime, клиент, swr, infrastructure, введение, карта раздела]
---
# Источники данных
Какие источники данных используются в проекте и как с ними работать.
## Принципы раздела
- **Клиент — в `infrastructure/`.** Каждый внешний сервис — отдельный модуль слоя `infrastructure/{service-name}/`.
- **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные.
- **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление.
- **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые хуки модуля API (`useUserList`, `usePostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает.
## Карта раздела
### REST
Канал «запрос-ответ» по HTTP. Покрывает большинство API.
- **Клиенты** — как создаётся клиент REST API:
- [Автоматическая генерация](/docs/usage/data/rest/clients/auto) — для API с OpenAPI-спецификацией, через `@gromlab/api-codegen`.
- [Ручное создание](/docs/usage/data/rest/clients/manual) — для API без схемы, клиент пишется и поддерживается руками.
- **Получение данных** — как клиент используется в приложении:
- [Серверные компоненты](/docs/usage/data/rest/fetching/server) — прямой `await` метода клиента в Server Components.
- [Клиентские компоненты](/docs/usage/data/rest/fetching/client) — через готовые хуки модуля API; SWR с кешем, дедупликацией и ревалидацией скрыт внутри хука.
### Realtime
Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка».
- [Realtime](/docs/usage/data/realtime) — клиент realtime в `infrastructure/`, потребление через `useSWRSubscription` или прямые подписки.
## Что даёт раздел
После прочтения раздела понятно:
- Где живёт код работы с API и почему именно там.
- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов.
- Как получать данные на сервере и на клиенте, чтобы не ломать кеш и не плодить лишние запросы.
- Как подключать realtime-источники в общую модель работы с данными.
- Какие правила обязательны и какие отклонения допустимы.
## Что не входит в раздел
- **Глобальное состояние UI** — Stores, формы, фичефлаги. Это [Stores](/docs/usage/stores).
- **Доменная логика** — как данные превращаются в сценарии бизнеса. Это слой `business/` в [Архитектуре](/docs/basics/architecture/).
- **Хуки общего назначения** — переиспользуемые хуки UI, не привязанные к конкретному API. Это [Хуки](/docs/usage/hooks).

View File

@@ -0,0 +1,79 @@
---
title: Realtime
description: "Работа с push-данными от сервера: подписки и события."
keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRSubscription, push, события]
---
# Realtime
Работа с push-данными от сервера: подписки и события.
## Принципы
- **Клиент realtime — в `infrastructure/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения.
- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт.
- **Использование на клиенте — два сценария:**
- **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST.
- **Прямая подписка** — для побочных эффектов (тосты, нотификации, аналитика), не привязанных к рендеру.
## Размещение клиента
```text
src/infrastructure/
└── {channel-name}/
├── connection.ts # установление соединения, реконнект
├── subscribe.ts # subscribe(topic, handler) → unsubscribe
├── types.ts
└── index.ts
```
## Использование через SWR
```tsx
'use client'
import useSWRSubscription from 'swr/subscription'
import { subscribe } from 'infrastructure/notifications'
export function NotificationCounter() {
const { data: count } = useSWRSubscription(
['notifications', 'count'],
(key, { next }) =>
subscribe('notifications.count', (value: number) => next(null, value)),
)
return <span>{count ?? 0}</span>
}
```
Плюсы: кеш и дедупликация подписки между несколькими местами рендера; единая модель данных с REST.
## Прямая подписка
Для побочных эффектов, которые не влияют на состояние UI напрямую:
```tsx
'use client'
import { useEffect } from 'react'
import { subscribe } from 'infrastructure/notifications'
import { showToast } from 'ui/toast'
export function NotificationsToaster() {
useEffect(() => {
return subscribe('notifications.new', (notification) => {
showToast(notification.message)
})
}, [])
return null
}
```
Возврат `unsubscribe` из `useEffect` обязателен — иначе утечка подписки.
## Запрет прямых соединений
Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infrastructure/`.
Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием.

View File

@@ -0,0 +1,279 @@
---
title: Автоматическая генерация
description: Генерация REST-клиента из OpenAPI-спецификации.
keywords: [api, rest, openapi, codegen, генерация, клиент, api-codegen, gromlab, infrastructure, swagger-typescript-api]
---
# Автоматическая генерация
Генерация REST-клиента из OpenAPI-спецификации.
В примерах ниже используется условный API `pet-project-api` (kebab-case в путях) / `petProjectApi` (camelCase в коде). В реальном проекте имена выбираются по конкретному API.
## Установка
```bash
npm install -D @gromlab/api-codegen
```
Скрипт генерации в `package.json` — по одному на каждый API:
```json
{
"scripts": {
"codegen:pet-project-api": "api-codegen --config src/infrastructure/pet-project-api/config/pet-project-api.config.ts"
}
}
```
Конфиг и опции — в репозитории [@gromlab/api-codegen](https://gromlab.ru/gromov/api-codegen).
## Структура модуля
Клиент кладётся в слой `infrastructure/` отдельным модулем по имени API (kebab-case):
```text
src/infrastructure/
└── pet-project-api/
├── generated/ # сегмент сгенерированного кода
│ └── pet-project-api.generated.ts # сгенерировано — не править
├── types/ # расширения сгенерированных типов
│ ├── user.ts # declare module + Extended-тип
│ └── index.ts # реэкспорт расширений
├── hooks/ # SWR-хуки для клиентских компонентов
│ ├── use-user-list.hook.ts
│ ├── use-user-detail.hook.ts
│ └── index.ts # реэкспорт хуков
├── config/ # конфиги модуля
│ └── pet-project-api.config.ts # конфиг генерации клиента
├── client.ts # настройка HttpClient, инстанс Api
└── index.ts # публичный API модуля
```
| Файл | Роль | Кто правит |
|------|------|-----------|
| `generated/{service-name}.generated.ts` | Сгенерированный код: типы, `class Api`, `class HttpClient` | codegen, не править |
| `types/{сущность}.ts` | `declare module` + `Extended`-типы по сущности | разработчик |
| `types/index.ts` | Реэкспорт публичных расширений | разработчик |
| `hooks/use-{action}.hook.ts` | SWR-хук поверх метода клиента | разработчик |
| `hooks/index.ts` | Реэкспорт хуков | разработчик |
| `config/{service-name}.config.ts` | Параметры генерации для конкретного API | разработчик |
| `client.ts` | `baseUrl` из env, конфиг `HttpClient`, инстанс `new Api(...)` | разработчик |
| `index.ts` | Публичный API: инстанс сервиса, расширенные типы, хуки | разработчик |
`client.ts` и `index.ts` — единственные корневые файлы модуля. Все остальные файлы живут в сегментах (`generated/`, `types/`, `hooks/`, `config/`).
Имя сгенерированного файла — `{service-name}.generated.ts` (имя сервиса в kebab-case + суффикс `.generated.ts`). Суффикс сигнализирует «не править руками».
## `client.ts`
Тонкий ручной слой поверх сгенерированного кода. Делает три вещи: читает и нормализует `baseUrl`, конфигурирует `HttpClient`, создаёт **именованный инстанс** сервиса.
```ts
// src/infrastructure/pet-project-api/client.ts
import { Api, HttpClient } from './generated/pet-project-api.generated'
const resolvedBaseUrl = process.env.NEXT_PUBLIC_API_URL
.replace(/\/+$/, '') // убираем хвостовой слэш
.replace(/\/v1$/, '') // версия уже в путях методов — режем дубль
const httpClient = new HttpClient({
baseApiParams: {
secure: false,
headers: {
'Content-Type': 'application/json',
// кастомные заголовки API — если требуются
// 'X-App-Key': '...',
},
},
})
httpClient.baseUrl = resolvedBaseUrl
export const petProjectApi = new Api(httpClient)
```
### Имя инстанса = имя сервиса
Инстанс называется по имени API в camelCase, не унифицированно `api`/`client`. Это даёт **процедурное обращение** и однозначность при работе с несколькими сервисами:
```ts
import { petProjectApi } from 'infrastructure/pet-project-api'
const user = await petProjectApi.user.getUser(id)
```
При нескольких API — каждый со своим именем:
```ts
import { petProjectApi } from 'infrastructure/pet-project-api'
import { paymentsApi } from 'infrastructure/payments-api'
const user = await petProjectApi.user.list()
const invoice = await paymentsApi.invoices.list()
```
### Нормализация `baseUrl`
`@gromlab/api-codegen` может включать версию (`/v1`) в `baseUrl` сгенерированного кода, а пути методов уже содержат её — отсюда дубль. Стандартный приём:
```ts
.replace(/\/+$/, '') // хвостовой слэш
.replace(/\/v1$/, '') // версия (если фигурирует в путях)
```
Подгоняется под конкретный API: если версия в путях не повторяется — второй `replace` не нужен.
## Расширения типов
Автогенерация не покрывает все реальные поля API: иногда тип `object`, иногда поле просто отсутствует. Расширения живут в `types/`, по файлу на сущность.
Две техники:
### `declare module` — добавление полей
Дополняет существующий интерфейс из `generated.ts`. Сама сгенерированная декларация не трогается.
```ts
// src/infrastructure/pet-project-api/types/user.ts
import type { User } from '../generated/pet-project-api.generated'
declare module '../generated/pet-project-api.generated' {
interface User {
avatar?: {
file?: string
title?: string
url?: string
}
}
}
```
### `Extended` через `Omit & {...}` — переопределение полей
Когда автогенерация даёт `object` или общий тип, а реально структура известна — создаётся отдельный тип `UserExtended` (по имени сущности + суффикс `Extended`).
```ts
// src/infrastructure/pet-project-api/types/user.ts
export type UserExtended = Omit<User, 'roles' | 'tags' | 'fields'> & {
roles?: Array<{ _id?: string; id?: string; slug?: string; name?: string }>
tags?: Array<{ _id?: string; id?: string; slug?: string; name?: string }>
fields?: Record<string, unknown>
}
```
### Реэкспорт
```ts
// src/infrastructure/pet-project-api/types/index.ts
export type { UserExtended } from './user'
```
### Правила
- Расширения — **только в `types/`**, не в `client.ts` и не в сгенерированном файле.
- Один файл на сущность (имя файла — kebab-case по сущности: `user.ts`, `order.ts`, `invoice.ts`).
- При регенерации `generated/{service-name}.generated.ts` файлы в `types/` не затрагиваются.
- Если сломался `Extended`-тип после regen — синхронизировать руками.
## Хуки для клиентских компонентов
В клиентских компонентах вызовы клиента не делаются напрямую — компонент получает готовый хук, который инкапсулирует SWR + метод клиента. Хуки живут в сегменте `hooks/`, по файлу на операцию.
```ts
// src/infrastructure/pet-project-api/hooks/use-user-list.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petProjectApi } from '../client'
import type { User } from '../generated/pet-project-api.generated'
/**
* Получение списка пользователей.
*/
export const useUserList = (
query?: { limit?: number; offset?: number },
config?: SWRConfiguration,
) => {
return useSWR<User[]>(
['pet-project-api', 'user', 'list', query],
() => petProjectApi.user.list(query ?? {}),
config,
)
}
```
```ts
// src/infrastructure/pet-project-api/hooks/use-user-detail.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petProjectApi } from '../client'
import type { UserExtended } from '../types'
/**
* Получение пользователя по идентификатору.
*/
export const useUserDetail = (
id: string | null,
config?: SWRConfiguration,
) => {
const key = id ? ['pet-project-api', 'user', 'detail', id] : null
const fetcher = () => petProjectApi.user.getUser(id!) as Promise<UserExtended>
return useSWR<UserExtended>(key, fetcher, config)
}
```
```ts
// src/infrastructure/pet-project-api/hooks/index.ts
export { useUserList } from './use-user-list.hook'
export { useUserDetail } from './use-user-detail.hook'
```
### Правила хуков
- **Один файл — один хук**, имя файла `use-{action}.hook.ts` ([Именование](/docs/basics/naming)).
- **Тонкая обёртка над SWR.** Внутри — построение ключа, fetcher через метод клиента, возврат `useSWR(...)`. Никакой бизнес-логики.
- **Ключ начинается с имени сервиса** (`['pet-project-api', ...]`) — изолирует кеш между разными API.
- **Условный ключ для опциональных параметров:** `id ? [...key, id] : null`. `null` приостанавливает запрос, пока параметры не готовы.
- **Параметр `config?: SWRConfiguration`** — даёт потребителю переопределить ревалидацию, `fallbackData`, `suspense` и т.п. без обёрток.
## Публичный API модуля
Из `index.ts` экспортируются инстанс, расширенные типы и хуки. Сырые типы из `generated/` экспортируются по необходимости — точечно.
```ts
// src/infrastructure/pet-project-api/index.ts
export { petProjectApi } from './client'
export type { UserExtended } from './types'
export * from './hooks'
```
## Регенерация
При изменении OpenAPI-схемы:
```bash
npm run codegen:pet-project-api
```
Что меняется:
- `generated/{service-name}.generated.ts` — перезаписывается полностью, изменения коммитятся.
- `client.ts`, `types/`, `config/`, `index.ts`**не трогаются** автоматически.
Поломка контракта (изменение типов в схеме) ловится TypeScript при сборке проекта. Если ломаются `Extended`-типы — синхронизировать вручную в соответствующих файлах `types/`.
## Сгенерированный файл коммитится
Файл `generated/{service-name}.generated.ts` **не добавляется в `.gitignore`** — попадает в репозиторий вместе с остальным кодом.
Причины:
- **Детерминированная сборка.** `npm run build` не зависит от доступности OpenAPI-схемы (обычно она на удалённом сервере). Сервис лёг — прод собирается.
- **Видимость изменений в PR.** Diff показывает, что именно поменялось в контракте API между версиями.
- **Простой онбординг.** После `git clone` IDE сразу видит типы, без предварительной генерации.
- **Фиксация версии контракта.** Пересборка старого коммита даёт ровно тот клиент, что был тогда.
Регенерация — **ручная команда** при обновлении схемы, не хук `predev`/`prebuild`. Запускается осознанно.
Исключение возможно, только если OpenAPI-схема лежит **в этом же репозитории** и генерация быстрая, без сети — тогда допустимо добавить сегмент `generated/` в `.gitignore` и хук `prebuild`, по аналогии со спрайтами. На практике встречается редко.

View File

@@ -0,0 +1,365 @@
---
title: Ручное создание
description: "Создание REST-клиента вручную, когда нет OpenAPI-спецификации."
keywords: [api, rest, клиент, ручной, fetch, infrastructure, api-клиент]
---
# Ручное создание
Создание REST-клиента вручную, когда нет OpenAPI-спецификации.
В примерах ниже используется условный API `pet-project-api` / `petProjectApi`. В реальном проекте имена выбираются по конкретному API.
## Структура модуля
Клиент живёт в слое `infrastructure/` отдельным модулем по имени API (kebab-case):
```text
src/infrastructure/
└── pet-project-api/
├── methods/ # методы по сущностям API
│ ├── pages.ts
│ ├── posts.ts
│ └── forms.ts
├── hooks/ # SWR-хуки для клиентских компонентов
│ ├── use-post-detail.hook.ts
│ ├── use-post-filter.hook.ts
│ └── index.ts
├── types/ # типы клиента и доменные типы
│ ├── client.ts # типы клиента: RequestOptions, ParamValue
│ ├── post.ts # доменные типы сущности post
│ ├── form.ts # доменные типы сущности form
│ └── index.ts # реэкспорт публичных типов
├── errors/ # доменные ошибки API
│ └── pet-project-api.error.ts
├── client.ts # класс клиента: baseUrl, headers, get/post
└── index.ts # публичный API модуля
```
| Файл | Роль |
|------|------|
| `client.ts` | Класс `PetProjectApiClient`: `baseUrl`, общие заголовки, `buildUrl`, базовые `get`/`post` |
| `methods/{entity}.ts` | Методы по сущности, экспортируются фабрикой `{entity}Methods(client)` |
| `hooks/use-{action}.hook.ts` | SWR-хук поверх метода клиента |
| `hooks/index.ts` | Реэкспорт хуков |
| `types/client.ts` | Типы инфраструктуры клиента: `RequestOptions`, `PostOptions`, `ParamValue` |
| `types/{entity}.ts` | Доменные типы: запросы, ответы, фильтры по сущности |
| `types/index.ts` | Реэкспорт публичных типов |
| `errors/{service-name}.error.ts` | Доменный класс ошибок API |
| `index.ts` | Публичный API: инстанс клиента, хуки, доменные ошибки, типы |
`methods/`, `hooks/`, `types/`, `errors/` — сегменты модуля по канону SLM. `client.ts` и `index.ts` — единственные корневые файлы.
## Типы клиента
Типы, описывающие саму инфраструктуру запросов (опции, параметры) — выносятся в `types/client.ts`. Это держит `client.ts` коротким и не смешивает декларации типов с реализацией класса.
```ts
// src/infrastructure/pet-project-api/types/client.ts
export type ParamValue = string | number | (string | number)[]
export type RequestOptions = {
params?: Record<string, ParamValue>
headers?: Record<string, string>
revalidate?: number | false
}
export type PostOptions = RequestOptions & {
type?: 'json' | 'formdata'
}
```
## Базовый клиент
Класс с конфигурацией (`baseUrl`, общие заголовки) и базовыми методами `get` / `post`. Конкретные методы API размещаются в сегменте `methods/`, а не на самом классе — это держит `client.ts` коротким и не плодит «бога-класс».
```ts
// src/infrastructure/pet-project-api/client.ts
import { PetProjectApiError } from './errors/pet-project-api.error'
import type { ParamValue, RequestOptions, PostOptions } from './types/client'
export class PetProjectApiClient {
constructor(
private readonly baseUrl: string,
private readonly defaultHeaders: Record<string, string> = {},
) {
this.defaultHeaders = {
Accept: 'application/json',
...defaultHeaders,
}
}
buildUrl(path: string, params?: Record<string, ParamValue>): string {
const base = this.baseUrl.replace(/\/+$/, '')
const tail = path.replace(/^\/+/, '')
const url = `${base}/${tail}`
if (!params) {
return url
}
const search = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
value.forEach((v) => search.append(key, String(v)))
} else {
search.set(key, String(value))
}
}
return `${url}?${search}`
}
async get<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { params, headers, revalidate } = options
const response = await fetch(this.buildUrl(path, params), {
headers: { ...this.defaultHeaders, ...headers },
...(revalidate !== undefined && { next: { revalidate } }),
})
if (!response.ok) {
throw await PetProjectApiError.fromResponse(response)
}
return response.json() as Promise<T>
}
async post<T>(path: string, body: unknown, options: PostOptions = {}): Promise<T> {
const { params, headers, revalidate, type = 'json' } = options
const isJson = type === 'json'
const response = await fetch(this.buildUrl(path, params), {
method: 'POST',
headers: {
...this.defaultHeaders,
...(isJson && { 'Content-Type': 'application/json' }),
...headers,
},
body: isJson ? JSON.stringify(body) : (body as BodyInit),
...(revalidate !== undefined && { next: { revalidate } }),
})
if (!response.ok) {
throw await PetProjectApiError.fromResponse(response)
}
return response.json() as Promise<T>
}
}
```
### Ключевые требования к клиенту
- **Класс с приватным состоянием** (`baseUrl`, `defaultHeaders`) — конфигурация инкапсулирована.
- **Типы клиента — в `types/client.ts`**, не в `client.ts`. Реализация и контракты разделены.
- **Базовые методы дженерик `<T>` без дефолта.** Вызов без типа невозможен — потребитель обязан указать форму ответа.
- **Доменная ошибка вместо `null`.** При не-`ok` бросается `PetProjectApiError`. Возврат `null` глотает причины (404 vs 500 vs 401) — не использовать.
- **Дефолт POST — `json`.** `formdata` указывается явно, на конкретных методах (загрузка файлов, отправка форм).
- **Нормализация слэшей** в `buildUrl``baseUrl` без хвостового `/`, `path` без ведущего `/`.
- **`async/await`**, не `.then()` — линейное чтение, простая обработка ошибок.
- **Поддержка `next.revalidate`** — клиент знает о Next.js App Router и пробрасывает кеш-флаги.
## Доменная ошибка
Сетевая ошибка превращается в класс ошибки модуля. Наружу не выходит сырой `Response`.
```ts
// src/infrastructure/pet-project-api/errors/pet-project-api.error.ts
export class PetProjectApiError extends Error {
constructor(
public readonly status: number,
public readonly body: string,
) {
super(`PetProjectApi ${status}: ${body.slice(0, 200)}`)
this.name = 'PetProjectApiError'
}
static async fromResponse(response: Response): Promise<PetProjectApiError> {
const body = await response.text().catch(() => '')
return new PetProjectApiError(response.status, body)
}
}
```
Дополнительные подклассы по необходимости: `PetProjectApiValidationError` (400), `PetProjectApiAuthError` (401/403), `PetProjectApiNotFoundError` (404). Вводятся когда у потребителя есть **разная реакция** на разные коды; иначе хватает базового класса.
## Доменные типы
Типы запросов, ответов и фильтров — по файлу на сущность. Типы должны лежать рядом по смыслу: всё, что относится к `posts`, — в `types/post.ts`.
```ts
// src/infrastructure/pet-project-api/types/post.ts
export type Post = {
id: string
slug: string
title: string
content: string
publishedAt: string
}
export type PostFilter = {
limit?: number
categories?: number[]
}
```
```ts
// src/infrastructure/pet-project-api/types/index.ts
export type * from './post'
export type * from './form'
// типы клиента — внутренние, наружу не реэкспортируются
```
Типы клиента (`RequestOptions`, `PostOptions`, `ParamValue`) **не реэкспортируются** через `types/index.ts` — они нужны только внутри модуля.
## Методы
Методы группируются по сущностям в сегменте `methods/`, экспортируются фабрикой, принимающей клиент. Это даёт **процедурное обращение** в стиле автогенерированного клиента (`petProjectApi.posts.get(slug)`), а не плоский список (`petProjectApi.getPost(slug)`).
```ts
// src/infrastructure/pet-project-api/methods/posts.ts
import type { PetProjectApiClient } from '../client'
import type { Post, PostFilter } from '../types/post'
export function postsMethods(client: PetProjectApiClient) {
return {
/** GET /posts/{slug} */
get: (slug: string, options?: { revalidate?: number | false }) =>
client.get<Post>(`posts/${slug}`, options),
/** POST /posts/filter */
filter: (body: PostFilter) =>
client.post<Post[]>('posts/filter', body),
}
}
```
```ts
// src/infrastructure/pet-project-api/methods/forms.ts
import type { PetProjectApiClient } from '../client'
import type { Form, FormSubmissionResult } from '../types/form'
export function formsMethods(client: PetProjectApiClient) {
return {
/** GET /forms/{id} */
get: (id: string) => client.get<Form>(`forms/${id}`),
/** POST /forms/{id} — multipart/form-data */
submit: (id: string, data: FormData) =>
client.post<FormSubmissionResult>(`forms/${id}`, data, { type: 'formdata' }),
}
}
```
### Правила методов
- **Группировка по сущности** (`pages`, `posts`, `forms`), не плоский список.
- **Имя метода — глагол действия**: `get`, `list`, `filter`, `create`, `update`, `delete`, `submit`. Не `getPost`/`getPosts` — сущность уже в имени группы.
- **Типы запросов и ответов — в `types/{entity}.ts`**, импортируются в файл методов. В `methods/` лежит только композиция вызовов клиента, без объявлений типов.
- **Фабрика принимает клиент** — это даёт тестируемость (моковый клиент в юнит-тестах) и единый источник конфигурации.
- **Никаких знаний об UI.** Клиент не знает про React, SWR, тосты — только данные и ошибки.
## Сборка инстанса
Группы методов соединяются в один объект на уровне `index.ts`. Это даёт процедурный доступ `petProjectApi.posts.get(...)`.
```ts
// src/infrastructure/pet-project-api/index.ts
import { PetProjectApiClient } from './client'
import { pagesMethods } from './methods/pages'
import { postsMethods } from './methods/posts'
import { formsMethods } from './methods/forms'
const client = new PetProjectApiClient(process.env.NEXT_PUBLIC_API_URL, {
'X-App-Key': process.env.NEXT_PUBLIC_APP_KEY,
})
export const petProjectApi = {
pages: pagesMethods(client),
posts: postsMethods(client),
forms: formsMethods(client),
}
export { PetProjectApiError } from './errors/pet-project-api.error'
export type { Post, PostFilter, Page, Form } from './types'
export * from './hooks'
```
## Хуки для клиентских компонентов
В клиентских компонентах вызовы клиента не делаются напрямую — компонент получает готовый хук, который инкапсулирует SWR + метод клиента. Хуки живут в сегменте `hooks/`, по файлу на операцию.
```ts
// src/infrastructure/pet-project-api/hooks/use-post-detail.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petProjectApi } from '..'
import type { Post } from '../types/post'
/**
* Получение поста по slug.
*/
export const usePostDetail = (
slug: string | null,
config?: SWRConfiguration,
) => {
const key = slug ? ['pet-project-api', 'post', 'detail', slug] : null
const fetcher = () => petProjectApi.posts.get(slug!)
return useSWR<Post>(key, fetcher, config)
}
```
```ts
// src/infrastructure/pet-project-api/hooks/use-post-filter.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petProjectApi } from '..'
import type { Post, PostFilter } from '../types/post'
/**
* Получение списка постов по фильтру.
*/
export const usePostFilter = (
filter: PostFilter,
config?: SWRConfiguration,
) => {
return useSWR<Post[]>(
['pet-project-api', 'post', 'filter', filter],
() => petProjectApi.posts.filter(filter),
config,
)
}
```
```ts
// src/infrastructure/pet-project-api/hooks/index.ts
export { usePostDetail } from './use-post-detail.hook'
export { usePostFilter } from './use-post-filter.hook'
```
### Правила хуков
- **Один файл — один хук**, имя файла `use-{action}.hook.ts` ([Именование](/docs/basics/naming)).
- **Тонкая обёртка над SWR.** Внутри — построение ключа, fetcher через метод клиента, возврат `useSWR(...)`. Никакой бизнес-логики.
- **Ключ начинается с имени сервиса** (`['pet-project-api', ...]`) — изолирует кеш между разными API.
- **Условный ключ для опциональных параметров:** `id ? [...key, id] : null`. `null` приостанавливает запрос, пока параметры не готовы.
- **Параметр `config?: SWRConfiguration`** — даёт потребителю переопределить ревалидацию, `fallbackData`, `suspense` и т.п. без обёрток.
## Запрет прямого `fetch`
В коде приложения (слои выше `infrastructure`) прямые вызовы `fetch` к API запрещены. Все запросы идут через клиент.
Исключение допускается точечно — например, разовая отладочная проверка эндпоинта в скрипте — и требует обоснования в коде (комментарий с причиной).
## Использование
```ts
import { petProjectApi } from 'infrastructure/pet-project-api'
const post = await petProjectApi.posts.get('my-post')
const list = await petProjectApi.posts.filter({ limit: 10, categories: [1, 2] })
const form = await petProjectApi.forms.get('contact')
```
Стиль вызовов совпадает с автогенерированным клиентом — потребитель не различает, ручной API или сгенерирован.

View File

@@ -0,0 +1,164 @@
---
title: Клиентские компоненты
description: Получение REST-данных в клиентских компонентах.
keywords: [swr, клиентские компоненты, useSWR, хук, мутация, useSWRMutation, кеш, ревалидация]
---
# Клиентские компоненты
Получение REST-данных в клиентских компонентах.
## Правила
- **Только готовые хуки.** В компоненте — `usePostDetail(slug)`, не `useSWR(['post', slug], () => api.posts.get(slug))`.
- **`useSWR` пишется один раз — в `hooks/`** модуля API. В клиентских компонентах никогда напрямую.
- **Прямой вызов методов клиента в `useEffect` запрещён.** Это потеря кеша, повторные запросы и гонки.
- **Мутации — через `useSWRMutation`**, тоже инкапсулированный в хуке. В компоненте вызывается готовый `trigger`.
## Чтение
```tsx
'use client'
import { usePostDetail } from 'infrastructure/pet-project-api'
export function PostView({ slug }: { slug: string }) {
const { data: post, error, isLoading } = usePostDetail(slug)
if (isLoading) return <Spinner />
if (error) return <ErrorView error={error} />
return <article>{post?.title}</article>
}
```
В компоненте нет `useSWR`, нет ключей, нет fetcher — только готовый хук.
## Параметризованный запрос
Хук сам обрабатывает «нет параметра — нет запроса». В компоненте можно безопасно передавать `null`:
```tsx
'use client'
import { useUserDetail } from 'infrastructure/pet-project-api'
export function UserProfile({ userId }: { userId: string | null }) {
const { data: user } = useUserDetail(userId)
if (!userId) return <EmptyState />
return <UserCard user={user} />
}
```
Внутри `useUserDetail` ключ становится `null`, когда `userId` не задан, и SWR не делает запрос — это поведение зашито в хук, потребитель об этом не думает.
## Мутации
Мутации тоже оборачиваются в хук модуля API:
```ts
// src/infrastructure/pet-project-api/hooks/use-create-user.hook.ts
import useSWRMutation from 'swr/mutation'
import { mutate } from 'swr'
import { petProjectApi } from '..'
import type { User, UserCreateInput } from '../types'
/**
* Создание пользователя с инвалидацией списка.
*/
export const useCreateUser = () => {
return useSWRMutation<User, Error, [string, string, string], UserCreateInput>(
['pet-project-api', 'user', 'create'],
(_key, { arg }) => petProjectApi.user.create(arg),
{
onSuccess: () => mutate(['pet-project-api', 'user', 'list']),
},
)
}
```
```tsx
'use client'
import { useCreateUser } from 'infrastructure/pet-project-api'
export function CreateUserForm() {
const { trigger, isMutating } = useCreateUser()
return (
<Form
onSubmit={(input) => trigger(input)}
disabled={isMutating}
/>
)
}
```
В компоненте — снова только хук. Логика инвалидации кеша зашита внутрь, потребитель её не дублирует.
## Передача config из компонента
Каждый хук принимает второй (или третий) параметр `config?: SWRConfiguration` — он пробрасывается в `useSWR`. Это даёт потребителю точечно настроить ревалидацию, `fallbackData`, `suspense` и т.п.:
```tsx
'use client'
import { usePostDetail } from 'infrastructure/pet-project-api'
export function PostView({ slug, initialPost }: Props) {
const { data: post } = usePostDetail(slug, { fallbackData: initialPost })
// ...
}
```
## Начальное состояние с сервера
Если данные пришли из серверного компонента (см. [Серверные компоненты](/docs/usage/data/rest/fetching/server)) — передаются в `fallbackData` через `config` хука:
```tsx
// page.tsx (server)
import { petProjectApi } from 'infrastructure/pet-project-api'
export default async function Page({ params }: { params: { slug: string } }) {
const initialPost = await petProjectApi.posts.get(params.slug)
return <PostView slug={params.slug} initialPost={initialPost} />
}
```
```tsx
// post-view.tsx ('use client')
import { usePostDetail } from 'infrastructure/pet-project-api'
export function PostView({ slug, initialPost }: Props) {
const { data: post } = usePostDetail(slug, { fallbackData: initialPost })
return <article>{post?.title}</article>
}
```
Для массового заполнения кеша на странице с несколькими хуками — используется `<SWRConfig fallback>` обёртка. Серверный компонент собирает данные и передаёт сериализованную карту ключей в провайдер; все вложенные хуки сразу видят кеш.
## Запрет прямых вызовов
```tsx
// Плохо — прямой fetch в обход клиента
useEffect(() => {
fetch('/api/users').then(...)
}, [])
// Плохо — клиент без SWR: нет кеша, нет дедупликации
useEffect(() => {
petProjectApi.user.list().then(setUsers)
}, [])
// Плохо — useSWR в компоненте: SWR должен быть в хуке модуля
const { data } = useSWR(
['pet-project-api', 'user', 'list'],
() => petProjectApi.user.list(),
)
// Хорошо — готовый хук модуля
const { data } = useUserList()
```
Если для нужной операции хука ещё нет — он добавляется в `hooks/` модуля API, не в компонент.

View File

@@ -0,0 +1,66 @@
---
title: Серверные компоненты
description: Получение REST-данных в серверных компонентах.
keywords: [server components, rsc, серверные компоненты, fetch, api, app router, прямой вызов]
---
# Серверные компоненты
Получение REST-данных в серверных компонентах.
## Правила
- **Прямой `await` метода клиента.** Никаких хуков, обёрток состояний, `useEffect` — серверный компонент не имеет жизненного цикла React-клиента.
- **Ошибки бросаются.** Не оборачивать `try/catch` без необходимости — Next.js поднимет ближайший `error.tsx`.
- **Параллельные запросы — через `Promise.all`.** Последовательный `await` за `await` блокирует рендер.
## Шаблон
```tsx
// src/app/(routes)/users/page.tsx
import { petProjectApi } from 'infrastructure/pet-project-api'
export default async function UsersPage() {
const users = await petProjectApi.user.list()
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
```
## Параллельные запросы
```tsx
export default async function DashboardPage() {
const [users, orders] = await Promise.all([
petProjectApi.user.list(),
petProjectApi.order.list(),
])
return <Dashboard users={users} orders={orders} />
}
```
## Передача данных в клиентский компонент
Серверный компонент получает данные и передаёт их пропсами в клиентский. На клиенте данные становятся начальным состоянием — при необходимости перезапрашиваются через SWR (см. [Клиентские компоненты](/docs/usage/data/rest/fetching/client)).
```tsx
// page.tsx (server)
import { petProjectApi } from 'infrastructure/pet-project-api'
import { UsersList } from 'widgets/users-list'
export default async function UsersPage() {
const initialUsers = await petProjectApi.user.list()
return <UsersList initialUsers={initialUsers} />
}
```
## Запрет прямого `fetch`
Серверный компонент тоже использует только клиент из `infrastructure/`. Прямой `fetch` в `page.tsx` или в server-action запрещён теми же правилами, что и на клиенте.

View File

@@ -1,10 +1,11 @@
--- ---
title: Файлы роутинга title: Файлы роутинга
description: "Что должно лежать в файлах роутинга, а что — в экранах."
--- ---
# Файлы роутинга # Файлы роутинга
Правила для специальных файлов App Router (`page.tsx`, `layout.tsx`, `error.tsx`, `not-found.tsx` и др.) — чем наш подход отличается от дефолтного. Что должно лежать в файлах роутинга, а что — в экранах.
## Организация ## Организация

View File

@@ -1,10 +1,11 @@
--- ---
title: Структура проекта title: Структура проекта
description: Из чего состоит проект и где что лежит.
--- ---
# Структура проекта # Структура проекта
Раздел описывает расположение файлов и папок в проекте Next.js (App Router). Из чего состоит проект и где что лежит.
## Корень репозитория ## Корень репозитория

View File

@@ -1,10 +1,11 @@
--- ---
title: Стили title: Стили
description: Как пишутся стили в проекте.
--- ---
# Стили # Стили
Раздел описывает правила написания CSS: PostCSS Modules, вложенность, медиа-запросы, переменные, форматирование. Как пишутся стили в проекте.
## Общие правила ## Общие правила

View File

@@ -0,0 +1,56 @@
---
title: SVG-спрайты
description: Как добавлять и использовать SVG-иконки в коде.
keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превью, preview, цвет, color]
---
# SVG-спрайты
Как добавлять и использовать SVG-иконки в коде.
## Шаги
1. **Положить SVG в папку спрайта:**
```text
src/shared/sprites/icons/new-icon.svg
```
2. **Импортировать компонент.** Компонент `<SvgSprite/>` генерируется пакетом вместе с типами имён иконок — автодополнение работает без ручных описаний:
```tsx
import { SvgSprite } from 'ui/svg-sprite'
<SvgSprite icon="new-icon" />
```
3. **Посмотреть и пощупать иконку — в превью.** Пакет генерирует HTML-превью рядом со спрайтом (`public/sprites/icons.preview.html`). Там виден набор иконок, имена и поведение цвета.
## Управление цветом
При сборке цвета в SVG заменяются на CSS-переменные `--icon-color-N`. Управление — через обычный CSS родителя.
**Моно-иконка** наследует `color` родителя (`--icon-color-1` по умолчанию `currentColor`):
```css
.button {
color: var(--color-primary);
}
```
**Точечное переопределение** — через переменную:
```css
.icon-danger {
--icon-color-1: var(--color-danger);
}
```
**Мульти-иконка** — переменные задаются явно, порядок виден в превью:
```css
.folder {
--icon-color-1: var(--color-folder-bg);
--icon-color-2: var(--color-folder-accent);
}
```

View File

@@ -1,5 +1,6 @@
--- ---
title: Шаблоны и генерация кода title: Шаблоны и генерация кода
description: Как устроены шаблоны кодогенерации и как ими пользоваться.
--- ---
<!-- @formatter:off --> <!-- @formatter:off -->
@@ -7,7 +8,7 @@ title: Шаблоны и генерация кода
# Шаблоны и генерация кода # Шаблоны и генерация кода
Как работают шаблоны, как их создавать, синтаксис переменных и как генерировать код с помощью расширения VS Code и CLI. Как устроены шаблоны кодогенерации и как ими пользоваться.
## Структура шаблонов ## Структура шаблонов

8
docs/docs/workflow.md Normal file
View File

@@ -0,0 +1,8 @@
---
title: Подсказки
description: Короткие ответы на типовые вопросы и решения для спорных ситуаций.
---
# Подсказки
Короткие ответы на типовые вопросы и решения для спорных ситуаций.

View File

@@ -1,5 +0,0 @@
---
title: API
---
# API

View File

@@ -1,7 +0,0 @@
---
title: Components
---
# Components
Rules for creating UI components across all FSD layers.

View File

@@ -1,5 +0,0 @@
---
title: Fonts
---
# Fonts

View File

@@ -1,5 +0,0 @@
---
title: Hooks
---
# Hooks

View File

@@ -1,5 +0,0 @@
---
title: Images
---
# Images

View File

@@ -1,5 +0,0 @@
---
title: Localization
---
# Localization

View File

@@ -1,7 +0,0 @@
---
title: Page-level Components
---
# Page-level Components
Next.js App Router special files used by the framework by convention: `layout.tsx`, `page.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, `template.tsx`.

View File

@@ -1,7 +0,0 @@
---
title: Project Structure
---
# Project Structure
Base project structure and principles of module organization at folder and file level.

View File

@@ -1,5 +0,0 @@
---
title: Stores
---
# Stores

View File

@@ -1,7 +0,0 @@
---
title: Styles
---
# Styles
CSS writing rules: PostCSS Modules, nesting, media queries, variables, formatting.

View File

@@ -1,5 +0,0 @@
---
title: SVG Sprites
---
# SVG Sprites

View File

@@ -1,7 +0,0 @@
---
title: Templates & Code Generation
---
# Templates & Code Generation
Template tools, syntax, and examples for code generation.

View File

@@ -1,5 +0,0 @@
---
title: Video
---
# Video

View File

@@ -1,7 +0,0 @@
---
title: Architecture
---
# Architecture
Architecture based on FSD (Feature-Sliced Design) and strict module boundaries.

View File

@@ -1,7 +0,0 @@
---
title: Code Style
---
# Code Style
Unified code formatting rules: indentation, line breaks, quotes, import order, and readability.

View File

@@ -1,7 +0,0 @@
---
title: Documentation
---
# Documentation
Documentation should help understand the purpose of an entity, not duplicate its types or obvious details.

View File

@@ -1,7 +0,0 @@
---
title: Naming
---
# Naming
Naming should be predictable, concise, and reflect the meaning of the entity.

View File

@@ -1,7 +0,0 @@
---
title: Tech Stack
---
# Tech Stack
Base technology stack and libraries used in projects.

View File

@@ -1,7 +0,0 @@
---
title: Typing
---
# Typing
Typing is required for all public interfaces, functions, and components.

View File

@@ -1,58 +0,0 @@
# NextJS Style Guide
Rules and standards for NextJS and TypeScript development: architecture, typing, styles, components, API, and infrastructure.
## Documentation Structure
### Processes
**What to do** in a specific situation — step-by-step instructions.
| Section | Answers the question |
|---------|---------------------|
| Getting Started | What tools to install before starting development? |
| Creating an App | How to create a new project, where to get a template? |
| Creating Pages | How to add a page: routing and screen? |
| Creating Components | How to generate components using templates? |
| Styling | What to use: Mantine, tokens, or PostCSS? |
| Data Fetching | How to fetch data: SWR, codegen, sockets? |
| State Management | When and how to create a store (Zustand)? |
| Localization | How to add translations and work with i18next? |
### Basic Rules
**What the code should look like** — standards not tied to a specific technology.
| Section | Answers the question |
|---------|---------------------|
| Tech Stack | What stack do we use? |
| Architecture | How are FSD layers, dependencies, and public API structured? |
| Code Style | How to format code: indentation, quotes, imports, early return? |
| Naming | How to name files, variables, components, hooks? |
| Documentation | How to write JSDoc: what to document and what not? |
| Typing | How to type: type vs interface, any/unknown? |
### Applied Sections
**How a specific area works** — rules, structure, and code examples for specific technologies and tools.
| Section | Answers the question |
|---------|---------------------|
| Project Structure | How are folders and files organized by FSD? |
| Components | How is a component structured: files, props, clsx? |
| Page-level Components | How to define layout, page, loading, error, not-found? |
| Templates & Code Generation | How do templates work: syntax, variables, modifiers? |
| Styles | How to write CSS: PostCSS Modules, nesting, media, tokens? |
| Images | _(not filled)_ |
| SVG Sprites | _(not filled)_ |
| Video | _(not filled)_ |
| API | _(not filled)_ |
| Stores | _(not filled)_ |
| Hooks | _(not filled)_ |
| Fonts | _(not filled)_ |
| Localization | _(not filled)_ |
## For Assistants
Documentation map with links to all sections ([llmstxt.org](https://llmstxt.org) format):
https://gromlab.ru/docs/nextjs-style-guide/raw/branch/main/generated/en/llms.txt

View File

@@ -1,7 +0,0 @@
---
title: Creating an App
---
# Creating an App
How to create a new application: choosing a project template and initialization.

View File

@@ -1,7 +0,0 @@
---
title: Creating Components
---
# Creating Components
Generating components using templates, working with child components.

View File

@@ -1,7 +0,0 @@
---
title: Creating Pages
---
# Creating Pages
Page creation pattern: routing (page.tsx) and screen.

View File

@@ -1,7 +0,0 @@
---
title: Data Fetching
---
# Data Fetching
How to fetch data: SWR, API client codegen, sockets.

View File

@@ -1,7 +0,0 @@
---
title: Getting Started
---
# Getting Started
Setting up the environment and installing tools before starting development.

View File

@@ -1,7 +0,0 @@
---
title: Localization
---
# Localization
How to add translations and work with i18next.

View File

@@ -1,7 +0,0 @@
---
title: State Management
---
# State Management
When and how to create a store (Zustand), what to store locally vs globally.

View File

@@ -1,7 +0,0 @@
---
title: Styling
---
# Styling
Styling tools priority and rules for their application.

View File

@@ -3,96 +3,21 @@ layout: false
--- ---
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, onMounted } from 'vue'
const STORAGE_KEY = 'nsg-landing-lang'
const THEME_KEY = 'vitepress-theme-appearance' const THEME_KEY = 'vitepress-theme-appearance'
// __BUILD_VERSION__ подставляется Vite-define из ENV `BUILD_VERSION` // __BUILD_VERSION__ подставляется Vite-define из ENV `BUILD_VERSION`
// (см. .vitepress/config.ts). В dev и build всегда определена. // (см. .vitepress/config.ts). В dev и build всегда определена.
const buildVersion = __BUILD_VERSION__ const buildVersion = __BUILD_VERSION__
const dict = {
ru: {
tagline: 'Готовые соглашения по архитектуре, коду, компонентам и инфраструктуре для Next.js + TypeScript-проектов — чтобы команда писала одинаково, а новые разработчики включались в проект быстрее.',
langLabel: 'Язык',
themeLabel: 'Тема',
themes: { auto: 'Авто', light: 'Светлая', dark: 'Тёмная' },
cards: {
docs: {
title: 'Документация',
desc: 'Все разделы: процессы разработки, базовые правила, прикладные руководства.',
href: './ru/',
cta: 'Открыть',
},
ai: {
title: 'Ассистенту',
desc: 'Карта документации в формате llms.txt для AI-агентов.',
href: './ru/llms.txt',
cta: 'Открыть',
},
zip: {
title: 'Скачать правила',
desc: 'Архив всех Markdown-файлов одним ZIP.',
href: './nextjs-style-guide-ru.zip',
cta: 'Скачать',
},
},
},
en: {
tagline: 'Ready-made standards for architecture, code, components, and infrastructure in Next.js + TypeScript projects — so your team writes consistently and new developers ramp up faster.',
langLabel: 'Language',
themeLabel: 'Theme',
themes: { auto: 'Auto', light: 'Light', dark: 'Dark' },
cards: {
docs: {
title: 'Documentation',
desc: 'All sections: development processes, basic rules, applied guides.',
href: '#',
cta: 'Open',
badge: 'in development',
},
ai: {
title: 'For Assistant',
desc: 'Documentation map in llms.txt format for AI agents.',
href: '#',
cta: 'Open',
badge: 'in development',
},
zip: {
title: 'Download rules',
desc: 'Archive of all Markdown files and llms.txt in a single ZIP.',
href: '#',
cta: 'Download',
badge: 'soon',
},
},
},
}
const lang = ref('ru')
const theme = ref('auto') const theme = ref('auto')
onMounted(() => { onMounted(() => {
const savedLang = localStorage.getItem(STORAGE_KEY)
if (savedLang === 'ru' || savedLang === 'en') {
lang.value = savedLang
} else {
const nav = (navigator.language || 'ru').toLowerCase()
lang.value = nav.startsWith('ru') ? 'ru' : 'en'
}
const savedTheme = localStorage.getItem(THEME_KEY) const savedTheme = localStorage.getItem(THEME_KEY)
theme.value = savedTheme === 'dark' || savedTheme === 'light' ? savedTheme : 'auto' theme.value = savedTheme === 'dark' || savedTheme === 'light' ? savedTheme : 'auto'
}) })
const t = computed(() => dict[lang.value])
function setLang(value) {
lang.value = value
localStorage.setItem(STORAGE_KEY, value)
}
function setTheme(value) { function setTheme(value) {
theme.value = value theme.value = value
if (value === 'auto') { if (value === 'auto') {
@@ -118,31 +43,26 @@ function toggleTheme(value) {
<section class="landing__hero"> <section class="landing__hero">
<h1 class="landing__title">NextJS Style Guide</h1> <h1 class="landing__title">NextJS Style Guide</h1>
<ClientOnly> <ClientOnly>
<p class="landing__tagline">{{ t.tagline }}</p> <p class="landing__tagline">Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.</p>
<div class="landing__controls"> <div class="landing__controls">
<div class="seg" role="group" :aria-label="t.langLabel"> <a
<button class="landing__repo"
type="button" href="https://gromlab.ru/docs/nextjs-style-guide"
class="seg__btn" target="_blank"
:class="{ 'seg__btn--active': lang === 'ru' }" rel="noopener"
:aria-pressed="lang === 'ru'" >
@click="setLang('ru')" <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
>Русский</button> <path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.12.83-.26.83-.57L9 21.07c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.08-.74.09-.73.09-.73 1.2.09 1.83 1.24 1.83 1.24 1.08 1.83 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.14-.3-.54-1.52.1-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.28-1.55 3.29-1.23 3.29-1.23.64 1.66.24 2.88.12 3.18a4.65 4.65 0 0 1 1.23 3.22c0 4.61-2.8 5.63-5.48 5.92.42.36.81 1.1.81 2.22l-.01 3.29c0 .31.2.69.82.57A12 12 0 0 0 12 .3Z"/>
<button </svg>
type="button" <span>Репозиторий</span>
class="seg__btn" </a>
:class="{ 'seg__btn--active': lang === 'en' }" <div class="seg seg--icons" role="group" aria-label="Тема">
:aria-pressed="lang === 'en'"
@click="setLang('en')"
>English</button>
</div>
<div class="seg seg--icons" role="group" :aria-label="t.themeLabel">
<button <button
type="button" type="button"
class="seg__btn" class="seg__btn"
:class="{ 'seg__btn--active': theme === 'light' }" :class="{ 'seg__btn--active': theme === 'light' }"
:aria-pressed="theme === 'light'" :aria-pressed="theme === 'light'"
:title="t.themes.light" title="Светлая"
@click="toggleTheme('light')" @click="toggleTheme('light')"
> >
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
@@ -152,7 +72,7 @@ function toggleTheme(value) {
class="seg__btn" class="seg__btn"
:class="{ 'seg__btn--active': theme === 'dark' }" :class="{ 'seg__btn--active': theme === 'dark' }"
:aria-pressed="theme === 'dark'" :aria-pressed="theme === 'dark'"
:title="t.themes.dark" title="Тёмная"
@click="toggleTheme('dark')" @click="toggleTheme('dark')"
> >
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
@@ -162,25 +82,29 @@ function toggleTheme(value) {
</ClientOnly> </ClientOnly>
</section> </section>
<ClientOnly> <section class="landing__cards">
<section class="landing__cards"> <a class="landing__card" href="/docs/">
<a <h3>Документация</h3>
v-for="key in ['docs', 'ai', 'zip']" <p>Все разделы: процессы разработки, базовые правила, прикладные руководства.</p>
:key="key" <span class="landing__cta">Открыть →</span>
class="landing__card" </a>
:class="{ 'landing__card--soon': t.cards[key].badge }" <div class="landing__card landing__card--multi">
:href="t.cards[key].href" <h3>Ассистенту</h3>
:aria-disabled="t.cards[key].badge ? 'true' : null" <p>
> Карта документации для AI-агентов:
<h3> <code>/llms.txt</code>, <code>/llms-full.txt</code>.
{{ t.cards[key].title }} </p>
<span v-if="t.cards[key].badge" class="landing__badge">{{ t.cards[key].badge }}</span> <div class="landing__buttons">
</h3> <a class="landing__button" href="/llms.txt">llms.txt</a>
<p>{{ t.cards[key].desc }}</p> <a class="landing__button" href="/llms-full.txt">llms-full.txt</a>
<span class="landing__cta">{{ t.cards[key].cta }} →</span> </div>
</a> </div>
</section> <a class="landing__card" href="/nextjs-style-guide.zip">
</ClientOnly> <h3>Скачать правила</h3>
<p>Архив всех Markdown-файлов одним ZIP.</p>
<span class="landing__cta">Скачать →</span>
</a>
</section>
<p class="landing__version">v{{ buildVersion }}</p> <p class="landing__version">v{{ buildVersion }}</p>
</div> </div>
@@ -207,6 +131,35 @@ function toggleTheme(value) {
flex-wrap: wrap; flex-wrap: wrap;
} }
.landing__controls > * {
height: 36px;
box-sizing: border-box;
}
.landing__repo {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 999px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: color 0.15s, border-color 0.15s;
}
.landing__repo:hover {
color: var(--vp-c-text-1);
border-color: var(--vp-c-brand-1);
}
.landing__repo svg {
flex-shrink: 0;
}
.seg { .seg {
display: inline-flex; display: inline-flex;
align-items: stretch; align-items: stretch;
@@ -305,12 +258,6 @@ function toggleTheme(value) {
transform: translateY(-2px); transform: translateY(-2px);
} }
.landing__card--soon {
opacity: 0.55;
cursor: not-allowed;
pointer-events: none;
}
.landing__card h3 { .landing__card h3 {
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
@@ -332,6 +279,33 @@ function toggleTheme(value) {
color: var(--vp-c-brand-1); color: var(--vp-c-brand-1);
} }
.landing__buttons {
margin-top: auto;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.landing__button {
display: inline-flex;
align-items: center;
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
font-family: var(--vp-font-family-mono, monospace);
color: var(--vp-c-text-1);
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
text-decoration: none;
transition: border-color 0.15s, color 0.15s;
}
.landing__button:hover {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.landing__version { .landing__version {
text-align: center; text-align: center;
margin: 24px 0 0; margin: 24px 0 0;
@@ -340,33 +314,58 @@ function toggleTheme(value) {
font-family: var(--vp-font-family-mono, monospace); font-family: var(--vp-font-family-mono, monospace);
} }
.landing__badge {
display: inline-block;
margin-left: 8px;
padding: 2px 8px;
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
border-radius: 999px;
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-3);
vertical-align: middle;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.landing { .landing {
padding: 16px 16px 48px; padding: 48px 20px 56px;
gap: 32px; gap: 40px;
justify-content: flex-start;
} }
.landing__title { .landing__title {
font-size: 36px; font-size: 36px;
} }
.landing__tagline {
font-size: 16px;
}
.landing__cards { .landing__cards {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 16px;
} }
.landing__controls { .landing__controls {
gap: 8px; gap: 8px;
margin-top: 36px;
}
}
@media (max-width: 480px) {
.landing {
padding: 44px 16px 48px;
gap: 36px;
}
.landing__title {
font-size: 30px;
}
.landing__tagline {
font-size: 15px;
line-height: 1.5;
}
.landing__controls {
margin-top: 32px;
}
/* Репозиторий — только иконка, без текста, чтобы все контролы влезли в строку */
.landing__repo {
width: 36px;
padding: 0;
justify-content: center;
}
.landing__repo span {
display: none;
}
.seg__btn {
padding: 6px 12px;
font-size: 12px;
}
.landing__card {
padding: 20px;
} }
} }
</style> </style>

View File

View File

@@ -1,5 +0,0 @@
---
title: SVG-спрайты
---
# SVG-спрайты

View File

@@ -1,61 +0,0 @@
# NextJS Style Guide
Правила и стандарты разработки на NextJS и TypeScript: архитектура, типизация, стили, компоненты, API и инфраструктурные разделы.
## Для ассистентов
Карта документации со ссылками на все разделы (формат [llmstxt.org](https://llmstxt.org)):
https://gromlab.ru/docs/nextjs-style-guide/raw/branch/main/generated/ru/llms.txt
## Структура документации
### Workflow
**Что делать и в каком порядке** — пошаговые инструкции.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Начало работы | Что нужно знать перед началом разработки? |
| Создание проекта | Как начать новый проект? |
| Генерация кода | Какие модули должны генерироваться из шаблонов? |
| Добавление страницы | Как добавить новую страницу в проект? |
| Добавление UI-модуля | Как создать компонент, бизнес-модуль, виджет или layout? |
| Стилизация | Как стилизовать компоненты в проекте? |
| Получение данных | Как получать данные с сервера? |
| Управление состоянием | Как работать с состоянием? |
| Локализация | Как добавлять переводы и подключать локализацию? |
### Базовые правила
**Каким должен быть код** — стандарты, не привязанные к конкретной технологии.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Технологии и библиотеки | Какой стек используем? |
| Архитектура | Как устроены слои SLM, зависимости, публичный API? |
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
| Именование | Как называть файлы, переменные, компоненты, хуки? |
| Документирование | Как писать JSDoc: что документировать, а что нет? |
| Типизация | Как типизировать: type vs interface, any/unknown? |
### Прикладные разделы
**Как это настроить и использовать** — конфигурация, структура и примеры кода для конкретных областей.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Настройка VS Code | Как настроить редактор для проекта? |
| Структура проекта | Как организованы папки и файлы по SLM? |
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
| Изображения | _(не заполнен)_ |
| SVG-спрайты | _(не заполнен)_ |
| Видео | _(не заполнен)_ |
| API | _(не заполнен)_ |
| Stores | _(не заполнен)_ |
| Хуки | _(не заполнен)_ |
| Шрифты | _(не заполнен)_ |
| Локализация | _(не заполнен)_ |

View File

@@ -1,86 +0,0 @@
---
title: Workflow
---
# Workflow
Порядок действий при разработке — от создания проекта до реализации фич.
## Создание проекта
Инициализация нового проекта из готового шаблона.
1. Создать проект из шаблона:
```bash
npx tiged git@gromlab.ru:templates/nextjs.git my-app
cd my-app
npm install
```
2. Проект готов к разработке — стек, структура SLM, конфигурация
редактора и шаблоны генерации уже настроены.
## Генерация кода
Создание модулей из шаблонов `.templates/` вместо ручного создания файлов.
1. Определить тип модуля и соответствующий шаблон:
| Модуль | Слой | Шаблон |
|------------|--------------|-------------|
| Компонент | `ui/` | `component` |
| Бизнес-модуль | `business/` | `module` |
| Виджет | `widgets/` | `widget` |
| Layout | `layouts/` | `layout` |
| Экран | `screens/` | `screen` |
| Стор | `stores/` | `store` |
2. Сгенерировать модуль из шаблона.
3. Если подходящего шаблона нет — сначала создать шаблон, затем использовать.
Ручное создание файловой структуры модулей запрещено.
## Добавление страницы
Создание нового маршрута: экран + точка входа для роутинга.
1. Сгенерировать экран из шаблона `screen` в `src/screens/`.
2. Заполнить экран логикой и стилями.
3. Создать `page.tsx` в нужном маршруте `src/app/`.
`page.tsx` — тонкая обёртка: только `metadata` и рендер экрана.
Логика, стили и хуки размещаются в экране, не в `page.tsx`.
## Добавление UI-модуля
Создание компонента, бизнес-модуля, виджета или layout.
1. Сгенерировать модуль из соответствующего шаблона в целевой слой.
2. Заполнить модуль логикой и стилями.
3. Дочерние компоненты — генерировать из шаблона `component` в папку `ui/`
внутри родителя.
Дочерние компоненты не экспортируются через `index.ts` родителя.
## Стилизация
Выбор инструмента стилизации по приоритету.
1. Использовать Mantine-компоненты и их пропсы.
2. Если Mantine не покрывает — использовать CSS-токены
(`--color-*`, `--space-*`, `--radius-*`).
3. Если нужна кастомная стилизация — PostCSS Modules.
Инлайн-стили (`style`), магические значения и глобальные стили
вне `app/styles/` запрещены.
## Получение данных
*Раздел в разработке* — SWR, генерация API-клиентов, сокеты.
## Управление состоянием
*Раздел в разработке* — когда создавать стор, что хранить локально и глобально.
## Локализация
*Раздел в разработке* — переводы и i18next.

View File

@@ -1,31 +0,0 @@
---
title: Генерация кода
---
# Генерация кода
Как создавать модули в проекте с помощью шаблонов — какие модули покрыты генерацией и когда стоит создавать новые шаблоны.
## Какие модули генерируются из шаблонов
| Модуль | Слой | Шаблон |
|---|---|---|
| Компонент | `ui/` | `component` |
| Бизнес-модуль | `business/` | `module` |
| Виджет | `widgets/` | `widget` |
| Layout | `layouts/` | `layout` |
| Экран | `screens/` | `screen` |
| Стор | `stores/` | `store` |
## Что нужно знать
В проекте принято создавать модули из шаблонов `.templates/`. Шаблоны задают единообразную файловую структуру и сокращают рутину — не нужно вручную создавать папки, файлы типов, стилей и экспорты.
Если для нужного модуля нет подходящего шаблона — стоит сначала создать шаблон, а затем использовать его.
## Когда создавать новый шаблон
- Повторяющаяся структура появляется больше одного раза.
- Существующий шаблон не покрывает нужный тип модуля.
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/ru/applied/templates-generation).

View File

@@ -1,31 +0,0 @@
---
title: Создание проекта
---
# Создание проекта
Как начать новый проект, соответствующий стандартам этого руководства.
## Что нужно знать
Новый проект создаётся из готового шаблона. Шаблон содержит настроенный стек, структуру SLM, конфигурацию редактора и шаблоны генерации кода — проект готов к разработке сразу после установки зависимостей.
### Создание из шаблона
```bash
npx tiged git@gromlab.ru:templates/nextjs.git my-app
cd my-app
npm install
```
## Что входит в шаблон
- Next.js + TypeScript (App Router)
- Mantine UI + PostCSS Modules
- Biome (линтинг и форматирование)
- Zustand, SWR
- Структура SLM (`layouts/`, `screens/`, `widgets/`, `business/`, `infrastructure/`, `ui/`, `shared/`)
- Шаблоны генерации (`.templates/`)
- Конфигурация VS Code (`.vscode/`)
- CSS-токены (цвета, отступы, радиусы, медиа)
- Open Graph метаданные

View File

@@ -1,22 +0,0 @@
---
title: Добавление UI-модуля
---
# Добавление UI-модуля
Как создать компонент, бизнес-модуль, виджет или layout в проекте.
## Что нужно знать
Все UI-модули создаются только из шаблонов `.templates/`. Ручное создание файловой структуры запрещено. Если подходящего шаблона нет — сначала создать шаблон в `.templates/`, затем использовать его.
## Порядок действий
1. [Сгенерировать](/ru/applied/templates-generation) модуль из соответствующего шаблона в целевой слой.
2. Заполнить модуль логикой и стилями.
## Дочерние компоненты
Если модулю нужны внутренние подкомпоненты — [генерировать](/ru/applied/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя.
Правила написания компонентов — [Компоненты](/ru/applied/components).

View File

@@ -1,27 +0,0 @@
---
title: Добавление страницы
---
# Добавление страницы
Как добавить новую страницу в проект по стандартам этого руководства.
## Что нужно знать
Страница в проекте — это два файла: экран в `src/screens/` (вся логика, стили, зависимости) и `page.tsx` в `src/app/` (точка входа для роутинга Next.js). Экран генерируется из шаблона, `page.tsx` создаётся вручную.
## Порядок действий
1. [Сгенерировать](/ru/applied/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
2. Заполнить экран логикой и стилями.
3. Создать `page.tsx` в нужном маршруте `src/app/`. Файл страницы должен быть тонким — только `metadata` и рендер экрана. Никакой логики, стилей и хуков в `page.tsx` не размещается — всё это живёт в экране.
## Правила
- Ручное создание файловой структуры экрана запрещено — только [генерация](/ru/applied/templates-generation) из шаблона.
- Логика, стили и зависимости размещаются в экране, не в `page.tsx`.
- Каждая страница содержит `metadata` с `title` и `description`.
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/ru/applied/page-level).

View File

@@ -1,7 +0,0 @@
---
title: Получение данных
---
# Получение данных
Как получать данные с сервера — SWR, генерация API-клиентов, сокеты.

View File

@@ -1,22 +0,0 @@
---
title: Начало работы
---
# Начало работы
Что нужно знать перед началом разработки в проекте.
## Стек проекта
**Next.js** (App Router), **Mantine**, **Zustand**, **SLM Design**.
Подробнее — [Технологии и библиотеки](/ru/basics/tech-stack).
## Ключевые особенности
- **Генерация вместо ручного создания** — компоненты, бизнес-модули, виджеты, сторы и другие модули не создаются вручную. Файловая структура генерируется из шаблонов `.templates/`. Ручное создание файловой структуры модулей запрещено.
- **Biome вместо ESLint + Prettier** — один инструмент для линтинга и форматирования. Автофикс и сортировка импортов происходят автоматически при сохранении файла.
## Настройка окружения
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/ru/applied/vscode).

View File

@@ -1,7 +0,0 @@
---
title: Локализация
---
# Локализация
Как добавлять переводы и подключать локализацию через i18next.

View File

@@ -1,7 +0,0 @@
---
title: Управление состоянием
---
# Управление состоянием
Как работать с состоянием — когда создавать стор, что хранить локально и глобально.

View File

@@ -1,23 +0,0 @@
---
title: Стилизация
---
# Стилизация
Как стилизовать компоненты в проекте — приоритет инструментов и правила их применения.
## Приоритет стилизации
Основной UI-фреймворк проекта — **Mantine**. При стилизации компонентов придерживаться следующего приоритета:
1. **Mantine-компоненты и их пропсы** — в первую очередь использовать встроенные возможности Mantine (пропсы, `classNames`, `styles`).
2. **Глобальные CSS-токены** (`--color-*`, `--space-*`, `--radius-*`) — для значений, которые не покрываются Mantine.
3. **PostCSS Modules** — когда Mantine не покрывает задачу и нужна кастомная стилизация.
## Что запрещено
- **Инлайн-стили** — использование атрибута `style` в компонентах строго запрещено.
- **Магические значения** — произвольные цвета, отступы и скругления запрещены, использовать токены.
- **Глобальные стили** вне `app/styles/` запрещены.
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили](/ru/applied/styles).

View File

@@ -11,7 +11,11 @@ const BUILD_DATE = new Date().toISOString();
/** Корневая папка для генерируемой статики (попадает в build dist). */ /** Корневая папка для генерируемой статики (попадает в build dist). */
const PUBLIC_DIR = 'docs/public'; const PUBLIC_DIR = 'docs/public';
type Lang = 'ru' | 'en'; /** Префикс URL документации. Соответствует структуре `docs/docs/...`. */
const DOC_PREFIX = '/docs/';
/** Канонический хост сайта (для sitemap/robots). Можно переопределить через ENV. */
const SITE_URL = (process.env.SITE_URL || 'https://nextjs-style-guide.gromlab.ru').replace(/\/$/, '');
interface SidebarItem { interface SidebarItem {
text: string; text: string;
@@ -76,13 +80,13 @@ const firstParagraphAfterH1 = (body: string): string | null => {
}; };
/** /**
* Преобразовать sidebar `link` в относительный путь файла внутри * Преобразовать sidebar `link` (например `/docs/foo`) в относительный
* `docs/{lang}/`. Sidebar links содержат полный префикс локали * путь файла внутри `docs/docs/`. Префикс `/docs/` отрезается.
* (`/ru/...`, `/en/...`) — отрезаем его.
*/ */
const linkToRel = (link: string, lang: Lang): string => { const linkToRel = (link: string): string => {
const prefix = `/${lang}/`; let rel = link.startsWith(DOC_PREFIX)
let rel = link.startsWith(prefix) ? link.slice(prefix.length) : link.replace(/^\//, ''); ? link.slice(DOC_PREFIX.length)
: link.replace(/^\//, '');
if (rel === '' || rel.endsWith('/')) { if (rel === '' || rel.endsWith('/')) {
rel += 'index.md'; rel += 'index.md';
} else { } else {
@@ -91,52 +95,50 @@ const linkToRel = (link: string, lang: Lang): string => {
return rel; return rel;
}; };
const linkToFilePath = (link: string, lang: Lang): string => const linkToFilePath = (link: string): string =>
path.join('docs', lang, linkToRel(link, lang)); path.join('docs/docs', linkToRel(link));
/** /** Абсолютный URL `.md`-копии страницы на сайте. */
* Абсолютный путь от корня сайта к `.md`-копии страницы. const linkToSiteUrl = (link: string): string =>
* После build файлы лежат в `dist/{lang}/...md` (через `docs/public/`). `${DOC_PREFIX}${linkToRel(link)}`;
*/
const linkToSiteUrl = (link: string, lang: Lang): string =>
`/${lang}/${linkToRel(link, lang)}`;
/** /**
* Развернуть sidebar в плоский список с сохранением группы и * Развернуть sidebar в плоский список с сохранением группы и
* опционального префикса вложенной группы. * накопленного префикса вложенных групп. Поддерживает произвольную
* глубину вложенности — префиксы подгрупп склеиваются через `: `.
*/ */
const flattenSidebar = (sidebar: SidebarItem[]): Entry[] => { const flattenSidebar = (sidebar: SidebarItem[]): Entry[] => {
const entries: Entry[] = []; const entries: Entry[] = [];
for (const top of sidebar) { const walk = (
const section = top.text; items: SidebarItem[],
section: string,
prefix: string | null,
): void => {
for (const item of items) {
const hasChildren = !!item.items && item.items.length > 0;
if (top.link && !top.items) { if (item.link) {
entries.push({ section, prefix: null, text: top.text, link: top.link }); entries.push({ section, prefix, text: item.text, link: item.link });
}
if (hasChildren) {
const nextPrefix = prefix ? `${prefix}: ${item.text}` : item.text;
walk(item.items!, section, nextPrefix);
}
}
};
for (const top of sidebar) {
const hasChildren = !!top.items && top.items.length > 0;
if (top.link && !hasChildren) {
entries.push({ section: top.text, prefix: null, text: top.text, link: top.link });
continue; continue;
} }
if (!top.items) continue; if (hasChildren) {
walk(top.items!, top.text, null);
for (const item of top.items) {
if (item.items) {
for (const sub of item.items) {
if (!sub.link) continue;
entries.push({
section,
prefix: item.text,
text: sub.text,
link: sub.link,
});
}
} else if (item.link) {
entries.push({
section,
prefix: null,
text: item.text,
link: item.link,
});
}
} }
} }
@@ -153,34 +155,20 @@ const groupBySection = (entries: Entry[]): Map<string, Entry[]> => {
return map; return map;
}; };
const buildLlms = (lang: Lang): void => { interface SiteConfig {
const localeKey = lang; title: string;
// VitePress-конфиг типизирован как `UserConfig`, но обращаемся к description: string;
// фактически переданным значениям — сужаем тип через any. themeConfig: { sidebar: SidebarItem[] };
const cfg = config as unknown as { llmsBlockquote?: string;
title: string; llmsContext?: string;
description: string; }
locales: Record<
string,
{
description?: string;
llmsBlockquote?: string;
llmsContext?: string;
themeConfig?: { sidebar?: SidebarItem[] };
}
>;
};
const locale = cfg.locales[localeKey]; const cfg = config as unknown as SiteConfig;
const sidebar = locale?.themeConfig?.sidebar;
if (!sidebar) { const buildLlms = (): void => {
console.warn(`[${lang}] sidebar не найден в config`); const sidebar = cfg.themeConfig.sidebar;
return; const blockquote = cfg.llmsBlockquote ?? cfg.description;
} const context = cfg.llmsContext;
// Для blockquote предпочитаем расширенный llms-текст; короткий
// description — fallback и используется для HTML meta-тега VitePress.
const blockquote = locale.llmsBlockquote ?? locale.description ?? cfg.description;
const context = locale.llmsContext;
const entries = flattenSidebar(sidebar); const entries = flattenSidebar(sidebar);
const grouped = groupBySection(entries); const grouped = groupBySection(entries);
@@ -200,19 +188,16 @@ const buildLlms = (lang: Lang): void => {
lines.push(''); lines.push('');
for (const entry of items) { for (const entry of items) {
const filePath = linkToFilePath(entry.link, lang); const filePath = linkToFilePath(entry.link);
const url = linkToSiteUrl(entry.link, lang); const url = linkToSiteUrl(entry.link);
// Текст ссылки берём из sidebar — он специально написан для навигации
// и точнее отражает иерархию (например "Обзор" внутри группы "Архитектура").
let description: string | null = null; let description: string | null = null;
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const raw = fs.readFileSync(filePath, 'utf8'); const raw = fs.readFileSync(filePath, 'utf8');
const { data, body } = parseFrontmatter(raw); const { data, body } = parseFrontmatter(raw);
description = data.description || firstParagraphAfterH1(body); description = data.description || firstParagraphAfterH1(body);
} else { } else {
console.warn(`[${lang}] файл не найден: ${filePath}`); console.warn(`файл не найден: ${filePath}`);
} }
const display = entry.prefix const display = entry.prefix
@@ -225,40 +210,6 @@ const buildLlms = (lang: Lang): void => {
lines.push(''); lines.push('');
} }
const outDir = path.join(PUBLIC_DIR, lang);
fs.mkdirSync(outDir, { recursive: true });
const outFile = path.join(outDir, 'llms.txt');
fs.writeFileSync(outFile, lines.join('\n'), 'utf8');
console.log(`${outFile} создан`);
};
/**
* Корневой `/llms.txt` — роутер. По стандарту llmstxt.org это
* единственный файл в корне сайта; для двуязычного проекта он
* указывает LLM на локализованные карты документации.
*/
const buildRootIndex = (): void => {
const cfg = config as unknown as {
title: string;
description: string;
locales: Record<string, { description?: string }>;
};
const ruDesc = cfg.locales.ru?.description ?? cfg.description;
const enDesc = cfg.locales.en?.description ?? cfg.description;
const lines: string[] = [
`# ${cfg.title}`,
'',
`> ${enDesc}.`,
'',
'## Documentation',
'',
`- [Русская версия (Russian)](/ru/llms.txt): ${ruDesc}.`,
'- English version: in development',
'',
];
fs.mkdirSync(PUBLIC_DIR, { recursive: true }); fs.mkdirSync(PUBLIC_DIR, { recursive: true });
const outFile = path.join(PUBLIC_DIR, 'llms.txt'); const outFile = path.join(PUBLIC_DIR, 'llms.txt');
fs.writeFileSync(outFile, lines.join('\n'), 'utf8'); fs.writeFileSync(outFile, lines.join('\n'), 'utf8');
@@ -288,41 +239,169 @@ const copyDirSync = (
}; };
/** /**
* Скопировать все `.md`-файлы локали в `docs/public/{lang}/`, * Скопировать все `.md`-файлы документации в `docs/public/docs/`,
* чтобы они попали в build `dist/` и были доступны по URL `/lang/path.md`. * чтобы они попали в build `dist/` и были доступны по URL `/docs/path.md`.
*/ */
const copyMdFiles = (lang: Lang): void => { const copyMdFiles = (): void => {
const srcDir = path.join('docs', lang); const srcDir = 'docs/docs';
const destDir = path.join(PUBLIC_DIR, lang); const destDir = path.join(PUBLIC_DIR, 'docs');
if (!fs.existsSync(srcDir)) return; if (!fs.existsSync(srcDir)) return;
const copied = copyDirSync(srcDir, destDir, (name) => name.endsWith('.md')); const copied = copyDirSync(srcDir, destDir, (name) => name.endsWith('.md'));
console.log(`[${lang}] скопировано ${copied} .md-файлов в ${destDir}`); console.log(`скопировано ${copied} .md-файлов в ${destDir}`);
}; };
/** /**
* Собрать `nextjs-style-guide-{lang}.zip` со всеми `.md` локали и `VERSION`. * Преобразовать sidebar `link` в относительный путь файла внутри архива
* Внутри архива — единая папка `nextjs-style-guide/`. * (от корня папки `nextjs-style-guide/`).
*
* `llms.txt` в архив не кладём: его ссылки указывают на сайт и локально
* не работают. Структура папки сама по себе является картой документации.
*/ */
const buildZip = (lang: Lang): void => { const linkToArchiveRel = (link: string): string => {
let rel = link.startsWith(DOC_PREFIX)
? link.slice(DOC_PREFIX.length)
: link.replace(/^\//, '');
if (rel === '' || rel.endsWith('/')) {
rel += 'index.md';
} else {
rel += '.md';
}
return rel;
};
/**
* Заменить во всех `.md` архива ссылки `[text](/docs/foo)` на относительные
* пути от расположения файла. Без этого внутренние ссылки в распакованной
* папке не работают.
*/
const transformLinksInDir = (rootDir: string): void => {
const linkRe = /\]\(\/docs\/([^)\s#]*)(#[^)]*)?\)/g;
const walk = (dir: string): void => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full);
continue;
}
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
const content = fs.readFileSync(full, 'utf8');
const fileDir = path.dirname(full);
const updated = content.replace(linkRe, (_match, route, hash = '') => {
const targetRel = linkToArchiveRel(`${DOC_PREFIX}${route}`);
const targetAbs = path.join(rootDir, targetRel);
let rel = path.relative(fileDir, targetAbs);
if (!rel.startsWith('.')) rel = './' + rel;
return `](${rel}${hash})`;
});
if (updated !== content) {
fs.writeFileSync(full, updated, 'utf8');
}
}
};
walk(rootDir);
};
/**
* Сгенерировать `README.md` — точка входа архива. Карта документации
* с относительными ссылками, описаниями из frontmatter/первого абзаца
* и метаинфо сборки.
*/
const buildArchiveReadme = (rootDir: string): void => {
const sidebar = cfg.themeConfig.sidebar;
const blockquote = cfg.llmsBlockquote ?? cfg.description ?? '';
const context = cfg.llmsContext;
const entries = flattenSidebar(sidebar).filter(
// «Главная» из sidebar — это страница раздела для веба, в архиве не нужна.
(e) => e.section !== 'Главная',
);
const grouped = groupBySection(entries);
const lines: string[] = [];
lines.push(`# ${cfg.title}`);
lines.push('');
if (blockquote) {
lines.push(`> ${blockquote}`);
lines.push('');
}
if (context) {
lines.push(context);
lines.push('');
}
lines.push('## Содержание');
lines.push('');
for (const [section, items] of grouped) {
lines.push(`### ${section}`);
lines.push('');
for (const entry of items) {
const targetRel = './' + linkToArchiveRel(entry.link);
const filePath = path.join(rootDir, linkToArchiveRel(entry.link));
let description: string | null = null;
if (fs.existsSync(filePath)) {
const raw = fs.readFileSync(filePath, 'utf8');
const { data, body } = parseFrontmatter(raw);
description = data.description || firstParagraphAfterH1(body);
}
const display = entry.prefix
? `${entry.prefix}: ${entry.text}`
: entry.text;
const descPart = description ? `${description}` : '';
lines.push(`- [${display}](${targetRel})${descPart}`);
}
lines.push('');
}
lines.push('---');
lines.push('');
lines.push(`Версия: ${VERSION} · Сборка: ${BUILD_DATE}`);
lines.push('');
fs.writeFileSync(path.join(rootDir, 'README.md'), lines.join('\n'), 'utf8');
};
/**
* Собрать `nextjs-style-guide.zip`. Внутри — папка `nextjs-style-guide/`
* с `.md`-файлами, README, `llms-full.txt` и `VERSION`. Внутренние ссылки
* преобразуются в относительные.
*/
const buildZip = (): void => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nsg-')); const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nsg-'));
const stage = path.join(tmpRoot, 'nextjs-style-guide'); const stage = path.join(tmpRoot, 'nextjs-style-guide');
fs.mkdirSync(stage, { recursive: true }); fs.mkdirSync(stage, { recursive: true });
copyDirSync(path.join('docs', lang), stage, (name) => name.endsWith('.md')); // 1. Копируем все .md в staging.
copyDirSync('docs/docs', stage, (name) => name.endsWith('.md'));
// 2. Удаляем веб-index.md — в архиве его роль выполняет README.md.
const indexPath = path.join(stage, 'index.md');
if (fs.existsSync(indexPath)) fs.unlinkSync(indexPath);
// 3. Преобразуем абсолютные ссылки `/docs/...` в относительные.
transformLinksInDir(stage);
// 4. Генерируем точку входа README.md.
buildArchiveReadme(stage);
// 5. Кладём llms-full.txt — удобно для одноразового чтения LLM.
const llmsFullSrc = path.join(PUBLIC_DIR, 'llms-full.txt');
if (fs.existsSync(llmsFullSrc)) {
fs.copyFileSync(llmsFullSrc, path.join(stage, 'llms-full.txt'));
}
// 6. Метаинформация сборки.
fs.writeFileSync( fs.writeFileSync(
path.join(stage, 'VERSION'), path.join(stage, 'VERSION'),
`${VERSION}\n${BUILD_DATE}\n`, `${VERSION}\n${BUILD_DATE}\n`,
); );
const outFile = path.resolve( const outFile = path.resolve(PUBLIC_DIR, 'nextjs-style-guide.zip');
PUBLIC_DIR,
`nextjs-style-guide-${lang}.zip`,
);
fs.rmSync(outFile, { force: true }); fs.rmSync(outFile, { force: true });
execFileSync('zip', ['-rq', outFile, 'nextjs-style-guide'], { execFileSync('zip', ['-rq', outFile, 'nextjs-style-guide'], {
@@ -333,21 +412,75 @@ const buildZip = (lang: Lang): void => {
console.log(`${outFile} создан (${VERSION})`); console.log(`${outFile} создан (${VERSION})`);
}; };
/** Удалить YAML frontmatter из исходника `.md`. */
const stripFrontmatter = (content: string): string =>
content.replace(/^---\n[\s\S]*?\n---\n*/, '');
/**
* Сдвинуть уровень заголовков на 1 вниз (h1→h2, h2→h3, ...).
* Игнорирует строки внутри блоков кода.
*/
const shiftHeadings = (content: string): string => {
const lines = content.split('\n');
let inCodeBlock = false;
return lines
.map((line) => {
if (line.startsWith('```')) inCodeBlock = !inCodeBlock;
if (inCodeBlock) return line;
if (/^#{1,5}\s/.test(line)) return '#' + line;
return line;
})
.join('\n');
};
/**
* Собрать `llms-full.txt` — все страницы в одном файле.
* Порядок страниц повторяет порядок в sidebar.
*/
const buildLlmsFull = (): void => {
const sidebar = cfg.themeConfig.sidebar;
const entries = flattenSidebar(sidebar);
const blockquote = cfg.llmsBlockquote ?? cfg.description ?? '';
const parts: string[] = [];
parts.push(`# ${cfg.title}`);
parts.push('');
if (blockquote) parts.push(`> ${blockquote}`);
if (cfg.llmsContext) {
parts.push('');
parts.push(cfg.llmsContext);
}
parts.push('');
for (const entry of entries) {
const filePath = linkToFilePath(entry.link);
if (!fs.existsSync(filePath)) continue;
const raw = fs.readFileSync(filePath, 'utf8');
const content = shiftHeadings(stripFrontmatter(raw)).trim();
if (!content) continue;
// Мета-якорь: путь страницы для ориентации LLM
parts.push(`<!-- ${entry.link} -->`);
parts.push('');
parts.push(content);
parts.push('');
}
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
const outFile = path.join(PUBLIC_DIR, 'llms-full.txt');
fs.writeFileSync(outFile, parts.join('\n'), 'utf8');
console.log(`${outFile} создан`);
};
/** Манифест сборки — для лендинга и внешних потребителей. */ /** Манифест сборки — для лендинга и внешних потребителей. */
const writeManifest = (): void => { const writeManifest = (): void => {
const manifest = { const manifest = {
version: VERSION, version: VERSION,
buildDate: BUILD_DATE, buildDate: BUILD_DATE,
languages: { llms: '/llms.txt',
ru: { llmsFull: '/llms-full.txt',
llms: '/ru/llms.txt', zip: '/nextjs-style-guide.zip',
zip: '/nextjs-style-guide-ru.zip',
},
en: {
llms: '/en/llms.txt',
zip: '/nextjs-style-guide-en.zip',
},
},
}; };
fs.mkdirSync(PUBLIC_DIR, { recursive: true }); fs.mkdirSync(PUBLIC_DIR, { recursive: true });
fs.writeFileSync( fs.writeFileSync(
@@ -358,26 +491,111 @@ const writeManifest = (): void => {
console.log(`${PUBLIC_DIR}/manifest.json создан`); console.log(`${PUBLIC_DIR}/manifest.json создан`);
}; };
/** Скопировать `index.md` локали в корневой README без frontmatter */ /**
const buildReadme = (lang: Lang, outFile: string): void => { * Сгенерировать `robots.txt` с указанием sitemap и явными ссылками
const indexPath = path.join('docs', lang, 'index.md'); * на llms.txt/llms-full.txt — стандартные файлы, которые читают агенты.
*/
const buildRobots = (): void => {
const lines = [
'User-agent: *',
'Allow: /',
'',
`Sitemap: ${SITE_URL}/sitemap.xml`,
'',
'# Карта документации для AI-агентов:',
`# ${SITE_URL}/llms.txt`,
`# ${SITE_URL}/llms-full.txt`,
'',
];
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
fs.writeFileSync(path.join(PUBLIC_DIR, 'robots.txt'), lines.join('\n'), 'utf8');
console.log(`${PUBLIC_DIR}/robots.txt создан`);
};
/**
* Сгенерировать `sitemap.xml` из sidebar + корневые ресурсы для LLM
* (llms.txt, llms-full.txt) — чтобы агенты, читающие sitemap, видели их.
*/
const buildSitemap = (): void => {
const sidebar = cfg.themeConfig.sidebar;
const entries = flattenSidebar(sidebar);
const urls = new Set<string>();
urls.add(`${SITE_URL}/`);
urls.add(`${SITE_URL}/llms.txt`);
urls.add(`${SITE_URL}/llms-full.txt`);
for (const entry of entries) {
const link = entry.link;
// cleanUrls: канон без `.html`. Index-страницы — каталог со слешем.
urls.add(`${SITE_URL}${link}`);
}
const today = BUILD_DATE.slice(0, 10);
const xml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
...[...urls].map(
(loc) => ` <url><loc>${loc}</loc><lastmod>${today}</lastmod></url>`,
),
'</urlset>',
'',
].join('\n');
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
fs.writeFileSync(path.join(PUBLIC_DIR, 'sitemap.xml'), xml, 'utf8');
console.log(`${PUBLIC_DIR}/sitemap.xml создан`);
};
/**
* Преобразовать абсолютные ссылки `index.md` в рабочие при открытии
* README в репозитории:
* - `/docs/foo` → относительный путь `docs/docs/foo.md`;
* - корневые ресурсы (`/llms.txt`, `/llms-full.txt`, `*.zip`, `/manifest.json`,
* `/sitemap.xml`, `/robots.txt`) — генерируемые, в репозитории отсутствуют,
* поэтому ссылки переписываются на абсолютный `SITE_URL`.
*/
const transformReadmeLinks = (content: string): string => {
const linkRe = /\]\((\/[^)\s]*)\)/g;
return content.replace(linkRe, (match, href: string) => {
const [pathPart, hash = ''] = href.split('#');
const hashPart = hash ? `#${hash}` : '';
if (pathPart.startsWith(DOC_PREFIX)) {
const rel = linkToArchiveRel(pathPart);
return `](docs/docs/${rel}${hashPart})`;
}
return `](${SITE_URL}${pathPart}${hashPart})`;
});
};
/** Скопировать `index.md` документации в корневой README без frontmatter. */
const buildReadme = (): void => {
const indexPath = 'docs/docs/index.md';
if (!fs.existsSync(indexPath)) { if (!fs.existsSync(indexPath)) {
console.warn(`Пропуск ${outFile}: ${indexPath} не найден`); console.warn(`Пропуск README.md: ${indexPath} не найден`);
return; return;
} }
const raw = fs.readFileSync(indexPath, 'utf8'); const raw = fs.readFileSync(indexPath, 'utf8');
const { body } = parseFrontmatter(raw); const { body } = parseFrontmatter(raw);
fs.writeFileSync(outFile, body.trimStart(), 'utf8'); const transformed = transformReadmeLinks(body.trimStart());
console.log(`${outFile} обновлён из ${indexPath}`);
// Порядок: H1 → описание (первый абзац) → ссылка на сайт.
const withSiteLink = transformed.replace(
/^(#\s[^\n]*\n\n[^\n]+\n)/,
`$1\nСайт: ${SITE_URL}\n`,
);
fs.writeFileSync('README.md', withSiteLink, 'utf8');
console.log(`README.md обновлён из ${indexPath}`);
}; };
buildLlms('ru'); buildLlms();
buildLlms('en'); buildLlmsFull();
buildRootIndex(); copyMdFiles();
copyMdFiles('ru'); buildZip();
copyMdFiles('en');
buildZip('ru');
buildZip('en');
writeManifest(); writeManifest();
buildReadme('en', 'README.md'); buildRobots();
buildReadme('ru', 'README_RU.md'); buildSitemap();
buildReadme();