feat: добавить хаб документаций
- добавлен React/Vite-лендинг с карточками документаций - добавлена генерация корневого llms.txt из конфига документов - добавлена сборка SLM Design через VitePress - добавлены Dockerfile, Caddyfile и Gitea CI/CD - настроены контекстные Link headers для llms.txt
This commit is contained in:
85
scripts/docs/prepare.ts
Normal file
85
scripts/docs/prepare.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
type Page = {
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
type DocsConfig = {
|
||||
mounts: Page[];
|
||||
};
|
||||
|
||||
const MD_LINK_RE = /\]\((?!#|[a-z][a-z0-9+.-]*:)([^)\s]+\.md)(#[^)]*)?\)/gi;
|
||||
|
||||
const siteName = process.argv[2];
|
||||
|
||||
if (!siteName) {
|
||||
throw new Error('Укажите имя сайта: tsx scripts/docs/prepare.ts slm-design');
|
||||
}
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const canonsDir = path.join(rootDir, 'canons');
|
||||
const siteDir = path.join(rootDir, 'docs', siteName);
|
||||
const contentDir = path.join(siteDir, 'content');
|
||||
const configPath = path.join(siteDir, 'docs.config.ts');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error(`Не найден конфиг сайта: ${configPath}`);
|
||||
}
|
||||
|
||||
const config = (await import(pathToFileURL(configPath).href)) as DocsConfig;
|
||||
const targetBySource = new Map(
|
||||
config.mounts.map((page) => [normalizePath(page.source), normalizePath(page.target)]),
|
||||
);
|
||||
|
||||
function normalizePath(value: string) {
|
||||
return value.split(path.sep).join('/').replace(/^\.\//, '');
|
||||
}
|
||||
|
||||
function formatRelativeMarkdownPath(fromTarget: string, toTarget: string) {
|
||||
const relative = path
|
||||
.relative(path.dirname(fromTarget), toTarget)
|
||||
.split(path.sep)
|
||||
.join('/');
|
||||
|
||||
return relative.startsWith('.') ? relative : `./${relative}`;
|
||||
}
|
||||
|
||||
function transformMarkdownLinks(content: string, page: Page) {
|
||||
const sourceDir = path.posix.dirname(normalizePath(page.source));
|
||||
|
||||
return content.replace(MD_LINK_RE, (match, href: string, hash = '') => {
|
||||
const [hrefPath, query = ''] = href.split('?');
|
||||
const sourcePath = normalizePath(path.posix.normalize(path.posix.join(sourceDir, hrefPath)));
|
||||
const target = targetBySource.get(sourcePath);
|
||||
|
||||
if (!target) return match;
|
||||
|
||||
const nextHref = formatRelativeMarkdownPath(page.target, `${target}${query ? `?${query}` : ''}`);
|
||||
|
||||
return `](${nextHref}${hash})`;
|
||||
});
|
||||
}
|
||||
|
||||
fs.rmSync(contentDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(contentDir, { recursive: true });
|
||||
|
||||
for (const page of config.mounts) {
|
||||
const sourcePath = path.join(canonsDir, page.source);
|
||||
const targetPath = path.join(contentDir, page.target);
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
throw new Error(`Не найден канон: ${sourcePath}`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
|
||||
const content = transformMarkdownLinks(fs.readFileSync(sourcePath, 'utf8'), page);
|
||||
fs.writeFileSync(targetPath, content, 'utf8');
|
||||
|
||||
console.log(`${page.target} -> canons/${page.source}`);
|
||||
}
|
||||
|
||||
console.log(`Подготовлен VitePress content для ${siteName}: ${config.mounts.length} страниц`);
|
||||
65
scripts/site/generate-artifacts.ts
Normal file
65
scripts/site/generate-artifacts.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { docs } from '../../src/config/docs.config';
|
||||
|
||||
const siteTitle = 'Документация';
|
||||
const siteDescription = 'Единое пространство для идей, черновиков и первых версий документаций, которые ещё формируются и постепенно становятся самостоятельными материалами.';
|
||||
|
||||
const rootDir = process.cwd();
|
||||
const publicDir = path.join(rootDir, 'public');
|
||||
const llmsPath = path.join(publicDir, 'llms.txt');
|
||||
|
||||
function formatMarkdownLink(label: string, href: string, description: string) {
|
||||
return `- [${label}](${href}): ${description}`;
|
||||
}
|
||||
|
||||
function findDocLink(doc: (typeof docs)[number], label: string) {
|
||||
return doc.links.find((link) => link.label === label);
|
||||
}
|
||||
|
||||
function formatLlmsLinks() {
|
||||
return docs
|
||||
.map((doc) => {
|
||||
const link = findDocLink(doc, 'llms.txt');
|
||||
|
||||
if (!link) return undefined;
|
||||
|
||||
return formatMarkdownLink(doc.title, link.href, doc.description);
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function formatFullLinks() {
|
||||
return docs
|
||||
.map((doc) => {
|
||||
const link = findDocLink(doc, 'llms-full.txt');
|
||||
|
||||
if (!link) return undefined;
|
||||
|
||||
return formatMarkdownLink(`${doc.title} full`, link.href, `Полный bundle документации: ${doc.label.toLowerCase()}.`);
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const content = [
|
||||
`# ${siteTitle}`,
|
||||
'',
|
||||
`> ${siteDescription}`,
|
||||
'',
|
||||
'Этот файл является корневой картой документаций. Для работы с конкретным направлением используйте его собственный `llms.txt`.',
|
||||
'',
|
||||
'## Documentation',
|
||||
'',
|
||||
...formatLlmsLinks(),
|
||||
'',
|
||||
'## Optional',
|
||||
'',
|
||||
...formatFullLinks(),
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
fs.mkdirSync(publicDir, { recursive: true });
|
||||
fs.writeFileSync(llmsPath, content, 'utf8');
|
||||
|
||||
console.log(`Сгенерирован ${path.relative(rootDir, llmsPath)}`);
|
||||
Reference in New Issue
Block a user