aboutsummaryrefslogtreecommitdiff
path: root/website/scripts/prepare-docs.mjs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-09-01 14:48:45 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-09-01 14:48:45 +0300
commit8a6d697308162874a236c84aac0b0f8c7ac01f83 (patch)
treeed7dc7e8c6f50d66bde721fae78576aede929f66 /website/scripts/prepare-docs.mjs
parentb54a78aa510b87d1e3b56c619b72eaeb59e4a2a1 (diff)
refactor: separate website presentation from runtime
Diffstat (limited to 'website/scripts/prepare-docs.mjs')
-rw-r--r--website/scripts/prepare-docs.mjs479
1 files changed, 0 insertions, 479 deletions
diff --git a/website/scripts/prepare-docs.mjs b/website/scripts/prepare-docs.mjs
deleted file mode 100644
index 1fc8bc56..00000000
--- a/website/scripts/prepare-docs.mjs
+++ /dev/null
@@ -1,479 +0,0 @@
-import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
-import { dirname, extname, join, relative, resolve } from 'node:path';
-import { fileURLToPath } from 'node:url';
-import process from 'node:process';
-
-const websiteDir = dirname(fileURLToPath(import.meta.url));
-const repoRoot = process.env.HERDR_DOCS_REPO_ROOT
- ? resolve(process.env.HERDR_DOCS_REPO_ROOT)
- : resolve(websiteDir, '../..');
-const publicDir = resolve(repoRoot, 'website/public');
-const stableDocsDir = resolve(repoRoot, 'website/src/content/docs');
-const previewDocsDir = resolve(stableDocsDir, 'preview');
-const generatedVersionsDocsDir = resolve(stableDocsDir, '_versions');
-const versionsDir = resolve(repoRoot, 'docs/versions');
-const versionsManifestPath = resolve(versionsDir, 'manifest.json');
-const previewManifestPath = resolve(repoRoot, 'website/preview.json');
-const generatedVersionsDataPath = resolve(repoRoot, 'website/src/data/docs-versions.json');
-const stableConfigReferenceDestination = resolve(
- repoRoot,
- 'website/src/data/config-reference.json',
-);
-const previewConfigReferenceDestination = resolve(
- repoRoot,
- 'website/src/data/config-reference-preview.json',
-);
-const generatedVersionReferencesPath = resolve(
- repoRoot,
- 'website/src/data/config-reference-versions.json',
-);
-
-if (process.argv[2] === '--rewrite-preview-doc-fixture') {
- const chunks = [];
- for await (const chunk of process.stdin) chunks.push(chunk);
- process.stdout.write(
- rewritePreviewDocContent(Buffer.concat(chunks).toString('utf8'), '', {
- buildId: '2026-07-29-44b3adb12552',
- commit: '44b3adb125524ea9a55739eee3776f922f2115ad',
- }),
- );
-} else if (process.argv[2] === '--rewrite-version-doc-fixture') {
- const chunks = [];
- for await (const chunk of process.stdin) chunks.push(chunk);
- process.stdout.write(
- rewriteVersionDocContent(Buffer.concat(chunks).toString('utf8'), {
- version: process.argv[3] ?? '0.7.5',
- tag: `v${process.argv[3] ?? '0.7.5'}`,
- relativePath: 'index.mdx',
- }),
- );
-} else {
- const supported = new Set(['--docs-only', '--draft']);
- const unsupported = process.argv.slice(2).filter((argument) => !supported.has(argument));
- if (unsupported.length > 0) {
- throw new Error('usage: node website/scripts/prepare-docs.mjs [--docs-only] [--draft]');
- }
- const docsOnly = process.argv.includes('--docs-only');
- if (!docsOnly) await preparePublicAssets();
- await prepareDocs({
- draft: process.argv.includes('--draft'),
- publishAgentIndexes: !docsOnly,
- });
-}
-
-async function preparePublicAssets() {
- await rm(publicDir, { recursive: true, force: true });
- await mkdir(publicDir, { recursive: true });
-
- for (const file of [
- 'install.sh',
- 'install.ps1',
- 'install.cmd',
- 'agent-guide.md',
- 'latest.json',
- 'preview.json',
- 'robots.txt',
- 'favicon.ico',
- '_headers',
- '_redirects',
- ]) {
- const source = resolve(repoRoot, 'website', file);
- try {
- await cp(source, resolve(publicDir, file));
- } catch (error) {
- if (file !== 'preview.json' || error.code !== 'ENOENT') throw error;
- }
- }
-
- for (const directory of ['assets', 'css', 'agent-detection']) {
- await cp(resolve(repoRoot, 'website', directory), resolve(publicDir, directory), {
- recursive: true,
- });
- }
-}
-
-async function prepareDocs({ draft, publishAgentIndexes }) {
- const manifest = JSON.parse(await readFile(versionsManifestPath, 'utf8'));
- if (
- manifest.schema_version !== 1 ||
- typeof manifest.current !== 'string' ||
- !['legacy', 'snapshot'].includes(manifest.stable_source ?? 'legacy')
- ) {
- throw new Error(`${versionsManifestPath} has an unsupported schema`);
- }
- const currentEntry = manifest.versions.find((entry) => entry.version === manifest.current);
- if (!currentEntry) throw new Error(`current docs version ${manifest.current} has no snapshot`);
-
- const previewManifest = JSON.parse(await readFile(previewManifestPath, 'utf8'));
- if (!/^[0-9a-f]{40}$/.test(previewManifest.commit ?? '')) {
- throw new Error(`${previewManifestPath} must contain a full preview commit SHA`);
- }
- const previewWebsiteRoot = resolve(repoRoot, draft ? 'docs/next/website' : 'docs/preview/website');
- const previewDocsSourceDir = resolve(previewWebsiteRoot, 'src/content/docs');
- const previewConfigReferenceSource = resolve(
- previewWebsiteRoot,
- 'src/data/config-reference.json',
- );
-
- if ((manifest.stable_source ?? 'legacy') === 'snapshot') {
- await rm(stableDocsDir, { recursive: true, force: true });
- const currentSnapshotRoot = resolve(versionsDir, manifest.current, 'website');
- await copyPreparedDocs(
- resolve(currentSnapshotRoot, 'src/content/docs'),
- stableDocsDir,
- (content, relativePath) =>
- rewriteStableDocContent(content, {
- version: currentEntry.version,
- tag: currentEntry.tag,
- relativePath,
- }),
- );
- const currentReference = resolve(currentSnapshotRoot, 'src/data/config-reference.json');
- try {
- await cp(currentReference, stableConfigReferenceDestination);
- } catch (error) {
- if (error.code !== 'ENOENT') throw error;
- await rm(stableConfigReferenceDestination, { force: true });
- }
- } else {
- await rm(previewDocsDir, { recursive: true, force: true });
- await rm(generatedVersionsDocsDir, { recursive: true, force: true });
- }
-
- await copyPreparedDocs(previewDocsSourceDir, previewDocsDir, (content, relativePath) =>
- rewritePreviewDocContent(content, relativePath, {
- buildId: draft ? 'draft' : previewManifest.build_id,
- commit: draft ? 'master' : previewManifest.commit,
- }),
- );
- await cp(previewConfigReferenceSource, previewConfigReferenceDestination);
-
- const scopes = {
- stable: await collectDocsScope(
- (manifest.stable_source ?? 'legacy') === 'snapshot'
- ? resolve(versionsDir, manifest.current, 'website/src/content/docs')
- : stableDocsDir,
- new Set(['preview', '_versions']),
- ),
- preview: await collectDocsScope(previewDocsSourceDir),
- };
- const configReferences = {};
-
- for (const entry of manifest.versions) {
- const version = entry.version;
- const snapshotDocsRoot = resolve(versionsDir, version, 'website/src/content/docs');
- const destinationRoot = resolve(generatedVersionsDocsDir, version);
- await copyPreparedDocs(snapshotDocsRoot, destinationRoot, (content, relativePath) =>
- rewriteVersionDocContent(content, {
- version,
- tag: entry.tag,
- relativePath,
- }),
- );
- scopes[version] = await collectDocsScope(snapshotDocsRoot);
-
- const referencePath = resolve(
- versionsDir,
- version,
- 'website/src/data/config-reference.json',
- );
- try {
- configReferences[version] = JSON.parse(await readFile(referencePath, 'utf8'));
- } catch (error) {
- if (error.code !== 'ENOENT') throw error;
- }
- }
-
- await writeFile(
- generatedVersionsDataPath,
- `${JSON.stringify({
- ...manifest,
- preview: {
- build_id: draft ? 'draft' : previewManifest.build_id,
- commit: draft ? 'master' : previewManifest.commit,
- },
- scopes,
- }, null, 2)}\n`,
- 'utf8',
- );
- await writeFile(
- generatedVersionReferencesPath,
- `${JSON.stringify(configReferences, null, 2)}\n`,
- 'utf8',
- );
-
- if (publishAgentIndexes) {
- if (typeof currentEntry.tag !== 'string' || typeof currentEntry.source !== 'string') {
- throw new Error(`current docs version ${manifest.current} is missing its tag or source`);
- }
- const stableSourceDir = resolve(versionsDir, manifest.current, 'website/src/content/docs');
- const previewRef = draft ? 'master' : previewManifest.commit;
- const previewBuild = draft ? 'draft' : previewManifest.build_id;
- await Promise.all([
- writeAgentDocsIndex({
- output: resolve(publicDir, 'llms.txt'),
- heading: 'Herdr stable documentation index',
- summary: `Current stable release: ${manifest.current}.`,
- sourceDir: stableSourceDir,
- ref: currentEntry.tag,
- repositoryPath: currentEntry.source,
- extraSections: [
- '## Optional bundles',
- '- [Abridged documentation](https://herdr.dev/llms-small.txt)',
- '- [Complete documentation](https://herdr.dev/llms-full.txt)',
- '',
- '## Other resources',
- '- [Agent guide](https://herdr.dev/agent-guide.md): help a human understand, set up, or troubleshoot Herdr',
- '- [Preview documentation](https://herdr.dev/llms-preview.txt): use only for the preview channel',
- ],
- }),
- writeAgentDocsIndex({
- output: resolve(publicDir, 'llms-preview.txt'),
- heading: 'Herdr preview documentation index',
- summary: `Active preview build: ${previewBuild} (${previewRef.slice(0, 12)}).`,
- sourceDir: previewDocsSourceDir,
- ref: previewRef,
- repositoryPath: 'docs/next/website/src/content/docs',
- extraSections: [
- '## Other resources',
- '- [Stable documentation](https://herdr.dev/llms.txt): use unless the human is running the preview channel',
- '- [Agent guide](https://herdr.dev/agent-guide.md): help a human understand, set up, or troubleshoot Herdr',
- ],
- }),
- ]);
- }
-}
-
-async function writeAgentDocsIndex({
- output,
- heading,
- summary,
- sourceDir,
- ref,
- repositoryPath,
- extraSections,
-}) {
- const pages = await collectAgentDocs(sourceDir);
- const rawRoot = `https://raw.githubusercontent.com/herdrdev/herdr/${ref}`;
- const rawBase = `${rawRoot}/${repositoryPath}`;
- const configDataPath = repositoryPath.replace(
- /\/content\/docs$/,
- '/data/config-reference.json',
- );
- if (configDataPath === repositoryPath) {
- throw new Error(`cannot derive config reference path from ${repositoryPath}`);
- }
- const configDataUrl = `${rawRoot}/${configDataPath}`;
-
- const lines = [
- `# ${heading}`,
- '',
- `> ${summary}`,
- '',
- 'Open only the pages relevant to the current task. Links return raw source files from the exact documented revision. For another topic, return to this index instead of following `/docs/` links inside a page, which serve human-facing HTML.',
- '',
- '## Documentation',
- '',
- ...pages.map(({ title, description, path }) => {
- const url = /^config-reference\.mdx?$/.test(path) ? configDataUrl : `${rawBase}/${path}`;
- return `- [${title}](${url})${description ? `: ${description}` : ''}`;
- }),
- '',
- '## Using the config reference',
- '',
- 'The Config reference link above is a large structured JSON file. When command tools are available, fetch and filter it by exact key instead of loading the whole file:',
- '',
- '```sh',
- `curl -fsSL '${configDataUrl}' | jq --arg key 'ui.sidebar_width' \\`,
- " '.sections[].keys[] | select(.key == $key)'",
- '```',
- '',
- ...extraSections,
- '',
- ];
- await writeFile(output, lines.join('\n'), 'utf8');
-}
-
-async function collectAgentDocs(sourceDir) {
- const pages = [];
-
- async function walk(directory, prefix = '') {
- for (const entry of await readdir(directory, { withFileTypes: true })) {
- if (!prefix && entry.isDirectory() && ['ja', 'zh-cn'].includes(entry.name)) continue;
- const path = join(directory, entry.name);
- const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
- if (entry.isDirectory()) {
- await walk(path, relativePath);
- continue;
- }
- if (!entry.isFile() || !['.md', '.mdx'].includes(extname(entry.name).toLowerCase())) {
- continue;
- }
- const content = await readFile(path, 'utf8');
- const title = frontmatterValue(content, 'title');
- if (!title) throw new Error(`${path} is missing a frontmatter title`);
- pages.push({
- title,
- description: frontmatterValue(content, 'description'),
- path: relativePath,
- });
- }
- }
-
- await walk(sourceDir);
- pages.sort((a, b) => {
- if (a.path === 'index.mdx' || a.path === 'index.md') return -1;
- if (b.path === 'index.mdx' || b.path === 'index.md') return 1;
- return a.title.localeCompare(b.title, 'en');
- });
- return pages;
-}
-
-function frontmatterValue(content, key) {
- const frontmatter = content.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
- const value = frontmatter?.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1]?.trim();
- if (!value) return undefined;
- const quote = value[0];
- return (quote === '"' || quote === "'") && value.endsWith(quote)
- ? value.slice(1, -1)
- : value;
-}
-
-async function copyPreparedDocs(sourceDir, destinationDir, rewrite, pathPrefix = '') {
- await mkdir(destinationDir, { recursive: true });
- for (const entry of await readdir(sourceDir, { withFileTypes: true })) {
- const source = join(sourceDir, entry.name);
- const destination = join(destinationDir, entry.name);
- const relativePath = pathPrefix ? `${pathPrefix}/${entry.name}` : entry.name;
- if (entry.isDirectory()) {
- await copyPreparedDocs(source, destination, rewrite, relativePath);
- continue;
- }
- if (!entry.isFile()) continue;
-
- if (!['.md', '.mdx'].includes(extname(entry.name).toLowerCase())) {
- await cp(source, destination);
- continue;
- }
- const content = await readFile(source, 'utf8');
- await writeFile(destination, rewrite(content, relativePath), 'utf8');
- }
-}
-
-async function collectDocsScope(sourceDir, excludedDirectories = new Set()) {
- const locales = { root: [], ja: [], 'zh-cn': [] };
-
- async function walk(directory, prefix = '') {
- for (const entry of await readdir(directory, { withFileTypes: true })) {
- if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue;
- const path = join(directory, entry.name);
- const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
- if (entry.isDirectory()) {
- await walk(path, relativePath);
- continue;
- }
- if (!entry.isFile() || !['.md', '.mdx'].includes(extname(entry.name))) continue;
-
- const segments = relativePath.replace(/\.(md|mdx)$/i, '').split('/');
- const locale = segments[0] === 'ja' || segments[0] === 'zh-cn' ? segments.shift() : 'root';
- const page = segments.join('/').replace(/(^|\/)index$/, '').replace(/\/$/, '');
- locales[locale].push(page);
- }
- }
-
- await walk(sourceDir);
- for (const pages of Object.values(locales)) pages.sort();
- for (const locale of ['ja', 'zh-cn']) {
- if (locales[locale].length === 0) locales[locale] = [...locales.root];
- }
- return { locales };
-}
-
-export function rewritePreviewDocContent(
- content,
- relativePath = '',
- { buildId, commit } = {
- buildId: 'preview',
- commit: 'master',
- },
-) {
- const taggedContent = rewriteRepositoryLinks(
- content.replaceAll('/docs/', '/docs/preview/'),
- commit,
- );
- const rewritten = rewriteRelativeDocPaths(taggedContent, 1);
- const withSourceLink = setGeneratedEditUrl(
- rewritten,
- `https://github.com/herdrdev/herdr/blob/${commit}/docs/next/website/src/content/docs/${relativePath}`,
- );
- return insertPreviewNotice(withSourceLink, relativePath, { buildId, commit });
-}
-
-export function rewriteStableDocContent(content, { version, tag, relativePath }) {
- const taggedContent = rewriteRepositoryLinks(content, tag);
- return setGeneratedEditUrl(taggedContent, versionedDocSourceUrl(version, relativePath));
-}
-
-export function rewriteVersionDocContent(content, { version, tag, relativePath }) {
- const taggedContent = rewriteRepositoryLinks(
- content.replaceAll('/docs/', `/docs/${version}/`),
- tag,
- );
- const rewritten = rewriteRelativeDocPaths(taggedContent, 2);
- return setGeneratedEditUrl(rewritten, versionedDocSourceUrl(version, relativePath));
-}
-
-function versionedDocSourceUrl(version, relativePath) {
- return `https://github.com/herdrdev/herdr/blob/master/docs/versions/${version}/website/src/content/docs/${relativePath}`;
-}
-
-function rewriteRepositoryLinks(content, ref) {
- return content
- .replaceAll(
- 'https://github.com/herdrdev/herdr/blob/master/',
- `https://github.com/herdrdev/herdr/blob/${ref}/`,
- )
- .replaceAll(
- 'https://raw.githubusercontent.com/herdrdev/herdr/master/',
- `https://raw.githubusercontent.com/herdrdev/herdr/${ref}/`,
- );
-}
-
-function rewriteRelativeDocPaths(content, extraDepth) {
- const parents = '../'.repeat(extraDepth);
- return content
- .replace(/((?:\.\.\/)+)(?=public\/)/g, `$1${parents}`)
- .replace(/^(import .*from\s+['"])(?=(?:\.\.\/)+components\/)/gm, `$1${parents}`);
-}
-
-function setGeneratedEditUrl(content, editUrl) {
- if (!content.startsWith('---\n') || /^editUrl:/m.test(content)) return content;
- return content.replace(/^---\n/, `---\neditUrl: ${editUrl}\n`);
-}
-
-function insertPreviewNotice(content, relativePath, { buildId, commit }) {
- const source = commit === 'master' ? '`master` draft' : `[${commit.slice(0, 12)}](https://github.com/herdrdev/herdr/commit/${commit})`;
- const notice = [
- `> Preview build \`${buildId}\`, published from ${source}. Stable docs remain at [/docs/](/docs/).`,
- '',
- '',
- ].join('\n');
- const indexPrefix =
- relativePath === 'index.mdx'
- ? content.replace('title: Herdr documentation', 'title: Herdr preview documentation')
- : content;
- const frontmatter = indexPrefix.match(/^---\n[\s\S]*?\n---\n/);
- if (!frontmatter) {
- return insertNoticeAfterImports(indexPrefix, notice);
- }
- const body = indexPrefix.slice(frontmatter[0].length);
- return `${frontmatter[0]}\n${insertNoticeAfterImports(body, notice)}`;
-}
-
-function insertNoticeAfterImports(content, notice) {
- const imports = content.match(/^(\s*import .+?;\n)+\s*/);
- if (!imports) {
- return `${notice}${content}`;
- }
- return `${imports[0]}${notice}${content.slice(imports[0].length)}`;
-}