refactor: удалить английскую локаль и упростить структуру
- Удалена английская версия документации (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:
289
generate-llms.ts
289
generate-llms.ts
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user