From 4c77e309b2934e42ba863d2942df030dcca57da8 Mon Sep 17 00:00:00 2001 From: PatrickJS Date: Sun, 14 Jun 2026 12:56:09 -0700 Subject: [PATCH] Unify GitHub Pages site --- .async-pipeline/tasks.lock.json | 2 +- .github/async-pipeline.lock.json | 2 +- pipeline.js | 6 +- scripts/build-pages.js | 230 +++++++++++++++++++++++++------ 4 files changed, 190 insertions(+), 50 deletions(-) diff --git a/.async-pipeline/tasks.lock.json b/.async-pipeline/tasks.lock.json index 3825842..afb2c7e 100644 --- a/.async-pipeline/tasks.lock.json +++ b/.async-pipeline/tasks.lock.json @@ -335,5 +335,5 @@ } ], "hash": "sha256:c0794418dd6a745cb8892a404c68926b88a3a79b9d522281edc0171aa88f3e2a", - "generatedAt": "2026-06-14T17:30:22.259Z" + "generatedAt": "2026-06-14T19:47:18.657Z" } diff --git a/.github/async-pipeline.lock.json b/.github/async-pipeline.lock.json index 85cd74b..230dfbd 100644 --- a/.github/async-pipeline.lock.json +++ b/.github/async-pipeline.lock.json @@ -4,7 +4,7 @@ "config": "pipeline.js", "workflow": ".github/workflows/async-pipeline.yml", "hash": "sha256:7c8a1dde68f81793d9c0cc4b1cbfd1ae3e6ff63f95429b50b103a4fad9b189f0", - "generatedAt": "2026-06-14T17:30:22.256Z", + "generatedAt": "2026-06-14T19:47:18.653Z", "triggers": { "pull_request": {}, "push": { diff --git a/pipeline.js b/pipeline.js index 51f6a3f..d2311b5 100644 --- a/pipeline.js +++ b/pipeline.js @@ -119,9 +119,9 @@ export default definePipeline({ ] }), "docs.site": task({ - description: "Build the README-backed GitHub Pages site.", - inputs: ["README.md", "scripts/build-pages.js"], - outputs: [".async/pages/index.html"], + description: "Build the standardized GitHub Pages documentation site.", + inputs: ["README.md", "templates/**/*.md", "scripts/build-pages.js"], + outputs: [".async/pages/**"], cache: true, // TODO(@async/pipeline): replace this fallback when pipeline provides a first-class README-to-Pages builder. run: sh`node scripts/build-pages.js` diff --git a/scripts/build-pages.js b/scripts/build-pages.js index 1404739..1a091e8 100644 --- a/scripts/build-pages.js +++ b/scripts/build-pages.js @@ -1,62 +1,202 @@ -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; +#!/usr/bin/env node +import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { basename, dirname, join, relative } from "node:path"; -const root = process.cwd(); -const readmePath = resolve(root, "README.md"); -const outputPath = resolve(root, ".async/pages/index.html"); +const site = { + "title": "@async/dispatch", + "repo": "dispatch", + "pipelineFile": "pipeline.js", + "stage": "Beta", + "description": "Installable local coordination CLI for goal-first async chat work, plan boards, runtime ledgers, workers, and receipts.", + "lead": "Keep broad async work explicit before it turns into repo edits, chats, or release handoffs.", + "quickstart": "pnpm add --global @async/dispatch\nasync-dispatch help", + "docsRoots": [ + "templates" + ] +}; +const outDir = ".async/pages"; +const asyncProjects = [ + ["@async/db", "https://async.github.io/db/", "Data workflow"], + ["@async/web", "https://async.github.io/web/", "Web runtime"], + ["@async/pipeline", "https://async.github.io/pipeline/", "Pipeline workflows"], + ["@async/dispatch", "https://async.github.io/dispatch/", "Goal-first coordination"], + ["@async/auto-git", "https://async.github.io/auto-git/", "Git handoffs"], + ["@async/api-contract", "https://async.github.io/api-contract/", "API ledgers"], + ["@async/claims", "https://async.github.io/claims/", "Doc claim checks"] +]; -const markdown = await readFile(readmePath, "utf8"); -const escaped = markdown - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">"); +await rm(outDir, { recursive: true, force: true }); +await mkdir(outDir, { recursive: true }); -const html = ` +const readme = await readFile("README.md", "utf8"); +const docs = await collectDocs(site.docsRoots); +for (const doc of docs) { + const markdown = await readFile(doc.path, "utf8"); + const htmlPath = join(outDir, doc.href); + await mkdir(dirname(htmlPath), { recursive: true }); + await writeFile(htmlPath, layout({ + title: doc.title, + body: renderMarkdown(markdown), + rootPrefix: "../".repeat(doc.href.split("/").length - 1) + })); +} + +await writeFile(join(outDir, "index.html"), layout({ + title: site.title, + body: home(readme, docs), + rootPrefix: "" +})); + +function home(readme, docs) { + const guideLinks = docs.length ? docs.map((doc) => + `${escapeHtml(doc.title)}${escapeHtml(doc.source)}` + ).join("\n") : "

No guide pages are published for this repo yet.

"; + const related = asyncProjects + .filter(([name]) => name !== site.title) + .map(([name, url, label]) => `${name}${label}`) + .join("\n"); + return ` +
+

${escapeHtml(site.stage)} / Async

+

${escapeHtml(site.title)}

+

${renderInline(site.description)}

+

${renderInline(site.lead)}

+
+ GitHub + npm +
+
+
+

Start

+
${escapeHtml(site.quickstart)}
+
+
+

Guides

+
${guideLinks}
+
+
+

Related Async Projects

+ +
+
+

README

+
${renderMarkdown(readme)}
+
+ `; +} + +function layout({ title, body, rootPrefix }) { + const nav = asyncProjects.map(([name, url]) => + `${name.replace("@async/", "")}` + ).join("\n"); + return ` - @async/dispatch + ${escapeHtml(title)} + + + +
${escapeHtml(site.title)}
${body}
Built by pnpm run pipeline:pages. Workflow source: ${escapeHtml(site.pipelineFile ?? "pipeline.ts")}.
+ + +`; +} + +async function collectDocs(roots) { + const docs = []; + for (const root of roots) { + try { await stat(root); } catch { continue; } + await walk(root, docs); + } + return docs.sort((a, b) => a.title.localeCompare(b.title)); +} - body { - margin: 0; +async function walk(dir, docs) { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + if (["node_modules", ".git", ".async", "dist", "_site"].includes(entry.name)) continue; + await walk(path, docs); + continue; } + if (!entry.isFile() || !entry.name.endsWith(".md")) continue; + const source = await readFile(path, "utf8"); + const rel = relative(process.cwd(), path).replaceAll("\\", "/"); + const href = rel.replace(/\.md$/, ".html"); + docs.push({ path, source: rel, href, title: firstHeading(source) || basename(path, ".md") }); + } +} - main { - max-width: 920px; - margin: 0 auto; - padding: 32px 20px 56px; +function renderMarkdown(source) { + const lines = source.split(/\r?\n/); + const html = []; + let list = ""; + let code = null; + let table = []; + const closeList = () => { if (list) { html.push(``); list = ""; } }; + const closeTable = () => { if (table.length) { html.push(renderTable(table)); table = []; } }; + for (const line of lines) { + if (line.startsWith("```")) { + closeList(); closeTable(); + if (code) { html.push(`
${escapeHtml(code.join("\n"))}
`); code = null; } else { code = []; } + continue; } + if (code) { code.push(line); continue; } + if (line.startsWith("|")) { closeList(); table.push(line); continue; } + closeTable(); + if (/^###\s+/.test(line)) { closeList(); html.push(`

${renderInline(line.slice(4))}

`); continue; } + if (/^##\s+/.test(line)) { closeList(); html.push(`

${renderInline(line.slice(3))}

`); continue; } + if (/^#\s+/.test(line)) { closeList(); html.push(`

${renderInline(line.slice(2))}

`); continue; } + if (/^-\s+/.test(line)) { if (list !== "ul") { closeList(); list = "ul"; html.push("