feat/wiki-astro #214

Merged
clawdie merged 19 commits from feat/wiki-astro into main 2026-06-26 14:16:50 +02:00
7 changed files with 298 additions and 0 deletions
Showing only changes of commit f704abc782 - Show all commits

3
astro/wiki/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules/
dist/
.astro/

View file

@ -0,0 +1,12 @@
import { defineConfig } from "astro/config";
const site = process.env.ASTRO_SITE_URL || "https://wiki.clawdie.si";
const outDir = process.env.ASTRO_OUT_DIR || "./dist";
// https://astro.build/config
export default defineConfig({
site,
outDir,
output: "static",
trailingSlash: "always",
});

14
astro/wiki/package.json Normal file
View file

@ -0,0 +1,14 @@
{
"name": "clawdie-wiki",
"private": true,
"version": "0.12.0",
"type": "module",
"scripts": {
"dev": "astro dev --host 0.0.0.0",
"build": "astro build",
"preview": "astro preview --host 0.0.0.0"
},
"dependencies": {
"astro": "^5.16.11"
}
}

View file

@ -0,0 +1,131 @@
---
import fs from "node:fs";
import path from "node:path";
const WIKI_DIR = path.resolve("../../docs/wiki");
const EXCLUDE = [".git", "sl", "index.md"];
export function getStaticPaths() {
function walk(dir, prefix = "") {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const slugs = [];
for (const e of entries) {
if (e.name.startsWith(".") || EXCLUDE.includes(e.name)) continue;
const full = path.join(dir, e.name);
if (e.isDirectory()) {
slugs.push(...walk(full, prefix ? `${prefix}/${e.name}` : e.name));
} else if (e.name.endsWith(".md")) {
const rel = prefix ? `${prefix}/${e.name}` : e.name;
slugs.push({ params: { slug: rel.replace(/\.md$/, "") } });
}
}
return slugs;
}
return walk(WIKI_DIR);
}
const { slug } = Astro.params;
const filePath = path.join(WIKI_DIR, `${slug}.md`);
if (!fs.existsSync(filePath)) {
return new Response("Not found", { status: 404 });
}
const raw = fs.readFileSync(filePath, "utf-8");
// Parse frontmatter
let content = raw;
let frontmatter = {};
if (raw.startsWith("---")) {
const end = raw.indexOf("---", 3);
if (end !== -1) {
const fm = raw.slice(3, end);
for (const line of fm.split("\n")) {
const m = line.match(/^(\w+):\s*(.+)$/);
if (m) frontmatter[m[1]] = m[2].replace(/^["']|["']$/g, "");
}
content = raw.slice(end + 3).trim();
}
}
// Resolve relative wiki links [label](./page.md) → [label](/page/)
const resolveLinks = (md) =>
md.replace(/\]\(\.\/([^)]+)\.md\)/g, "](/$1/)")
.replace(/\]\(\.\.\/([^)]+)\.md\)/g, "](/$1/)");
content = resolveLinks(content);
// Render fenced code blocks
const renderCode = (md) =>
md.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
return `<pre><code${lang ? ` class="language-${lang}"` : ""}>${code.trim()}</code></pre>`;
});
content = renderCode(content);
// Render tables
const renderTables = (md) => {
return md.replace(/\|(.+)\|\n\|[-| ]+\|\n((?:\|.+\|\n?)*)/gm, (_, header, rows) => {
const hcells = header.split("|").map(c => c.trim()).filter(Boolean);
const thead = `<tr>${hcells.map(c => `<th>${c}</th>`).join("")}</tr>`;
const tbody = rows.trim().split("\n").map(row => {
const cells = row.split("|").map(c => c.trim()).filter(Boolean);
return `<tr>${cells.map(c => `<td>${c}</td>`).join("")}</tr>`;
}).join("");
return `<table><thead>${thead}</thead><tbody>${tbody}</tbody></table>`;
});
};
content = renderTables(content);
// Render inline code, bold, italic, links, headings, lists
content = content
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
.replace(/^# (.+)$/gm, "<h1>$1</h1>")
.replace(/^- (.+)$/gm, "<li>$1</li>")
.replace(/((?:<li>.*<\/li>\n?)+)/g, "<ul>$1</ul>")
.replace(/\n\n/g, "</p><p>")
.replace(/^(.+)$/gm, (line) => {
if (line.startsWith("<")) return line;
return line;
});
const title = frontmatter.title || slug;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{(title)} — Colibri Wiki</title>
<style>
:root { --bg: #fff; --fg: #1a1a1a; --link: #0366d6; --muted: #666; --border: #e0e0e0; }
@media (prefers-color-scheme: dark) { :root { --bg: #1a1a1a; --fg: #e6e6e6; --link: #58a6ff; --muted: #999; --border: #333; } }
body { max-width: 720px; margin: 2rem auto; padding: 0 1rem; font: 16px/1.6 system-ui; background: var(--bg); color: var(--fg); }
nav { margin-bottom: 1.5rem; }
nav a { color: var(--muted); font-size: .9rem; }
h1 { font-size: 1.8rem; }
h2 { font-size: 1.4rem; margin-top: 2rem; border-bottom: 1px solid var(--border); padding-bottom: .3rem; }
h3 { font-size: 1.1rem; margin-top: 1.5rem; }
a { color: var(--link); }
pre { background: var(--border); padding: .8rem 1rem; border-radius: 4px; overflow-x: auto; font-size: .9rem; }
code { font-size: .9em; background: var(--border); padding: .1em .3em; border-radius: 3px; }
pre code { background: none; padding: 0; }
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
th, td { border: 1px solid var(--border); padding: .4rem .6rem; text-align: left; font-size: .9rem; }
th { background: var(--border); }
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
</style>
</head>
<body>
<nav><a href="/">← wiki index</a></nav>
<article>
<h1>{title}</h1>
<p set:html={content} />
</article>
</body>
</html>

View file

@ -0,0 +1,60 @@
---
import fs from "node:fs";
import path from "node:path";
const WIKI_DIR = path.resolve("../../docs/wiki");
const EXCLUDE = [".git", "sl", "index.md"];
function walkMarkdown(dir, prefix = "") {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const files = [];
for (const e of entries) {
if (e.name.startsWith(".") || EXCLUDE.includes(e.name)) continue;
const full = path.join(dir, e.name);
if (e.isDirectory()) {
files.push(...walkMarkdown(full, prefix ? `${prefix}/${e.name}` : e.name));
} else if (e.name.endsWith(".md")) {
const rel = prefix ? `${prefix}/${e.name}` : e.name;
const slug = rel.replace(/\.md$/, "");
// Skip frontmatter, grab first H1 as title
const raw = fs.readFileSync(full, "utf-8");
const title = raw.match(/^#\s+(.+)$/m)?.[1] || slug;
files.push({ slug, title, file: rel });
}
}
return files.sort((a, b) => a.title.localeCompare(b.title));
}
const pages = walkMarkdown(WIKI_DIR);
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Colibri Wiki</title>
<style>
:root { --bg: #fff; --fg: #1a1a1a; --link: #0366d6; --muted: #666; }
@media (prefers-color-scheme: dark) { :root { --bg: #1a1a1a; --fg: #e6e6e6; --link: #58a6ff; --muted: #999; } }
body { max-width: 720px; margin: 2rem auto; padding: 0 1rem; font: 16px/1.6 system-ui; background: var(--bg); color: var(--fg); }
h1 { margin-bottom: .25rem; }
p.lede { color: var(--muted); margin-bottom: 1.5rem; }
ul { list-style: none; padding: 0; }
li { margin: .35rem 0; }
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>Colibri Wiki</h1>
<p class="lede">
Decision pages — the <em>why</em> behind the architecture.
<a href="https://github.com/karpathy/llm-wiki">LLM Wiki pattern</a>.
</p>
<ul>
{pages.map((p) => (
<li><a href={`/${p.slug}/`}>{p.title}</a></li>
))}
</ul>
</body>
</html>

View file

@ -0,0 +1,49 @@
# wiki.clawdie.si — separate domain for decision pages
**Status:** planned · **Created:** 26.jun.2026 · **Blocks:** nothing in 0.12
## Goal
Split the current docs.clawdie.si (single site) into two domains during the
Astro migration from clawdie-ai → colibri:
```
docs.clawdie.si → guide (procedural: install, operate, reference)
wiki.clawdie.si → wiki (decisions: architecture rationale, LLM-wiki)
clawdie.si → landing (unchanged)
```
## Why
- Wiki stays pure Karpathy LLM-wiki pattern — one decision per page, flat list
- Guide stays procedural — structured sidebar with install/operate/architecture
- Different audiences: wiki for agents/architects, guide for operators
- ISO can toggle each surface independently (FEATURE_DOCS, FEATURE_WIKI)
## What needs building
| Layer | Task |
|---|---|
| DNS | `wiki.clawdie.si` A/AAAA → same host |
| TLS | New Let's Encrypt cert (acme.sh auto-renew) |
| Nginx | New vhost for wiki.clawdie.si |
| Astro | Two Starlight configs from one colibri source tree |
| Build | `build-docs.sh` → dist-guide/ + dist-wiki/ |
| ISO | `FEATURE_DOCS` / `FEATURE_WIKI` toggle knobs |
## Two Starlight configs
```
colibri/astro/
guide.config.mjs → full sidebar: Install, Operate, Architecture...
wiki.config.mjs → minimal sidebar: autogenerate flat article list
```
Same toolchain, two configs, two output dirs. Wiki uses autogenerate — no
manual sidebar to maintain as pages are added.
## Prerequisite
The Astro build pipeline must be migrated from clawdie-ai to colibri first.
The content already lives in colibri (docs/guide/ + docs/wiki/). The build
scripts and Astro config don't yet.

29
scripts/build-wiki.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/sh
# Build the Colibri wiki site — plain Astro, no Starlight.
#
# Prerequisites: Node.js + npm (node24 npm-node24 on FreeBSD).
# cd astro/wiki && npm ci
#
# Usage:
# ./scripts/build-wiki.sh # build to astro/wiki/dist/
# ./scripts/build-wiki.sh --preview # dev server at localhost:4321
#
# Site URL override:
# ASTRO_SITE_URL=https://wiki.clawdie.si ./scripts/build-wiki.sh
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
WIKI_DIR="$REPO_ROOT/astro/wiki"
cd "$WIKI_DIR"
if [ "${1:-}" = "--preview" ]; then
echo "==> wiki dev server (http://localhost:4321)"
npx astro dev --host 0.0.0.0
else
echo "==> building wiki ($WIKI_DIR)"
npx astro build
echo "==> wiki built → $WIKI_DIR/dist/"
fi