atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

at dfd015c4e12850beb5b43c8b301c91bc8ef0f877 223 lines 6.9 kB view raw
1import { 2 readdir, 3 readFile, 4 mkdir, 5 rm, 6 cp, 7 writeFile, 8 access, 9} from "node:fs/promises"; 10import path from "node:path"; 11import { execFile } from "node:child_process"; 12import { promisify } from "node:util"; 13 14const repoRoot = path.resolve(new URL("..", import.meta.url).pathname); 15const docsDir = path.join(repoRoot, "docs"); 16const devlogDir = path.join(repoRoot, "devlog"); 17const siteSrcDir = path.join(repoRoot, "site"); 18const outDir = path.join(repoRoot, "site-out"); 19const outDocsDir = path.join(outDir, "docs"); 20 21const execFileAsync = promisify(execFile); 22 23async function exists(filePath) { 24 try { 25 await access(filePath); 26 return true; 27 } catch { 28 return false; 29 } 30} 31 32function isMarkdown(filePath) { 33 return filePath.toLowerCase().endsWith(".md"); 34} 35 36async function listMarkdownFiles(dir, prefix = "") { 37 const entries = await readdir(dir, { withFileTypes: true }); 38 const out = []; 39 for (const e of entries) { 40 if (e.name.startsWith(".")) continue; 41 const rel = path.join(prefix, e.name); 42 const abs = path.join(dir, e.name); 43 if (e.isDirectory()) { 44 out.push(...(await listMarkdownFiles(abs, rel))); 45 } else if (e.isFile() && isMarkdown(e.name)) { 46 out.push(rel.replaceAll(path.sep, "/")); 47 } 48 } 49 return out.sort((a, b) => a.localeCompare(b)); 50} 51 52function titleFromMarkdown(md, fallback) { 53 const lines = md.split(/\r?\n/); 54 for (const line of lines) { 55 const m = /^#\s+(.+)\s*$/.exec(line); 56 if (m) return m[1].trim(); 57 } 58 return fallback.replace(/\.md$/i, ""); 59} 60 61function normalizeTitle(title) { 62 let t = String(title || "").trim(); 63 // Strip markdown links: [text](url) -> text 64 t = t.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); 65 // If pages follow a "zat - ..." style, drop the redundant prefix in the nav. 66 t = t.replace(/^zat\s*-\s*/i, ""); 67 // Cheaply capitalize (keeps the rest as-authored). 68 if (t.length) t = t[0].toUpperCase() + t.slice(1); 69 return t; 70} 71 72async function getBuildId() { 73 try { 74 const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], { 75 cwd: repoRoot, 76 }); 77 const full = String(stdout || "").trim(); 78 if (full) return full.slice(0, 12); 79 } catch { 80 // ignore 81 } 82 return String(Date.now()); 83} 84 85async function main() { 86 await rm(outDir, { recursive: true, force: true }); 87 await mkdir(outDir, { recursive: true }); 88 89 // Copy static site shell 90 await cp(siteSrcDir, outDir, { recursive: true }); 91 92 // Cache-bust immutable assets on Wisp by appending a per-commit query string. 93 const buildId = await getBuildId(); 94 const outIndex = path.join(outDir, "index.html"); 95 if (await exists(outIndex)) { 96 let html = await readFile(outIndex, "utf8"); 97 html = html.replaceAll('href="./style.css"', `href="./style.css?v=${buildId}"`); 98 html = html.replaceAll( 99 'src="./vendor/marked.min.js"', 100 `src="./vendor/marked.min.js?v=${buildId}"`, 101 ); 102 html = html.replaceAll( 103 'src="./app.js"', 104 `src="./app.js?v=${buildId}"`, 105 ); 106 html = html.replaceAll( 107 'href="./favicon.svg"', 108 `href="./favicon.svg?v=${buildId}"`, 109 ); 110 await writeFile(outIndex, html, "utf8"); 111 } 112 113 // Copy docs 114 await mkdir(outDocsDir, { recursive: true }); 115 116 const pages = []; 117 118 // Prefer an explicit docs homepage if present; otherwise use repo README as index. 119 const docsIndex = path.join(docsDir, "index.md"); 120 if (!(await exists(docsIndex))) { 121 const readme = path.join(repoRoot, "README.md"); 122 if (await exists(readme)) { 123 let md = await readFile(readme, "utf8"); 124 // Strip docs/ prefix from links since we're now inside the docs context. 125 md = md.replace(/\]\(docs\//g, "]("); 126 await writeFile(path.join(outDocsDir, "index.md"), md, "utf8"); 127 pages.push({ 128 path: "index.md", 129 title: normalizeTitle(titleFromMarkdown(md, "index.md")), 130 }); 131 } 132 } 133 134 const changelog = path.join(repoRoot, "CHANGELOG.md"); 135 const docsChangelog = path.join(docsDir, "changelog.md"); 136 if ((await exists(changelog)) && !(await exists(docsChangelog))) { 137 const md = await readFile(changelog, "utf8"); 138 await writeFile(path.join(outDocsDir, "changelog.md"), md, "utf8"); 139 pages.push({ 140 path: "changelog.md", 141 title: normalizeTitle(titleFromMarkdown(md, "changelog.md")), 142 }); 143 } 144 145 const mdFiles = (await exists(docsDir)) ? await listMarkdownFiles(docsDir) : []; 146 147 // Copy all markdown under docs/ (including archives), but only include non-archive 148 // paths in the sidebar manifest. 149 for (const rel of mdFiles) { 150 const src = path.join(docsDir, rel); 151 const dst = path.join(outDocsDir, rel); 152 await mkdir(path.dirname(dst), { recursive: true }); 153 await cp(src, dst); 154 155 const md = await readFile(src, "utf8"); 156 if (!rel.startsWith("archive/")) { 157 pages.push({ path: rel, title: normalizeTitle(titleFromMarkdown(md, rel)) }); 158 } 159 } 160 161 // Copy devlog files to docs/devlog/ and generate an index 162 const devlogFiles = (await exists(devlogDir)) ? await listMarkdownFiles(devlogDir) : []; 163 const devlogEntries = []; 164 165 for (const rel of devlogFiles) { 166 const src = path.join(devlogDir, rel); 167 const dst = path.join(outDocsDir, "devlog", rel); 168 await mkdir(path.dirname(dst), { recursive: true }); 169 await cp(src, dst); 170 171 // create short-name alias (e.g. 001.md for 001-self-publishing-docs.md) 172 // so ATProto published paths like /devlog/001 resolve correctly 173 const base = path.basename(rel, ".md"); 174 const shortName = base.replace(/-.*$/, ""); 175 if (shortName !== base) { 176 await cp(src, path.join(outDocsDir, "devlog", shortName + ".md")); 177 } 178 179 const md = await readFile(src, "utf8"); 180 devlogEntries.push({ 181 path: `devlog/${rel}`, 182 title: titleFromMarkdown(md, rel), 183 }); 184 } 185 186 // Generate devlog index listing all entries (newest first by filename) 187 if (devlogEntries.length > 0) { 188 devlogEntries.sort((a, b) => b.path.localeCompare(a.path)); 189 const indexMd = [ 190 "# devlog", 191 "", 192 ...devlogEntries.map((e) => `- [${e.title}](${e.path})`), 193 "", 194 ].join("\n"); 195 await writeFile(path.join(outDocsDir, "devlog", "index.md"), indexMd, "utf8"); 196 } 197 198 // Stable nav order: README homepage, then roadmap, then changelog, then the rest. 199 pages.sort((a, b) => { 200 const order = (p) => { 201 if (p === "index.md") return 0; 202 if (p === "roadmap.md") return 1; 203 if (p === "changelog.md") return 2; 204 return 3; 205 }; 206 const ao = order(a.path); 207 const bo = order(b.path); 208 if (ao !== bo) return ao - bo; 209 return a.title.localeCompare(b.title); 210 }); 211 212 await writeFile( 213 path.join(outDir, "manifest.json"), 214 JSON.stringify({ pages }, null, 2) + "\n", 215 "utf8", 216 ); 217 218 process.stdout.write( 219 `Built Wisp docs site: ${pages.length} markdown file(s) -> ${outDir}\n`, 220 ); 221} 222 223await main();