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
This commit is contained in:
2026-04-26 15:04:10 +03:00
parent 90bf360c06
commit f645b2ad40
70 changed files with 263 additions and 901 deletions

View File

@@ -1,101 +1,51 @@
import { defineConfig } from 'vitepress';
const ruSidebar = [
const sidebar = [
{
text: 'Главная',
link: '/ru/',
link: '/docs/',
},
{
text: 'Workflow',
link: '/ru/workflow',
link: '/docs/workflow',
},
{
text: 'Базовые правила',
items: [
{ text: 'Технологии и библиотеки', link: '/ru/basics/tech-stack' },
{ text: 'Именование', link: '/ru/basics/naming' },
{ text: 'Технологии и библиотеки', link: '/docs/basics/tech-stack' },
{ text: 'Именование', link: '/docs/basics/naming' },
{
text: 'Архитектура',
collapsed: true,
items: [
{ text: 'Обзор', link: '/ru/basics/architecture/' },
{ text: 'Слои', link: '/ru/basics/architecture/reference/layers' },
{ text: 'Модули', link: '/ru/basics/architecture/reference/modules' },
{ text: 'Сегменты', link: '/ru/basics/architecture/reference/segments' },
{ text: 'Обзор', link: '/docs/basics/architecture/' },
{ text: 'Слои', link: '/docs/basics/architecture/reference/layers' },
{ text: 'Модули', link: '/docs/basics/architecture/reference/modules' },
{ text: 'Сегменты', link: '/docs/basics/architecture/reference/segments' },
],
},
{ text: 'Стиль кода', link: '/ru/basics/code-style' },
{ text: 'Документирование', link: '/ru/basics/documentation' },
{ text: 'Типизация', link: '/ru/basics/typing' },
{ text: 'Стиль кода', link: '/docs/basics/code-style' },
{ text: 'Документирование', link: '/docs/basics/documentation' },
{ text: 'Типизация', link: '/docs/basics/typing' },
],
},
{
text: 'Прикладные разделы',
items: [
{ text: 'Структура проекта', link: '/ru/applied/project-structure' },
{ text: 'Компоненты', link: '/ru/applied/components' },
{ text: 'Страницы (App Router)', link: '/ru/applied/page-level' },
{ 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: 'Home',
link: '/en/',
},
{
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',
items: [
{ text: 'Tech Stack', link: '/en/basics/tech-stack' },
{ text: 'Architecture', link: '/en/basics/architecture' },
{ text: 'Code Style', link: '/en/basics/code-style' },
{ text: 'Naming', link: '/en/basics/naming' },
{ text: 'Documentation', link: '/en/basics/documentation' },
{ text: 'Typing', link: '/en/basics/typing' },
],
},
{
text: 'Applied Sections',
items: [
{ text: 'VS Code Setup', link: '/en/applied/vscode' },
{ text: 'Project Structure', link: '/en/applied/project-structure' },
{ text: 'Components', link: '/en/applied/components' },
{ text: 'Page-level Components', link: '/en/applied/page-level' },
{ text: 'Templates & Code Generation', link: '/en/applied/templates-generation' },
{ text: 'Styles', link: '/en/applied/styles' },
{ text: 'Images', link: '/en/applied/images-sprites' },
{ text: 'SVG Sprites', link: '/en/applied/svg-sprites' },
{ text: 'Video', link: '/en/applied/video' },
{ text: 'API', link: '/en/applied/api' },
{ text: 'Stores', link: '/en/applied/stores' },
{ text: 'Hooks', link: '/en/applied/hooks' },
{ text: 'Fonts', link: '/en/applied/fonts' },
{ text: 'Localization', link: '/en/applied/localization' },
{ text: 'Структура проекта', link: '/docs/applied/project-structure' },
{ text: 'Компоненты', link: '/docs/applied/components' },
{ text: 'Страницы (App Router)', link: '/docs/applied/page-level' },
{ text: 'Шаблоны и генерация кода', link: '/docs/applied/templates-generation' },
{ text: 'Стили', link: '/docs/applied/styles' },
{ text: 'Изображения', link: '/docs/applied/images-sprites' },
{ text: 'SVG-спрайты', link: '/docs/applied/svg-sprites' },
{ text: 'Видео', link: '/docs/applied/video' },
{ text: 'API', link: '/docs/applied/api' },
{ text: 'Stores', link: '/docs/applied/stores' },
{ text: 'Хуки', link: '/docs/applied/hooks' },
{ text: 'Шрифты', link: '/docs/applied/fonts' },
{ text: 'Локализация', link: '/docs/applied/localization' },
{ text: 'Настройка VS Code', link: '/docs/applied/vscode' },
],
},
];
@@ -123,8 +73,9 @@ export default defineConfig({
// (попадают в корень `dist/` как статика). Исключаем из сканирования
// страниц, иначе VitePress рендерит их как HTML-страницы.
srcExclude: ['public/**'],
lang: 'ru-RU',
title: 'NextJS Style Guide',
description: 'Правила и стандарты разработки на NextJS и TypeScript',
description: 'Стандарты разработки на Next.js + TypeScript с архитектурой SLM',
vite: {
plugins: [utf8TextPlugin],
@@ -133,44 +84,18 @@ export default defineConfig({
},
},
locales: {
root: {
label: 'Languages',
lang: 'en',
},
ru: {
label: 'Русский',
lang: 'ru-RU',
link: '/ru/',
description: 'Стандарты разработки на Next.js + TypeScript с архитектурой SLM',
themeConfig: {
sidebar: ruSidebar,
socialLinks: [
{ icon: 'github', link: 'https://gromlab.ru/docs/nextjs-style-guide' },
],
},
// Расширенный блок описания для 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,
socialLinks: [
{ icon: 'github', link: 'https://gromlab.ru/docs/nextjs-style-guide' },
],
},
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.',
},
themeConfig: {
sidebar,
socialLinks: [
{ icon: 'github', link: 'https://gromlab.ru/docs/nextjs-style-guide' },
],
},
});
// Расширенный блок описания для 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

@@ -16,13 +16,4 @@
overflow-wrap: anywhere;
}
/*
* Скрыть root-локаль (лендинг с layout: false) из переключателя языка.
* VitePress не даёт исключить root через config — она всегда попадает
* в lang-switcher. Идентифицируем по href="/" — другие локали используют
* href="/ru/" и href="/en/".
*/
.VPNavBarTranslations a[href="/"],
.VPNavScreenTranslations a[href="/"] {
display: none;
}

View File

@@ -7,9 +7,8 @@
Документационный сайт с правилами и стандартами фронтенд-разработки на Next.js + TypeScript.
- Движок: VitePress
- Языки: русский (основной), английский
- Русская версия: `docs/ru/`
- Английская версия: `docs/en/`
- Язык: русский
- Контент: `docs/docs/`
## Команды
@@ -17,51 +16,53 @@
|---------|-----------|
| `npm run dev` | Локальный сервер разработки |
| `npm run build` | Сборка статического сайта |
| `npm run llms` | Генерация `generated/{lang}/llms.txt` (карта документации для LLM) и README |
| `npm run llms` | Генерация `llms.txt` (карта документации для LLM) и README |
## Структура файлов
```
docs/
├── ru/ # Русская версия (основная)
│ ├── index.md # Главная страница
├── basics/ # Базовые правила
│ │ ├── tech-stack.md
│ │ ├── architecture.md
│ │ ├── code-style.md
│ ├── naming.md
│ ├── documentation.md
── typing.md
── applied/ # Прикладные разделы
├── vscode.md
── project-structure.md
── components.md
├── page-level.md
├── templates-generation.md
├── styles.md
├── images-sprites.md
├── svg-sprites.md
├── video.md
├── api.md
├── stores.md
├── hooks.md
├── fonts.md
── localization.md
├── en/ # Английская версия (зеркало ru/)
├── index.md # Лендинг (URL `/`)
└── docs/ # Контент документации (URL `/docs/...`)
├── index.md # Главная страница
├── workflow.md
├── workflow/ # Процессы разработки
├── basics/ # Базовые правила
│ ├── tech-stack.md
│ ├── architecture/
── code-style.md
── naming.md
├── documentation.md
── typing.md
── applied/ # Прикладные разделы
├── vscode.md
├── project-structure.md
├── components.md
├── page-level.md
├── templates-generation.md
├── styles.md
├── images-sprites.md
├── svg-sprites.md
├── video.md
├── api.md
── stores.md
├── hooks.md
├── fonts.md
└── localization.md
.vitepress/
── config.ts # Конфигурация VitePress, сайдбары, локали
generated/
├── ru/llms.txt # Карта документации для LLM (ru, llmstxt.org)
└── en/llms.txt # Карта документации для LLM (en, llmstxt.org)
── config.ts # Конфигурация VitePress, сайдбар
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/`).
2. Добавить пункт в сайдбар — `.vitepress/config.ts` (оба языка, если есть перевод).
1. Создать `.md`-файл в нужной папке (`docs/docs/basics/` или `docs/docs/applied/`).
2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
Сайдбар — единственный источник порядка и группировки для `llms.txt`.
3. Запустить `npm run llms` для обновления `generated/{lang}/llms.txt`.
3. Запустить `npm run llms` для обновления `llms.txt` и README.
## Два типа документации

View File

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

View File

@@ -1,66 +1,69 @@
# NextJS Style Guide
Conventions for Next.js project development: application architecture and layers, code structure, module organization, styling, typing, and infrastructure.
Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.
## Usage
## Использование
**For AI agents:**
**Для AI-агентов:**
- [Sections map](https://nextjs-style-guide.gromlab.ru/en/llms.txt) — `llms.txt`, table of contents with links to sections.
- [Full text](https://nextjs-style-guide.gromlab.ru/en/llms-full.txt) — `llms-full.txt`, the entire documentation in a single file.
- [Карта разделов](https://nextjs-style-guide.gromlab.ru/llms.txt) — `llms.txt`, оглавление со ссылками на разделы.
- [Полный текст](https://nextjs-style-guide.gromlab.ru/llms-full.txt) — `llms-full.txt`, вся документация одним файлом.
**For projects:**
**Для проекта:**
- [Rules archive](https://nextjs-style-guide.gromlab.ru/nextjs-style-guide-en.zip) — `nextjs-style-guide-en.zip`, a set of Markdown files to unpack into `./ai/nextjs-style-guide/` or another project folder.
- [Архив с правилами](https://nextjs-style-guide.gromlab.ru/nextjs-style-guide.zip) — `nextjs-style-guide.zip`, набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
## Documentation Structure
## Структура документации
### Processes
### Workflow
**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? |
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Начало работы | Что нужно знать перед началом разработки? |
| Создание проекта | Как начать новый проект? |
| Генерация кода | Какие модули должны генерироваться из шаблонов? |
| Добавление страницы | Как добавить новую страницу в проект? |
| Добавление UI-модуля | Как создать компонент, бизнес-модуль, виджет или layout? |
| Стилизация | Как стилизовать компоненты в проекте? |
| Получение данных | Как получать данные с сервера? |
| Управление состоянием | Как работать с состоянием? |
| Локализация | Как добавлять переводы и подключать локализацию? |
### 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 SLM 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? |
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Технологии и библиотеки | Какой стек используем? |
| Архитектура | Как устроены слои SLM, зависимости, публичный API? |
| Стиль кода | Как оформлять код: отступы, кавычки, импорты, early return? |
| Именование | Как называть файлы, переменные, компоненты, хуки? |
| Документирование | Как писать JSDoc: что документировать, а что нет? |
| Типизация | Как типизировать: type vs interface, any/unknown? |
### Applied Sections
### Прикладные разделы
**How a specific area works** — rules, structure, and code examples for specific technologies and tools.
**Как это настроить и использовать** — конфигурация, структура и примеры кода для конкретных областей.
| Раздел | Отвечает на вопрос |
|--------|-------------------|
| Настройка VS Code | Как настроить редактор для проекта? |
| Структура проекта | Как организованы папки и файлы по SLM? |
| Компоненты | Как устроен компонент: файлы, пропсы, clsx? |
| Page-level компоненты | Как описывать layout, page, loading, error, not-found? |
| Шаблоны и генерация кода | Как работают шаблоны, синтаксис и инструменты генерации? |
| Стили | Как писать CSS: PostCSS Modules, вложенность, медиа, токены? |
| Изображения | _(не заполнен)_ |
| SVG-спрайты | _(не заполнен)_ |
| Видео | _(не заполнен)_ |
| API | _(не заполнен)_ |
| Stores | _(не заполнен)_ |
| Хуки | _(не заполнен)_ |
| Шрифты | _(не заполнен)_ |
| Локализация | _(не заполнен)_ |
| 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)_ |

View File

@@ -1,69 +0,0 @@
# NextJS Style Guide
Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.
## Использование
**Для AI-агентов:**
- [Карта разделов](https://nextjs-style-guide.gromlab.ru/ru/llms.txt) — `llms.txt`, оглавление со ссылками на разделы.
- [Полный текст](https://nextjs-style-guide.gromlab.ru/ru/llms-full.txt) — `llms-full.txt`, вся документация одним файлом.
**Для проекта:**
- [Архив с правилами](https://nextjs-style-guide.gromlab.ru/nextjs-style-guide-ru.zip) — `nextjs-style-guide-ru.zip`, набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
## Структура документации
### 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

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

View File

@@ -52,7 +52,7 @@ auth/
└── index.ts # публичный API
```
Подробное описание каждого сегмента — в разделе [Сегменты](/ru/basics/architecture/reference/segments).
Подробное описание каждого сегмента — в разделе [Сегменты](/docs/basics/architecture/reference/segments).
## Публичный API

View File

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

View File

@@ -6,12 +6,12 @@
**Для AI-агентов:**
- [Карта разделов](https://nextjs-style-guide.gromlab.ru/ru/llms.txt) — `llms.txt`, оглавление со ссылками на разделы.
- [Полный текст](https://nextjs-style-guide.gromlab.ru/ru/llms-full.txt) — `llms-full.txt`, вся документация одним файлом.
- [llms.txt](/llms.txt) — Карта разделов, оглавление со ссылками на разделы.
- [llms-full.txt](/llms-full.txt) — Вся документация одним файлом.
**Для проекта:**
- [Архив с правилами](https://nextjs-style-guide.gromlab.ru/nextjs-style-guide-ru.zip) — `nextjs-style-guide-ru.zip`, набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
- [nextjs-style-guide.zip](/nextjs-style-guide.zip) — Набор Markdown-файлов для распаковки в `./ai/nextjs-style-guide/` или другую папку проекта.
## Структура документации

View File

@@ -28,4 +28,4 @@ title: Генерация кода
- Повторяющаяся структура появляется больше одного раза.
- Существующий шаблон не покрывает нужный тип модуля.
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/ru/applied/templates-generation).
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/docs/applied/templates-generation).

View File

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

View File

@@ -12,7 +12,7 @@ title: Добавление страницы
## Порядок действий
1. [Сгенерировать](/ru/applied/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
1. [Сгенерировать](/docs/applied/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
2. Заполнить экран логикой и стилями.
@@ -20,8 +20,8 @@ title: Добавление страницы
## Правила
- Ручное создание файловой структуры экрана запрещено — только [генерация](/ru/applied/templates-generation) из шаблона.
- Ручное создание файловой структуры экрана запрещено — только [генерация](/docs/applied/templates-generation) из шаблона.
- Логика, стили и зависимости размещаются в экране, не в `page.tsx`.
- Каждая страница содержит `metadata` с `title` и `description`.
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/ru/applied/page-level).
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/docs/applied/page-level).

View File

@@ -10,7 +10,7 @@ title: Начало работы
**Next.js** (App Router), **Mantine**, **Zustand**, **SLM Design**.
Подробнее — [Технологии и библиотеки](/ru/basics/tech-stack).
Подробнее — [Технологии и библиотеки](/docs/basics/tech-stack).
## Ключевые особенности
@@ -19,4 +19,4 @@ title: Начало работы
## Настройка окружения
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/ru/applied/vscode).
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/docs/applied/vscode).

View File

@@ -20,4 +20,4 @@ title: Стилизация
- **Магические значения** — произвольные цвета, отступы и скругления запрещены, использовать токены.
- **Глобальные стили** вне `app/styles/` запрещены.
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили](/ru/applied/styles).
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили](/docs/applied/styles).

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,66 +0,0 @@
# NextJS Style Guide
Conventions for Next.js project development: application architecture and layers, code structure, module organization, styling, typing, and infrastructure.
## Usage
**For AI agents:**
- [Sections map](https://nextjs-style-guide.gromlab.ru/en/llms.txt) — `llms.txt`, table of contents with links to sections.
- [Full text](https://nextjs-style-guide.gromlab.ru/en/llms-full.txt) — `llms-full.txt`, the entire documentation in a single file.
**For projects:**
- [Rules archive](https://nextjs-style-guide.gromlab.ru/nextjs-style-guide-en.zip) — `nextjs-style-guide-en.zip`, a set of Markdown files to unpack into `./ai/nextjs-style-guide/` or another project folder.
## 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 SLM 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)_ |

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,102 +3,21 @@ layout: false
---
<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'
// __BUILD_VERSION__ подставляется Vite-define из ENV `BUILD_VERSION`
// (см. .vitepress/config.ts). В dev и build всегда определена.
const buildVersion = __BUILD_VERSION__
const dict = {
ru: {
tagline: 'Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.',
langLabel: 'Язык',
themeLabel: 'Тема',
repoLabel: 'Репозиторий',
themes: { auto: 'Авто', light: 'Светлая', dark: 'Тёмная' },
cards: {
docs: {
title: 'Документация',
desc: 'Все разделы: процессы разработки, базовые правила, прикладные руководства.',
href: './ru/',
cta: 'Открыть',
},
ai: {
title: 'Ассистенту',
desc: 'Карта документации в формате llms.txt для AI-агентов.',
buttons: [
{ label: 'llms.txt', href: './ru/llms.txt' },
{ label: 'llms-full.txt', href: './ru/llms-full.txt' },
],
},
zip: {
title: 'Скачать правила',
desc: 'Архив всех Markdown-файлов одним ZIP.',
href: './nextjs-style-guide-ru.zip',
cta: 'Скачать',
},
},
},
en: {
tagline: 'Conventions for Next.js project development: application architecture and layers, code structure, module organization, styling, typing, and infrastructure.',
langLabel: 'Language',
themeLabel: 'Theme',
repoLabel: 'Repository',
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.',
badge: 'in development',
buttons: [
{ label: 'llms.txt', href: '#' },
{ label: 'llms-full.txt', href: '#' },
],
},
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')
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)
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) {
theme.value = value
if (value === 'auto') {
@@ -124,7 +43,7 @@ function toggleTheme(value) {
<section class="landing__hero">
<h1 class="landing__title">NextJS Style Guide</h1>
<ClientOnly>
<p class="landing__tagline">{{ t.tagline }}</p>
<p class="landing__tagline">Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.</p>
<div class="landing__controls">
<a
class="landing__repo"
@@ -135,31 +54,15 @@ function toggleTheme(value) {
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<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"/>
</svg>
<span>{{ t.repoLabel }}</span>
<span>Репозиторий</span>
</a>
<div class="seg" role="group" :aria-label="t.langLabel">
<button
type="button"
class="seg__btn"
:class="{ 'seg__btn--active': lang === 'ru' }"
:aria-pressed="lang === 'ru'"
@click="setLang('ru')"
>Русский</button>
<button
type="button"
class="seg__btn"
:class="{ 'seg__btn--active': lang === 'en' }"
:aria-pressed="lang === 'en'"
@click="setLang('en')"
>English</button>
</div>
<div class="seg seg--icons" role="group" :aria-label="t.themeLabel">
<div class="seg seg--icons" role="group" aria-label="Тема">
<button
type="button"
class="seg__btn"
:class="{ 'seg__btn--active': theme === 'light' }"
:aria-pressed="theme === 'light'"
:title="t.themes.light"
title="Светлая"
@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>
@@ -169,7 +72,7 @@ function toggleTheme(value) {
class="seg__btn"
:class="{ 'seg__btn--active': theme === 'dark' }"
:aria-pressed="theme === 'dark'"
:title="t.themes.dark"
title="Тёмная"
@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>
@@ -179,48 +82,26 @@ function toggleTheme(value) {
</ClientOnly>
</section>
<ClientOnly>
<section class="landing__cards">
<template v-for="key in ['docs', 'ai', 'zip']" :key="key">
<div
v-if="t.cards[key].buttons"
class="landing__card landing__card--multi"
:class="{ 'landing__card--soon': t.cards[key].badge }"
:aria-disabled="t.cards[key].badge ? 'true' : null"
>
<h3>
{{ t.cards[key].title }}
<span v-if="t.cards[key].badge" class="landing__badge">{{ t.cards[key].badge }}</span>
</h3>
<p>{{ t.cards[key].desc }}</p>
<div class="landing__buttons">
<a
v-for="btn in t.cards[key].buttons"
:key="btn.label"
class="landing__button"
:href="btn.href"
target="_blank"
rel="noopener"
>{{ btn.label }}</a>
</div>
</div>
<a
v-else
class="landing__card"
:class="{ 'landing__card--soon': t.cards[key].badge }"
:href="t.cards[key].href"
:aria-disabled="t.cards[key].badge ? 'true' : null"
>
<h3>
{{ t.cards[key].title }}
<span v-if="t.cards[key].badge" class="landing__badge">{{ t.cards[key].badge }}</span>
</h3>
<p>{{ t.cards[key].desc }}</p>
<span class="landing__cta">{{ t.cards[key].cta }} →</span>
</a>
</template>
</section>
</ClientOnly>
<section class="landing__cards">
<a class="landing__card" href="./docs/">
<h3>Документация</h3>
<p>Все разделы: процессы разработки, базовые правила, прикладные руководства.</p>
<span class="landing__cta">Открыть →</span>
</a>
<div class="landing__card landing__card--multi">
<h3>Ассистенту</h3>
<p>Карта документации в формате llms.txt для AI-агентов.</p>
<div class="landing__buttons">
<a class="landing__button" href="./llms.txt" target="_blank" rel="noopener">llms.txt</a>
<a class="landing__button" href="./llms-full.txt" target="_blank" rel="noopener">llms-full.txt</a>
</div>
</div>
<a class="landing__card" href="./nextjs-style-guide.zip">
<h3>Скачать правила</h3>
<p>Архив всех Markdown-файлов одним ZIP.</p>
<span class="landing__cta">Скачать →</span>
</a>
</section>
<p class="landing__version">v{{ buildVersion }}</p>
</div>
@@ -374,12 +255,6 @@ function toggleTheme(value) {
transform: translateY(-2px);
}
.landing__card--soon {
opacity: 0.55;
cursor: not-allowed;
pointer-events: none;
}
.landing__card h3 {
margin: 0;
font-size: 18px;
@@ -436,20 +311,6 @@ function toggleTheme(value) {
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) {
.landing {
padding: 48px 20px 56px;

View File

@@ -11,7 +11,8 @@ const BUILD_DATE = new Date().toISOString();
/** Корневая папка для генерируемой статики (попадает в build dist). */
const PUBLIC_DIR = 'docs/public';
type Lang = 'ru' | 'en';
/** Префикс URL документации. Соответствует структуре `docs/docs/...`. */
const DOC_PREFIX = '/docs/';
interface SidebarItem {
text: string;
@@ -76,13 +77,13 @@ const firstParagraphAfterH1 = (body: string): string | null => {
};
/**
* Преобразовать sidebar `link` в относительный путь файла внутри
* `docs/{lang}/`. Sidebar links содержат полный префикс локали
* (`/ru/...`, `/en/...`) — отрезаем его.
* Преобразовать sidebar `link` (например `/docs/foo`) в относительный
* путь файла внутри `docs/docs/`. Префикс `/docs/` отрезается.
*/
const linkToRel = (link: string, lang: Lang): string => {
const prefix = `/${lang}/`;
let rel = link.startsWith(prefix) ? link.slice(prefix.length) : link.replace(/^\//, '');
const linkToRel = (link: string): string => {
let rel = link.startsWith(DOC_PREFIX)
? link.slice(DOC_PREFIX.length)
: link.replace(/^\//, '');
if (rel === '' || rel.endsWith('/')) {
rel += 'index.md';
} else {
@@ -91,15 +92,12 @@ const linkToRel = (link: string, lang: Lang): string => {
return rel;
};
const linkToFilePath = (link: string, lang: Lang): string =>
path.join('docs', lang, linkToRel(link, lang));
const linkToFilePath = (link: string): string =>
path.join('docs/docs', linkToRel(link));
/**
* Абсолютный путь от корня сайта к `.md`-копии страницы.
* После build файлы лежат в `dist/{lang}/...md` (через `docs/public/`).
*/
const linkToSiteUrl = (link: string, lang: Lang): string =>
`/${lang}/${linkToRel(link, lang)}`;
/** Абсолютный URL `.md`-копии страницы на сайте. */
const linkToSiteUrl = (link: string): string =>
`${DOC_PREFIX}${linkToRel(link)}`;
/**
* Развернуть sidebar в плоский список с сохранением группы и
@@ -153,34 +151,20 @@ const groupBySection = (entries: Entry[]): Map<string, Entry[]> => {
return map;
};
const buildLlms = (lang: Lang): void => {
const localeKey = lang;
// VitePress-конфиг типизирован как `UserConfig`, но обращаемся к
// фактически переданным значениям — сужаем тип через any.
const cfg = config as unknown as {
title: string;
description: string;
locales: Record<
string,
{
description?: string;
llmsBlockquote?: string;
llmsContext?: string;
themeConfig?: { sidebar?: SidebarItem[] };
}
>;
};
interface SiteConfig {
title: string;
description: string;
themeConfig: { sidebar: SidebarItem[] };
llmsBlockquote?: string;
llmsContext?: string;
}
const locale = cfg.locales[localeKey];
const sidebar = locale?.themeConfig?.sidebar;
if (!sidebar) {
console.warn(`[${lang}] sidebar не найден в config`);
return;
}
// Для blockquote предпочитаем расширенный llms-текст; короткий
// description — fallback и используется для HTML meta-тега VitePress.
const blockquote = locale.llmsBlockquote ?? locale.description ?? cfg.description;
const context = locale.llmsContext;
const cfg = config as unknown as SiteConfig;
const buildLlms = (): void => {
const sidebar = cfg.themeConfig.sidebar;
const blockquote = cfg.llmsBlockquote ?? cfg.description;
const context = cfg.llmsContext;
const entries = flattenSidebar(sidebar);
const grouped = groupBySection(entries);
@@ -200,19 +184,16 @@ const buildLlms = (lang: Lang): void => {
lines.push('');
for (const entry of items) {
const filePath = linkToFilePath(entry.link, lang);
const url = linkToSiteUrl(entry.link, lang);
const filePath = linkToFilePath(entry.link);
const url = linkToSiteUrl(entry.link);
// Текст ссылки берём из sidebar — он специально написан для навигации
// и точнее отражает иерархию (например "Обзор" внутри группы "Архитектура").
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);
} else {
console.warn(`[${lang}] файл не найден: ${filePath}`);
console.warn(`файл не найден: ${filePath}`);
}
const display = entry.prefix
@@ -225,40 +206,6 @@ const buildLlms = (lang: Lang): void => {
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 });
const outFile = path.join(PUBLIC_DIR, 'llms.txt');
fs.writeFileSync(outFile, lines.join('\n'), 'utf8');
@@ -288,26 +235,26 @@ const copyDirSync = (
};
/**
* Скопировать все `.md`-файлы локали в `docs/public/{lang}/`,
* чтобы они попали в build `dist/` и были доступны по URL `/lang/path.md`.
* Скопировать все `.md`-файлы документации в `docs/public/docs/`,
* чтобы они попали в build `dist/` и были доступны по URL `/docs/path.md`.
*/
const copyMdFiles = (lang: Lang): void => {
const srcDir = path.join('docs', lang);
const destDir = path.join(PUBLIC_DIR, lang);
const copyMdFiles = (): void => {
const srcDir = 'docs/docs';
const destDir = path.join(PUBLIC_DIR, 'docs');
if (!fs.existsSync(srcDir)) return;
const copied = copyDirSync(srcDir, destDir, (name) => name.endsWith('.md'));
console.log(`[${lang}] скопировано ${copied} .md-файлов в ${destDir}`);
console.log(`скопировано ${copied} .md-файлов в ${destDir}`);
};
/**
* Преобразовать sidebar `link` в относительный путь файла внутри архива
* (от корня папки `nextjs-style-guide/`). Это путь, по которому файл лежит
* в распакованной папке, без расширения добавляется `.md`.
* (от корня папки `nextjs-style-guide/`).
*/
const linkToArchiveRel = (link: string, lang: Lang): string => {
const prefix = `/${lang}/`;
let rel = link.startsWith(prefix) ? link.slice(prefix.length) : link.replace(/^\//, '');
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 {
@@ -317,12 +264,12 @@ const linkToArchiveRel = (link: string, lang: Lang): string => {
};
/**
* Заменить во всех `.md` архива ссылки `[text](/ru/foo)` на относительные
* Заменить во всех `.md` архива ссылки `[text](/docs/foo)` на относительные
* пути от расположения файла. Без этого внутренние ссылки в распакованной
* папке не работают.
*/
const transformLinksInDir = (rootDir: string, lang: Lang): void => {
const linkRe = /\]\(\/([a-z]{2})\/([^)\s#]*)(#[^)]*)?\)/g;
const transformLinksInDir = (rootDir: string): void => {
const linkRe = /\]\(\/docs\/([^)\s#]*)(#[^)]*)?\)/g;
const walk = (dir: string): void => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -336,12 +283,8 @@ const transformLinksInDir = (rootDir: string, lang: Lang): void => {
const content = fs.readFileSync(full, 'utf8');
const fileDir = path.dirname(full);
const updated = content.replace(linkRe, (match, urlLang, route, hash = '') => {
// Ссылки на другую локаль не трогаем — её в архиве нет.
if (urlLang !== lang) return match;
const fakeLink = `/${urlLang}/${route}${route.endsWith('/') ? '' : ''}`;
const targetRel = linkToArchiveRel(fakeLink, lang);
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;
@@ -362,29 +305,14 @@ const transformLinksInDir = (rootDir: string, lang: Lang): void => {
* с относительными ссылками, описаниями из frontmatter/первого абзаца
* и метаинфо сборки.
*/
const buildArchiveReadme = (lang: Lang, rootDir: string): void => {
const cfg = config as unknown as {
title: string;
locales: Record<
string,
{
description?: string;
llmsBlockquote?: string;
llmsContext?: string;
themeConfig?: { sidebar?: SidebarItem[] };
}
>;
};
const buildArchiveReadme = (rootDir: string): void => {
const sidebar = cfg.themeConfig.sidebar;
const blockquote = cfg.llmsBlockquote ?? cfg.description ?? '';
const context = cfg.llmsContext;
const locale = cfg.locales[lang];
const sidebar = locale?.themeConfig?.sidebar;
if (!sidebar) return;
const blockquote = locale.llmsBlockquote ?? locale.description ?? '';
const context = locale.llmsContext;
const entries = flattenSidebar(sidebar).filter(
// «Главная» из sidebar — это страница локали для веба, в архиве не нужна.
(e) => !(e.section === 'Главная' || e.section === 'Home'),
// «Главная» из sidebar — это страница раздела для веба, в архиве не нужна.
(e) => e.section !== 'Главная',
);
const grouped = groupBySection(entries);
@@ -400,16 +328,15 @@ const buildArchiveReadme = (lang: Lang, rootDir: string): void => {
lines.push('');
}
const heading = lang === 'ru' ? 'Содержание' : 'Contents';
lines.push(`## ${heading}`);
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, lang);
const filePath = path.join(rootDir, linkToArchiveRel(entry.link, lang));
const targetRel = './' + linkToArchiveRel(entry.link);
const filePath = path.join(rootDir, linkToArchiveRel(entry.link));
let description: string | null = null;
if (fs.existsSync(filePath)) {
@@ -436,33 +363,30 @@ const buildArchiveReadme = (lang: Lang, rootDir: string): void => {
};
/**
* Собрать `nextjs-style-guide-{lang}.zip`. Внутри архива — единая папка
* `nextjs-style-guide/` с `.md`-файлами локали, README-точкой входа,
* `llms-full.txt` и `VERSION`. Внутренние ссылки в `.md` преобразуются
* в относительные.
*
* Веб-`index.md` локали из архива удаляется — его роль выполняет README.md.
* Собрать `nextjs-style-guide.zip`. Внутри — папка `nextjs-style-guide/`
* с `.md`-файлами, README, `llms-full.txt` и `VERSION`. Внутренние ссылки
* преобразуются в относительные.
*/
const buildZip = (lang: Lang): void => {
const buildZip = (): void => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nsg-'));
const stage = path.join(tmpRoot, 'nextjs-style-guide');
fs.mkdirSync(stage, { recursive: true });
// 1. Копируем все .md локали в staging.
copyDirSync(path.join('docs', lang), stage, (name) => name.endsWith('.md'));
// 1. Копируем все .md в staging.
copyDirSync('docs/docs', stage, (name) => name.endsWith('.md'));
// 2. Удаляем веб-index.md локали — в архиве он избыточен.
// 2. Удаляем веб-index.md — в архиве его роль выполняет README.md.
const indexPath = path.join(stage, 'index.md');
if (fs.existsSync(indexPath)) fs.unlinkSync(indexPath);
// 3. Преобразуем абсолютные ссылки `/ru/...` в относительные.
transformLinksInDir(stage, lang);
// 3. Преобразуем абсолютные ссылки `/docs/...` в относительные.
transformLinksInDir(stage);
// 4. Генерируем точку входа README.md.
buildArchiveReadme(lang, stage);
buildArchiveReadme(stage);
// 5. Кладём llms-full.txt — удобно для одноразового чтения LLM.
const llmsFullSrc = path.join(PUBLIC_DIR, lang, 'llms-full.txt');
const llmsFullSrc = path.join(PUBLIC_DIR, 'llms-full.txt');
if (fs.existsSync(llmsFullSrc)) {
fs.copyFileSync(llmsFullSrc, path.join(stage, 'llms-full.txt'));
}
@@ -473,10 +397,7 @@ const buildZip = (lang: Lang): void => {
`${VERSION}\n${BUILD_DATE}\n`,
);
const outFile = path.resolve(
PUBLIC_DIR,
`nextjs-style-guide-${lang}.zip`,
);
const outFile = path.resolve(PUBLIC_DIR, 'nextjs-style-guide.zip');
fs.rmSync(outFile, { force: true });
execFileSync('zip', ['-rq', outFile, 'nextjs-style-guide'], {
@@ -509,42 +430,26 @@ const shiftHeadings = (content: string): string => {
};
/**
* Собрать `llms-full.txt` — все страницы локали в одном файле.
* Собрать `llms-full.txt` — все страницы в одном файле.
* Порядок страниц повторяет порядок в sidebar.
*/
const buildLlmsFull = (lang: Lang): void => {
const cfg = config as unknown as {
title: string;
locales: Record<
string,
{
description?: string;
llmsBlockquote?: string;
llmsContext?: string;
themeConfig?: { sidebar?: SidebarItem[] };
}
>;
};
const locale = cfg.locales[lang];
const sidebar = locale?.themeConfig?.sidebar;
if (!sidebar) return;
const buildLlmsFull = (): void => {
const sidebar = cfg.themeConfig.sidebar;
const entries = flattenSidebar(sidebar);
const blockquote = locale.llmsBlockquote ?? locale.description ?? '';
const blockquote = cfg.llmsBlockquote ?? cfg.description ?? '';
const parts: string[] = [];
parts.push(`# ${cfg.title}`);
parts.push('');
if (blockquote) parts.push(`> ${blockquote}`);
if (locale.llmsContext) {
if (cfg.llmsContext) {
parts.push('');
parts.push(locale.llmsContext);
parts.push(cfg.llmsContext);
}
parts.push('');
for (const entry of entries) {
const filePath = linkToFilePath(entry.link, lang);
const filePath = linkToFilePath(entry.link);
if (!fs.existsSync(filePath)) continue;
const raw = fs.readFileSync(filePath, 'utf8');
@@ -558,9 +463,8 @@ const buildLlmsFull = (lang: Lang): void => {
parts.push('');
}
const outDir = path.join(PUBLIC_DIR, lang);
fs.mkdirSync(outDir, { recursive: true });
const outFile = path.join(outDir, 'llms-full.txt');
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} создан`);
};
@@ -570,18 +474,9 @@ const writeManifest = (): void => {
const manifest = {
version: VERSION,
buildDate: BUILD_DATE,
languages: {
ru: {
llms: '/ru/llms.txt',
llmsFull: '/ru/llms-full.txt',
zip: '/nextjs-style-guide-ru.zip',
},
en: {
llms: '/en/llms.txt',
llmsFull: '/en/llms-full.txt',
zip: '/nextjs-style-guide-en.zip',
},
},
llms: '/llms.txt',
llmsFull: '/llms-full.txt',
zip: '/nextjs-style-guide.zip',
};
fs.mkdirSync(PUBLIC_DIR, { recursive: true });
fs.writeFileSync(
@@ -592,28 +487,22 @@ const writeManifest = (): void => {
console.log(`${PUBLIC_DIR}/manifest.json создан`);
};
/** Скопировать `index.md` локали в корневой README без frontmatter */
const buildReadme = (lang: Lang, outFile: string): void => {
const indexPath = path.join('docs', lang, 'index.md');
/** Скопировать `index.md` документации в корневой README без frontmatter. */
const buildReadme = (): void => {
const indexPath = 'docs/docs/index.md';
if (!fs.existsSync(indexPath)) {
console.warn(`Пропуск ${outFile}: ${indexPath} не найден`);
console.warn(`Пропуск README.md: ${indexPath} не найден`);
return;
}
const raw = fs.readFileSync(indexPath, 'utf8');
const { body } = parseFrontmatter(raw);
fs.writeFileSync(outFile, body.trimStart(), 'utf8');
console.log(`${outFile} обновлён из ${indexPath}`);
fs.writeFileSync('README.md', body.trimStart(), 'utf8');
console.log(`README.md обновлён из ${indexPath}`);
};
buildLlms('ru');
buildLlms('en');
buildLlmsFull('ru');
buildLlmsFull('en');
buildRootIndex();
copyMdFiles('ru');
copyMdFiles('en');
buildZip('ru');
buildZip('en');
buildLlms();
buildLlmsFull();
copyMdFiles();
buildZip();
writeManifest();
buildReadme('en', 'README.md');
buildReadme('ru', 'README_RU.md');
buildReadme();