feat: llms-full.txt, README архива и доработка лендинга
- добавлен генератор 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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,9 +166,26 @@ function toggleTheme(value) {
|
||||
|
||||
<ClientOnly>
|
||||
<section class="landing__cards">
|
||||
<template v-for="key in ['docs', 'ai', 'zip']" :key="key">
|
||||
<div
|
||||
v-if="t.cards[key].buttons && !t.cards[key].badge"
|
||||
class="landing__card landing__card--multi"
|
||||
>
|
||||
<h3>{{ t.cards[key].title }}</h3>
|
||||
<p>{{ t.cards[key].desc }}</p>
|
||||
<div class="landing__buttons">
|
||||
<a
|
||||
v-for="key in ['docs', 'ai', 'zip']"
|
||||
:key="key"
|
||||
v-for="btn in t.cards[key].buttons"
|
||||
:key="btn.href"
|
||||
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"
|
||||
@@ -179,6 +198,7 @@ function toggleTheme(value) {
|
||||
<p>{{ t.cards[key].desc }}</p>
|
||||
<span class="landing__cta">{{ t.cards[key].cta }} →</span>
|
||||
</a>
|
||||
</template>
|
||||
</section>
|
||||
</ClientOnly>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 проектов: архитектура и слои приложения, структура кода, организация модулей, стилизация, типизация и инфраструктура.
|
||||
|
||||
## Структура документации
|
||||
|
||||
|
||||
244
generate-llms.ts
244
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(`<!-- ${entry.link} -->`);
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user