From ae103e962e9b43105f0b5abe6f7d316c7a5ec28f Mon Sep 17 00:00:00 2001 From: "S.Gromov" Date: Sat, 25 Apr 2026 19:56:44 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20llms-full.txt,=20README=20=D0=B0=D1=80?= =?UTF-8?q?=D1=85=D0=B8=D0=B2=D0=B0=20=D0=B8=20=D0=B4=D0=BE=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BB=D0=B5=D0=BD=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавлен генератор 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 --- .vitepress/config.ts | 12 ++- docs/en/index.md | 9 +- docs/index.md | 83 +++++++++++---- docs/ru/index.md | 7 +- generate-llms.ts | 244 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 318 insertions(+), 37 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 085eca7..e9e7087 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -1,6 +1,10 @@ import { defineConfig } from 'vitepress'; const ruSidebar = [ + { + text: 'Главная', + link: '/ru/', + }, { text: 'Workflow', link: '/ru/workflow', @@ -47,6 +51,10 @@ const ruSidebar = [ ]; const enSidebar = [ + { + text: 'Home', + link: '/en/', + }, { text: 'Processes', items: [ @@ -133,7 +141,7 @@ export default defineConfig({ ru: { label: 'Русский', lang: 'ru-RU', - link: '/ru/', + link: '/', description: 'Стандарты разработки на Next.js + TypeScript с архитектурой SLM', themeConfig: { sidebar: ruSidebar, @@ -148,7 +156,7 @@ export default defineConfig({ en: { label: 'English', lang: 'en-US', - link: '/en/', + link: '/', description: 'Next.js + TypeScript development standards with SLM architecture', themeConfig: { sidebar: enSidebar, diff --git a/docs/en/index.md b/docs/en/index.md index 0baefe1..39bd837 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -1,6 +1,6 @@ # NextJS Style Guide -Rules and standards for NextJS and TypeScript development: architecture, typing, styles, components, API, and infrastructure. +Conventions for Next.js project development: application architecture and layers, code structure, module organization, styling, typing, and infrastructure. ## Documentation Structure @@ -26,7 +26,7 @@ Rules and standards for NextJS and TypeScript development: architecture, typing, | Section | Answers the question | |---------|---------------------| | Tech Stack | What stack do we use? | -| Architecture | How are FSD layers, dependencies, and public API structured? | +| 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? | @@ -51,8 +51,3 @@ Rules and standards for NextJS and TypeScript development: architecture, typing, | 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 diff --git a/docs/index.md b/docs/index.md index ac6b102..18972d7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,7 +14,7 @@ const buildVersion = __BUILD_VERSION__ const dict = { ru: { - tagline: 'Готовые соглашения по архитектуре, коду, компонентам и инфраструктуре для Next.js + TypeScript-проектов — чтобы команда писала одинаково, а новые разработчики включались в проект быстрее.', + tagline: 'Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.', langLabel: 'Язык', themeLabel: 'Тема', themes: { auto: 'Авто', light: 'Светлая', dark: 'Тёмная' }, @@ -28,8 +28,10 @@ const dict = { ai: { title: 'Ассистенту', desc: 'Карта документации в формате llms.txt для AI-агентов.', - href: './ru/llms.txt', - cta: 'Открыть', + buttons: [ + { label: 'llms.txt', href: './ru/llms.txt' }, + { label: 'llms-full.txt', href: './ru/llms-full.txt' }, + ], }, zip: { title: 'Скачать правила', @@ -164,21 +166,39 @@ function toggleTheme(value) {
- -

- {{ t.cards[key].title }} - {{ t.cards[key].badge }} -

-

{{ t.cards[key].desc }}

- {{ t.cards[key].cta }} → -
+
@@ -332,6 +352,33 @@ function toggleTheme(value) { 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 { text-align: center; margin: 24px 0 0; diff --git a/docs/ru/index.md b/docs/ru/index.md index 6278457..75e9289 100644 --- a/docs/ru/index.md +++ b/docs/ru/index.md @@ -1,11 +1,6 @@ # 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 +Соглашения по разработке Next.js проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура. ## Структура документации diff --git a/generate-llms.ts b/generate-llms.ts index 97c6df7..d57340a 100644 --- a/generate-llms.ts +++ b/generate-llms.ts @@ -301,19 +301,173 @@ const copyMdFiles = (lang: Lang): void => { }; /** - * Собрать `nextjs-style-guide-{lang}.zip` со всеми `.md` локали и `VERSION`. - * Внутри архива — единая папка `nextjs-style-guide/`. + * Преобразовать sidebar `link` в относительный путь файла внутри архива + * (от корня папки `nextjs-style-guide/`). Это путь, по которому файл лежит + * в распакованной папке, без расширения добавляется `.md`. + */ +const linkToArchiveRel = (link: string, lang: Lang): string => { + const prefix = `/${lang}/`; + let rel = link.startsWith(prefix) ? link.slice(prefix.length) : link.replace(/^\//, ''); + if (rel === '' || rel.endsWith('/')) { + rel += 'index.md'; + } else { + rel += '.md'; + } + return rel; +}; + +/** + * Заменить во всех `.md` архива ссылки `[text](/ru/foo)` на относительные + * пути от расположения файла. Без этого внутренние ссылки в распакованной + * папке не работают. + */ +const transformLinksInDir = (rootDir: string, lang: Lang): void => { + const linkRe = /\]\(\/([a-z]{2})\/([^)\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, urlLang, route, hash = '') => { + // Ссылки на другую локаль не трогаем — её в архиве нет. + if (urlLang !== lang) return match; + + const fakeLink = `/${urlLang}/${route}${route.endsWith('/') ? '' : ''}`; + const targetRel = linkToArchiveRel(fakeLink, lang); + 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 = (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 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'), + ); + 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(''); + } + + const heading = lang === 'ru' ? 'Содержание' : 'Contents'; + lines.push(`## ${heading}`); + 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)); + + 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-{lang}.zip`. Внутри архива — единая папка + * `nextjs-style-guide/` с `.md`-файлами локали, README-точкой входа, + * `llms-full.txt` и `VERSION`. Внутренние ссылки в `.md` преобразуются + * в относительные. * - * `llms.txt` в архив не кладём: его ссылки указывают на сайт и локально - * не работают. Структура папки сама по себе является картой документации. + * Веб-`index.md` локали из архива удаляется — его роль выполняет README.md. */ const buildZip = (lang: Lang): 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')); + // 2. Удаляем веб-index.md локали — в архиве он избыточен. + const indexPath = path.join(stage, 'index.md'); + if (fs.existsSync(indexPath)) fs.unlinkSync(indexPath); + + // 3. Преобразуем абсолютные ссылки `/ru/...` в относительные. + transformLinksInDir(stage, lang); + + // 4. Генерируем точку входа README.md. + buildArchiveReadme(lang, stage); + + // 5. Кладём llms-full.txt — удобно для одноразового чтения LLM. + const llmsFullSrc = path.join(PUBLIC_DIR, lang, 'llms-full.txt'); + if (fs.existsSync(llmsFullSrc)) { + fs.copyFileSync(llmsFullSrc, path.join(stage, 'llms-full.txt')); + } + + // 6. Метаинформация сборки. fs.writeFileSync( path.join(stage, 'VERSION'), `${VERSION}\n${BUILD_DATE}\n`, @@ -333,6 +487,84 @@ const buildZip = (lang: Lang): void => { 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 = (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 entries = flattenSidebar(sidebar); + const blockquote = locale.llmsBlockquote ?? locale.description ?? ''; + + const parts: string[] = []; + parts.push(`# ${cfg.title}`); + parts.push(''); + if (blockquote) parts.push(`> ${blockquote}`); + if (locale.llmsContext) { + parts.push(''); + parts.push(locale.llmsContext); + } + parts.push(''); + + for (const entry of entries) { + const filePath = linkToFilePath(entry.link, lang); + if (!fs.existsSync(filePath)) continue; + + const raw = fs.readFileSync(filePath, 'utf8'); + const content = shiftHeadings(stripFrontmatter(raw)).trim(); + if (!content) continue; + + // Мета-якорь: путь страницы для ориентации LLM + parts.push(``); + parts.push(''); + parts.push(content); + parts.push(''); + } + + const outDir = path.join(PUBLIC_DIR, lang); + fs.mkdirSync(outDir, { recursive: true }); + const outFile = path.join(outDir, 'llms-full.txt'); + fs.writeFileSync(outFile, parts.join('\n'), 'utf8'); + console.log(`${outFile} создан`); +}; + /** Манифест сборки — для лендинга и внешних потребителей. */ const writeManifest = (): void => { const manifest = { @@ -341,10 +573,12 @@ const writeManifest = (): void => { 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', }, }, @@ -373,6 +607,8 @@ const buildReadme = (lang: Lang, outFile: string): void => { buildLlms('ru'); buildLlms('en'); +buildLlmsFull('ru'); +buildLlmsFull('en'); buildRootIndex(); copyMdFiles('ru'); copyMdFiles('en');