Skip to content

Commit ec4d7f0

Browse files
committed
fix(site): wire docs into Starlight with a prebuild sync script
The v0.3.0 docs site was broken — site/src/content/docs/ contained only index.mdx, so every sidebar link (why-skdd, configuration, skill-colony, colony/discovery, integrations/*, spec/*) rendered as Starlight's 404. Rather than duplicate the canonical docs under site/, add a small sync-docs.mjs that copies docs/**/*.md and colony/*.md into site/src/content/docs/ before each astro dev/build run. Frontmatter is injected automatically (title from first h1, description from first blockquote), and relative .md links are rewritten to Starlight slugs. The generated files are gitignored so the canonical docs stay the single source of truth. Includes a thin wrapper page at spec/colony-v1.md that inlines docs/spec/colony-v1.json so the sidebar link doesn't 404. Local build: 23 pages rendered (up from 2).
1 parent 404304c commit ec4d7f0

3 files changed

Lines changed: 163 additions & 0 deletions

File tree

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ build/
1414
*.tsbuildinfo
1515
.astro/
1616

17+
# Starlight generated content (authored copies live in docs/ and colony/)
18+
site/src/content/docs/why-skdd.md
19+
site/src/content/docs/configuration.md
20+
site/src/content/docs/skill-colony.md
21+
site/src/content/docs/forging-skills.md
22+
site/src/content/docs/specification-alignment.md
23+
site/src/content/docs/schemastore-submission.md
24+
site/src/content/docs/colony/
25+
site/src/content/docs/integrations/
26+
site/src/content/docs/spec/
27+
1728
# Environment & secrets
1829
.env
1930
.env.*

site/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
"private": true,
66
"description": "Starlight-powered documentation site for Skills-Driven Development.",
77
"scripts": {
8+
"sync": "node scripts/sync-docs.mjs",
9+
"predev": "pnpm sync",
810
"dev": "astro dev",
911
"start": "astro dev",
12+
"prebuild": "pnpm sync",
1013
"build": "astro build",
1114
"preview": "astro preview",
1215
"astro": "astro"

site/scripts/sync-docs.mjs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#!/usr/bin/env node
2+
// Sync repo docs into Starlight's content collection.
3+
//
4+
// Starlight wants Markdown under site/src/content/docs/. Our canonical docs
5+
// live at docs/**/*.md and colony/**/*.md so they stay co-located with the
6+
// code. Rather than duplicating, this script copies the canonical files into
7+
// the content dir before `astro build` runs. Never edit site/src/content/docs
8+
// by hand (except index.mdx, which is a Starlight landing page).
9+
10+
import { readFile, writeFile, mkdir, rm, readdir, stat } from "node:fs/promises";
11+
import { dirname, join, resolve, relative } from "node:path";
12+
import { fileURLToPath } from "node:url";
13+
14+
const scriptDir = dirname(fileURLToPath(import.meta.url));
15+
const siteDir = resolve(scriptDir, "..");
16+
const repoRoot = resolve(siteDir, "..");
17+
const contentDir = resolve(siteDir, "src/content/docs");
18+
19+
// Map: { src: path relative to repoRoot, dest: path relative to contentDir }
20+
// Keep paths aligned with site/astro.config.mjs sidebar entries.
21+
const mappings = [
22+
{ src: "docs/why-skdd.md", dest: "why-skdd.md" },
23+
{ src: "docs/configuration.md", dest: "configuration.md" },
24+
{ src: "docs/skill-colony.md", dest: "skill-colony.md" },
25+
{ src: "docs/forging-skills.md", dest: "forging-skills.md" },
26+
{ src: "docs/specification-alignment.md", dest: "specification-alignment.md" },
27+
{ src: "docs/schemastore-submission.md", dest: "schemastore-submission.md" },
28+
{ src: "colony/discovery.md", dest: "colony/discovery.md" },
29+
{ src: "colony/evolution.md", dest: "colony/evolution.md" },
30+
{ src: "docs/spec/agent-skills-v1.md", dest: "spec/agent-skills-v1.md" },
31+
// Integrations — autogenerated by the sidebar; all markdown files that
32+
// aren't README.md (Starlight wants index.md, not README.md).
33+
{ src: "docs/integrations/claude-code.md", dest: "integrations/claude-code.md" },
34+
{ src: "docs/integrations/codex.md", dest: "integrations/codex.md" },
35+
{ src: "docs/integrations/cursor.md", dest: "integrations/cursor.md" },
36+
{ src: "docs/integrations/github-copilot.md", dest: "integrations/github-copilot.md" },
37+
{ src: "docs/integrations/gemini-cli.md", dest: "integrations/gemini-cli.md" },
38+
{ src: "docs/integrations/opencode.md", dest: "integrations/opencode.md" },
39+
{ src: "docs/integrations/goose.md", dest: "integrations/goose.md" },
40+
{ src: "docs/integrations/amp.md", dest: "integrations/amp.md" },
41+
{ src: "docs/integrations/vscode.md", dest: "integrations/vscode.md" },
42+
{ src: "docs/integrations/junie.md", dest: "integrations/junie.md" },
43+
{ src: "docs/integrations/roo-code.md", dest: "integrations/roo-code.md" },
44+
];
45+
46+
function extractTitle(body) {
47+
const match = body.match(/^#\s+(.+?)\s*$/m);
48+
return match ? match[1].trim() : null;
49+
}
50+
51+
function extractDescription(body) {
52+
const match = body.match(/^>\s+(.+?)\s*$/m);
53+
if (!match) return null;
54+
return match[1].replace(/\*+/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").trim();
55+
}
56+
57+
function stripLeadingH1(body) {
58+
return body.replace(/^#\s+.+?\s*\n+/, "");
59+
}
60+
61+
function rewriteRelativeLinks(body) {
62+
// Internal .md links should resolve as Starlight slugs. Drop the .md
63+
// extension so Starlight's routing picks them up.
64+
return body.replace(
65+
/(\]\()([^)]+?)\.md(#[^)]*)?(\))/g,
66+
(_, pre, path, hash = "", post) => `${pre}${path}/${hash}${post}`,
67+
);
68+
}
69+
70+
function ensureFrontmatter(body, fallbackTitle) {
71+
if (body.startsWith("---\n")) return body;
72+
const title = extractTitle(body) || fallbackTitle;
73+
const description = extractDescription(body);
74+
const fm = ["---", `title: ${JSON.stringify(title)}`];
75+
if (description) fm.push(`description: ${JSON.stringify(description)}`);
76+
fm.push("---", "");
77+
return `${fm.join("\n")}\n${stripLeadingH1(body)}`;
78+
}
79+
80+
async function cleanStaleGenerated() {
81+
// Remove everything under contentDir *except* index.mdx so old generated
82+
// files don't linger after a mapping changes.
83+
const entries = await readdir(contentDir);
84+
for (const entry of entries) {
85+
if (entry === "index.mdx") continue;
86+
await rm(join(contentDir, entry), { recursive: true, force: true });
87+
}
88+
}
89+
90+
async function writeColonySchemaPage() {
91+
// The sidebar links /spec/colony-v1/ which isn't a markdown file —
92+
// it's a JSON schema. Generate a thin wrapper page so the link works.
93+
const schemaPath = resolve(repoRoot, "docs/spec/colony-v1.json");
94+
const schema = await readFile(schemaPath, "utf8");
95+
const body = `---
96+
title: "Colony v1 schema"
97+
description: "JSON Schema for .colony.json (the manifest every SkDD colony ships)."
98+
---
99+
100+
The \`.colony.json\` file at a colony root conforms to this schema. It's the
101+
machine-readable entry point for marketplaces, indexers, and \`skdd doctor\`.
102+
103+
See [\`schemastore-submission\`](../schemastore-submission/) for the draft PR
104+
body we'll use to register this schema with SchemaStore.org (so VS Code and
105+
JetBrains auto-complete it for every user).
106+
107+
\`\`\`json
108+
${schema.trimEnd()}
109+
\`\`\`
110+
`;
111+
const dest = resolve(contentDir, "spec/colony-v1.md");
112+
await mkdir(dirname(dest), { recursive: true });
113+
await writeFile(dest, body);
114+
}
115+
116+
async function syncOne({ src, dest }) {
117+
const srcPath = resolve(repoRoot, src);
118+
const destPath = resolve(contentDir, dest);
119+
let body;
120+
try {
121+
body = await readFile(srcPath, "utf8");
122+
} catch (err) {
123+
if (err.code === "ENOENT") {
124+
console.warn(`skip (missing): ${src}`);
125+
return;
126+
}
127+
throw err;
128+
}
129+
const fallbackTitle = dest.replace(/\.md$/, "").replace(/\//g, " · ");
130+
const rewritten = rewriteRelativeLinks(body);
131+
const withFm = ensureFrontmatter(rewritten, fallbackTitle);
132+
await mkdir(dirname(destPath), { recursive: true });
133+
await writeFile(destPath, withFm);
134+
console.log(`wrote ${relative(repoRoot, destPath)}`);
135+
}
136+
137+
async function main() {
138+
await cleanStaleGenerated();
139+
for (const mapping of mappings) {
140+
await syncOne(mapping);
141+
}
142+
await writeColonySchemaPage();
143+
console.log(`\n✓ synced ${mappings.length + 1} pages into ${relative(repoRoot, contentDir)}`);
144+
}
145+
146+
main().catch((err) => {
147+
console.error(err);
148+
process.exit(1);
149+
});

0 commit comments

Comments
 (0)