fuzzyworld

fuzzyworld-tools / commit

Site pages in Markdown: /why/ is server/content/why.md

ec9751fae0f29865413ba420f68fc6e1da97e478 · Pavel Safronov · 2026-10-27 11:58:08 -0700 · parents 9eb01df74d91

Claude Code usage: this commit ~9.7K out / ~901.1K cached (first commit of its session: everything since it began) · cost unknown (no per-model trailer) · session 3 tally 40 in / 9.7K out / 901.1K cached · usage by commit and session

Site pages in Markdown: /why/ is server/content/why.md

gen.py content renders server/content/*.md through the shared page template;
fw content runs it and install.sh calls it. The hand-written why/index.html is gone.

Usage: 40 in / 9.7K out / 901.1K cached (session tally)
rtk:   2.5M saved, 53.5% over 5810 cmds (global/lifetime)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
 PLAN.md                    | 11 ++++-------
 README.md                  |  2 +-
 server/content/why.md      | 29 +++++++++++++++++++++++++++++
 server/fw                  |  2 ++
 server/gen.py              | 22 ++++++++++++++++++++++
 server/install.sh          |  2 +-
 server/site/why/index.html | 44 --------------------------------------------
 7 files changed, 59 insertions(+), 53 deletions(-)

diff --git a/PLAN.md b/PLAN.md
index 9d6ad75..4aa51f8 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -91,6 +91,8 @@ The order goes from cheap to expensive, and each step reuses what the one before
   - CI `cache:` support (a per-repo volume on TrueNAS), and `services:`
   - Branch pages: link to the default branch's page when a file is identical, instead of a copy
 - Found in review (2026-10-27): discussed, not done
+  - Site pages in Markdown: `/why/` done (`server/content/why.md`, `gen.py content`). Still HTML: `/principles/`,
+    `/contribute/`, `/how/` draft, `404.html`
   - `/how/` goes live: finish the draft, add it to `server/install.sh` (`/why/` already says it is coming)
   - CI runner tags (`docker`, `chrome`, `android`) and the laptop as a second runner (`fw-runner.py --once`):
     designed, not built; there is one runner, and it takes every job
@@ -115,13 +117,8 @@ Wording approved 2026-10-26:
 > Things made with AI should be shared freely with the world. The code and apps here were made with
 > AI, so they are MIT-licensed: use them, change them, share them.
 >
-> My conversations with the AI are different. My words and my way of working are mine. I publish a
-> conversation only when I choose to, I can take that permission back, and nobody may copy my words or
-> my style of work without my express permission.
-
-**Style** (keep this note with the wording). Copyright protects my words; it generally doesn't protect a
-style or a way of working. "Nobody may copy my style" is therefore a statement of my terms and wishes,
-not something a license can enforce. The wording is written to be honest about that.
+> My conversations with the AI are different. My words and my way of working are mine to publish or not.
+> I will publish a conversation only when I choose to.
 
 ## CI
 
diff --git a/README.md b/README.md
index a99ca68..837b14e 100644
--- a/README.md
+++ b/README.md
@@ -133,7 +133,7 @@ Every repo uses the shared hook via `core.hooksPath=~/fw/tools/server/hooks`. `p
 | `/<name>/ci/badge.svg` | status of the latest pipeline on the default branch |
 | `/ci/` | CI dashboard: running pipelines (each job's state and time), the queue, the last 50 results, for public repos |
 | `/<name>/files.json`, `/<name>/objects/<sha>` | for VS Code in the browser: every file's blob SHA and size, and each file's contents (≤ 1 MB) |
-| `/why/`, `/principles/`, `/contribute/` | about the site |
+| `/why/`, `/principles/`, `/contribute/` | about the site. `/why/` is written in Markdown (`server/content/why.md`, rendered by `fw content`); the other two are still hand-written HTML in `server/site/` |
 
 **Branches:** the default branch, and every branch **not yet merged** into it, get browse pages. A merged branch
 adds nothing new to read and would duplicate every page, so the branches page lists it with a link to its last
diff --git a/server/content/why.md b/server/content/why.md
new file mode 100644
index 0000000..1c4a584
--- /dev/null
+++ b/server/content/why.md
@@ -0,0 +1,29 @@
+title: Why this site exists
+
+# Why I left GitLab
+
+On Monday, October 26, 2026, I came back to work after a week's vacation to Friday Harbor. While on vacation I did what I like to do for fun and made a turn-based rogue-like Bomberman clone.
+So back at work, I was thinking of sharing it with some coworkers,
+but very quickly GitLab showed me a "Rate limit exceeded" error. And I rapidly learned that GitLab enacted
+some rate-limiting policies recently that blocked repo access at work, and, very quickly, even
+access to GitLab itself!
+
+So yeah, I couldn't look at my personal code at work, and that pissed me off, and now all of my code and
+projects are hosted on Dreamhost, where I already keep a blog and a lightweight webpage.
+
+- code: <https://code.fuzzyworld.net/>
+- apps: <https://apps.fuzzyworld.net/>
+
+No rate limiting, no sign-in, no "modern" limits. This is a static website! What more do I really need?
+
+I'm currently trying to formalize the process of the move from GitLab to Dreamhost, and eventually my
+learnings are going to be at /how/ on this website.
+
+## Principles
+
+Things made with AI should be shared freely with the world. The code and apps here were made with
+AI, so they are MIT-licensed: use them, change them, share them.
+
+My conversations with the AI are different. My words and my way of working are mine. I publish a conversation only when I choose to.
+
+More on the [principles](/principles/) page.
diff --git a/server/fw b/server/fw
index a48aef7..248b773 100755
--- a/server/fw
+++ b/server/fw
@@ -11,6 +11,7 @@
 #   fw publish-all                  publish every public repo, then the index
 #   fw install-hooks                point every repo at the shared hooks
 #   fw apps-index                   rebuild the apps.fuzzyworld.net index
+#   fw content                      render the Markdown site pages (server/content/*.md, e.g. /why/)
 #   fw app-deployed <app> <repo>    after `fw deploy`: apps index, plus the repo's code page
 #   fw ci <name> [branch]           queue a CI run of a branch's latest commit (default: the default branch)
 #   fw ci-queue <name> <ref> <sha>  queue a CI run (the push hook runs this)
@@ -224,6 +225,7 @@ cmd_ci_watch() {
 mkdir -p "$STATE" "$LOGS"
 case "${1:-}" in
   apps-index) "${GEN[@]}" apps ;;
+  content) "${GEN[@]}" content ;;
   app-deployed) cmd_app_deployed "${2:-}" "${3:-}" ;;
   list) cmd_list ;;
   new) shift; cmd_new "$@" ;;
diff --git a/server/gen.py b/server/gen.py
index dfba4c8..096ad01 100755
--- a/server/gen.py
+++ b/server/gen.py
@@ -4,6 +4,7 @@
     gen.py repo <name>    build or update the pages of one public repo
     gen.py index          rebuild the site index (public repos only)
     gen.py apps           rebuild the apps.fuzzyworld.net index from deployed apps
+    gen.py content        render server/content/<name>.md to code.fuzzyworld.net/<name>/ (site pages written in Markdown)
     gen.py ci <name>      rebuild one public repo's CI pages, then the CI dashboard
     gen.py ci-dashboard   rebuild code.fuzzyworld.net/ci/ (queue, running, recent; public repos only)
     gen.py ci-status      the same, as text for the terminal, private repos included (fw ci-watch)
@@ -508,6 +509,25 @@ def render_markdown(text, ctx):
     return _ATTR.sub(fix, out)
 
 
+CONTENT = os.environ.get("FW_CONTENT", os.path.join(os.path.dirname(os.path.abspath(__file__)), "content"))
+
+
+def build_content():
+    """Site pages written in Markdown: content/<name>.md becomes <site>/<name>/index.html.
+    A `title: ...` line at the top of the file is the page title (Python-Markdown's meta extension)."""
+    if not markdown:
+        raise SystemExit("gen.py content needs Python-Markdown in " + PYLIB)
+    for name in sorted(os.listdir(CONTENT)):
+        stem, ext = os.path.splitext(name)
+        if ext != ".md":
+            continue
+        with open(os.path.join(CONTENT, name), encoding="utf-8") as f:
+            md = markdown.Markdown(extensions=["meta", "tables", "fenced_code", "sane_lists"], output_format="html")
+            body = md.convert(f.read())
+        title = " ".join(md.Meta.get("title", [stem]))
+        write(os.path.join(SITE, stem, "index.html"), page(title, body))
+
+
 # ---------------------------------------------------------------- highlighting
 
 # Pygments token types folded into a few CSS classes (colors in style.css). The most
@@ -1249,6 +1269,8 @@ def main(argv):
         build_index()
     elif len(argv) == 2 and argv[1] == "apps":
         build_apps_index()
+    elif len(argv) == 2 and argv[1] == "content":
+        build_content()
     elif len(argv) == 3 and argv[1] == "ci":
         name = argv[2]
         if public_repo(name):
diff --git a/server/install.sh b/server/install.sh
index 3617376..be55591 100755
--- a/server/install.sh
+++ b/server/install.sh
@@ -26,7 +26,6 @@ ln -sfn ~/fw/tools/server/fw ~/bin/fw
 site=~/code.fuzzyworld.net
 cp ~/fw/tools/server/site/htaccess "$site/.htaccess"
 cp ~/fw/tools/server/site/style.css ~/fw/tools/server/site/robots.txt ~/fw/tools/server/site/404.html "$site/"
-mkdir -p "$site/why" && cp ~/fw/tools/server/site/why/index.html "$site/why/"
 mkdir -p "$site/contribute" && cp ~/fw/tools/server/site/contribute/index.html "$site/contribute/"
 mkdir -p "$site/principles" && cp ~/fw/tools/server/site/principles/index.html "$site/principles/"
 apps=~/apps.fuzzyworld.net
@@ -34,6 +33,7 @@ cp ~/fw/tools/server/site-apps/htaccess "$apps/.htaccess"
 cp ~/fw/tools/server/site/style.css "$apps/"
 ~/bin/fw install-hooks
 ~/bin/fw apps-index
+~/bin/fw content
 # Scheduled jobs: replace only our own block in the crontab, keeping anything else.
 chmod +x ~/fw/tools/server/jobs/*.sh
 { { crontab -l 2>/dev/null || true; } | sed '/^# fw:begin/,/^# fw:end/d'; cat ~/fw/tools/server/jobs/crontab; } | crontab -
diff --git a/server/site/why/index.html b/server/site/why/index.html
deleted file mode 100644
index 205f2f4..0000000
--- a/server/site/why/index.html
+++ /dev/null
@@ -1,44 +0,0 @@
-<!doctype html>
-<html lang="en">
-
-<head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <title>Why this site exists · code.fuzzyworld.net</title>
-    <link rel="stylesheet" href="/style.css">
-</head>
-
-<body>
-    <header class="site"><a class="home" href="/">code.fuzzyworld.net</a></header>
-    <main>
-        <h1>Why I left GitLab</h1>
-        <p>
-            On Monday, October 26, 2026, I came back to work after a week's vacation to Friday Harbor. While on vacation I did what I like to do for fun and made a turn-based rogue-like Bomberman clone.
-            So back at work, I was thinking of sharing it with some coworkers,
-            but very quickly GitLab showed me a "Rate limit exceeded" error. And I rapidly learned that GitLab enacted
-            some rate-limiting policies recently that blocked repo access at work, and, very quickly, even
-            access to GitLab itself!</p>
-        <p>So yeah, I couldn't look at my personal code at work, and that pissed me off, and now all of my code and
-            projects are hosted on Dreamhost, where I already keep a blog and a lightweight webpage.</p>
-        <ul>
-            <li>code: <a href="https://code.fuzzyworld.net/">https://code.fuzzyworld.net/</a>
-            </li>
-            <li>apps: <a href="https://apps.fuzzyworld.net/">https://apps.fuzzyworld.net/</a></li>
-        </ul>
-        <p>No rate limiting, no sign-in, no "modern" limits. This is a static website! What more do I really need?</p>
-        <p></p>
-        <p>I'm currently trying to formalize the process of the move from GitLab to Dreamhost, and eventually my
-            learnings are going to be at /how/ on this website.</p>
-
-        <h2>Principles</h2>
-        <p>Things made with AI should be shared freely with the world. The code and apps here were made with
-            AI, so they are MIT-licensed: use them, change them, share them.</p>
-        <p>My conversations with the AI are different. My words and my way of working are mine. I publish a
-            conversation only when I choose to, I can take that permission back, and nobody may copy my words or
-            my style of work without my express permission.</p>
-        <p>More on the <a href="/principles/">principles</a> page.</p>
-    </main>
-    <footer class="site"><a href="/">All repositories</a> · <a href="https://apps.fuzzyworld.net/">Apps</a> · <a href="/principles/">Principles</a></footer>
-</body>
-
-</html>
\ No newline at end of file