Blog post: /GIT/ URL refactor plan + /images/ wildcard route
The plan post explains why dropping the redundant `:owner` segment from /GIT/:owner/:repo/... URLs is safe (isAllowedRepo enforces a single tenant by design) and walks the design decision behind the one-regex 301 redirect that covers all 49 known references plus every future URL with the same shape. Also establishes the new image convention: every site image lives under public/images/ (served from /images/) with a literal "https://tdd.md" watermark. The wildcard /images/<name>.<ext> handler in d21_handlers_fallback.ts makes the convention mechanical — no per-image route registration needed in d21_app.ts. Two visualizations in this post: URL anatomy (BEFORE/AFTER with the isAllowedRepo snippet) + redirect flow (inbound → regex matcher → 301 Location header) — both watermarked. Co-Authored-By: Claude Opus 4.7 <[email protected]>
7 files changed · +279 −0
content/blog/sama-v2-git-url-refactor-plan.md
+135
−0
| @@ -0,0 +1,135 @@ | ||
| 1 | +# Shortening `/GIT/` URLs: a single-tenant URL has a redundant segment | |
| 2 | + | |
| 3 | +Every link on this site that points at the source code passes through `/GIT/:owner/:repo/...`. The owner segment is always `syntaxai`. The repo segment is always `tdd.md`. The handler validates both, 404s anything else, and never reads them again. The user-visible URL is doing structural work for a multi-tenant case that doesn't exist. | |
| 4 | + | |
| 5 | +Concrete example — the verifier source link: | |
| 6 | + | |
| 7 | +``` | |
| 8 | +before: https://tdd.md/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts | |
| 9 | +after: https://tdd.md/GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts | |
| 10 | +``` | |
| 11 | + | |
| 12 | +Nine characters shorter. The change is small but the workflow it sits inside is the same one this site is built around — `/goal` slash command as contract, SAMA v2 as discipline, the verifier as anti-fudge gate. This post is the **plan**, written before the `/goal` fires. | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | +## Why dropping the owner is safe | |
| 17 | + | |
| 18 | +The relevant code is twenty-one lines down [`src/d21_handlers_repo_browse.ts`](/GIT/syntaxai/tdd.md/blob/main/src/d21_handlers_repo_browse.ts): | |
| 19 | + | |
| 20 | +```ts | |
| 21 | +const isAllowedRepo = (owner: string, repo: string): boolean => | |
| 22 | + owner === LIVE_REPO_OWNER && // "syntaxai" | |
| 23 | + repo === LIVE_REPO_NAME && // "tdd.md" | |
| 24 | + SAFE_OWNER_REPO.test(owner) && | |
| 25 | + SAFE_OWNER_REPO.test(repo); | |
| 26 | +``` | |
| 27 | + | |
| 28 | +The check is structural — there is exactly one allowed pair, and any deviation produces a 404. So the owner segment carries no information the handler couldn't supply itself. It's a position in the URL that exists only to make the URL look like a GitHub URL — which, given that the data is *not* on GitHub, is a costume rather than a contract. | |
| 29 | + | |
| 30 | +The signature also drops to `isAllowedRepo(repo)`. `LIVE_REPO_OWNER` stays in `src/a31_site_config.ts` — it's still the truthful owner for the backing git operations, the Forgejo proxy, and any future feature that needs to talk about provenance. It just stops showing up in user-facing URLs. | |
| 31 | + | |
| 32 | +## The interesting design decision — one regex, not 49 redirects | |
| 33 | + | |
| 34 | +A grep across the repo finds **49 references** to the old URL form across **10 source files** and **7 content files** — link builders, hard-coded markdown in `/sama/v2/verify`, blog posts that point at specific files for their empirical claims, the verifier page itself. | |
| 35 | + | |
| 36 | +Naive approach: hand-maintain a list of 49 old-URL → new-URL mappings as a redirect table. Cost: rewrites work today, but the list rots the next time someone adds a new file or blog post (50 grows to 60 grows to 100). Anti-pattern. | |
| 37 | + | |
| 38 | +The right shape is **one regex in the fallback handler** that matches the *pattern* of the old URL and rewrites to the new one: | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | +```ts | |
| 43 | +const oldGitUrl = url.pathname.match( | |
| 44 | + /^\/GIT\/syntaxai\/tdd\.md\/(.+)$/, | |
| 45 | +); | |
| 46 | +if (oldGitUrl) { | |
| 47 | + return new Response(null, { | |
| 48 | + status: 301, | |
| 49 | + headers: { | |
| 50 | + Location: `/GIT/tdd.md/${oldGitUrl[1]}`, | |
| 51 | + "Cache-Control": "public, max-age=86400", | |
| 52 | + }, | |
| 53 | + }); | |
| 54 | +} | |
| 55 | +``` | |
| 56 | + | |
| 57 | +Five lines. Covers all 49 known references and every future URL with the same shape. Cost: one commit. Lifetime maintenance: zero. | |
| 58 | + | |
| 59 | +The 301 (permanent redirect) is the load-bearing detail — search engines treat 301 as "update your index"; they treat 302 as "this is temporary, keep the old URL." We want the index to converge on the new URL, so 301 it is. | |
| 60 | + | |
| 61 | +## How this maps onto SAMA v2 | |
| 62 | + | |
| 63 | +The refactor touches files across three layers, all in expected ways: | |
| 64 | + | |
| 65 | +| Layer | What changes | | |
| 66 | +|---|---| | |
| 67 | +| **Layer 0 · Pure** (`a31_site_config.ts`) | `LIVE_REPO_OWNER` stays exported — still the truthful owner constant, just no longer used to build URLs | | |
| 68 | +| **Layer 1 · Core** | No changes — there are no Layer-1 helpers in the `/GIT/` flow; the URL surface is pure routing | | |
| 69 | +| **Layer 2 · Adapter** (`c14_git.ts`) | No changes — `lsTree` and `readBlobAtRef` already take `(ref, path)`, never owner/repo | | |
| 70 | +| **Layer 3 · Entry** | All the changes live here — `parseRepoBrowsePath` callers, `repoBrowseHandler` signature, the Bun explicit route `/GIT/:repo/commit/:sha`, the new 301 redirect, and the link builders in `b51_render_*.ts` | | |
| 71 | + | |
| 72 | +The layer surface tells you the refactor is contained — no Adapter changes, no business-logic changes, no test-of-pure-helper changes. Only the routing/rendering surface moves. That's the "small refactor" smell the [Layer 2 stays empty](/blog/sama-v2-sitemap-implementation-plan) sitemap post identified — when the change is genuinely about the URL surface, the deeper layers don't need to move. | |
| 73 | + | |
| 74 | +## Anti-fudge — what the `/goal` rules out | |
| 75 | + | |
| 76 | +The plan deliberately doesn't do these things, even though each is locally appealing: | |
| 77 | + | |
| 78 | +- **No hand-maintained list of redirects.** One regex pattern covers all 49 current references and every future one. If the regex grows into "a list", the anti-fudge clause has been violated. | |
| 79 | +- **No removal of `LIVE_REPO_OWNER`.** The constant has callers beyond URL construction (the live-reports view, the Forgejo proxy hostname). Removing it from `a31_site_config.ts` would be a different, larger refactor that the URL change shouldn't drag in. | |
| 80 | +- **No touching of git-protocol URLs.** `/syntaxai/tdd.md.git` and the bare-repo view at `/syntaxai/tdd.md` go through `isGitProtocol` + `repoMatch` in [`d21_handlers_fallback.ts`](/GIT/syntaxai/tdd.md/blob/main/src/d21_handlers_fallback.ts). Those URLs are git-client-facing — agents and humans have copy-pasted them into clone commands, into CI configs, into other agents' system prompts. Changing them risks breakage for cosmetics. They stay. | |
| 81 | +- **No alias.** Both URL forms working forever creates two canonical URLs and lets the old one quietly remain in new code. The 301 is what forces consolidation — search engines update, internal code paths rewrite themselves, and a year from now the old form is just a redirect line in one file. | |
| 82 | +- **No verifier change.** `/sama/v2/verify` stays at 7/7 ✓ across the merge. The §4 check logic is frozen; if a structural choice the refactor wants to make would fail the verifier, the choice changes — not the verifier. | |
| 83 | + | |
| 84 | +## The work, sized | |
| 85 | + | |
| 86 | +Three categories of file change: | |
| 87 | + | |
| 88 | +- **Wiring (4 files)**: the fallback handler gets the new redirect + the parse regex drops `owner`; the explicit Bun commit route in `d21_app.ts` becomes `/GIT/:repo/commit/:sha`; `repoBrowseHandler` and `commitViewHandler` lose the `owner` argument; `isAllowedRepo` collapses to one argument. | |
| 89 | +- **Link builders (3 files)**: `b51_render_repo.ts` (eight call sites — breadcrumbs, parent-dir, raw/source links), `b51_render_commit.ts` (two call sites), `b51_render_edit.ts` (one hard-coded URL). | |
| 90 | +- **Hard-coded markdown (7 files)**: `content/home.md`, `content/sama/v2.md`, four blog posts that point at specific source files for their empirical claims, `src/d21_handlers_sama.ts:137` (markdown embedded in the verifier page body). One sed pass, all done. | |
| 91 | + | |
| 92 | +The test files (`b51_render_repo.test.ts`, `b51_render_commit.test.ts`) pin the rendered URL strings — those expectations update mechanically with the link-builder changes. Test count stays at 379+; no test count regression. | |
| 93 | + | |
| 94 | +## Live-verify clauses | |
| 95 | + | |
| 96 | +What the `/goal` requires to verify *after deploy*, not just *in CI*: | |
| 97 | + | |
| 98 | +```bash | |
| 99 | +$ curl -I https://tdd.md/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts | |
| 100 | +HTTP/2 301 | |
| 101 | +location: /GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts | |
| 102 | + | |
| 103 | +$ curl -L https://tdd.md/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts | |
| 104 | +HTTP/2 200 | |
| 105 | +< file content > | |
| 106 | + | |
| 107 | +$ curl -s https://tdd.md/GIT/tdd.md/tree/main | head -1 | |
| 108 | +< 200, directory listing HTML > | |
| 109 | + | |
| 110 | +$ curl -s https://tdd.md/sama/v2/verify | grep -o '7/7' | |
| 111 | +7/7 | |
| 112 | +``` | |
| 113 | + | |
| 114 | +Plus the silent live check: every blog post on the site has its `/GIT/` links rewritten, so clicking any "view source" link in any of the empirical-chain posts lands on a working URL — no broken navigation surfaced after the merge. | |
| 115 | + | |
| 116 | +## What lands when this ships | |
| 117 | + | |
| 118 | +After deploy: | |
| 119 | + | |
| 120 | +- Every `/GIT/` URL on the site uses the new shape. | |
| 121 | +- The verifier source — the URL search engines and AI crawlers should index as "the artifact this site's argument rests on" — gets shorter and more readable. | |
| 122 | +- Old URLs already indexed by Google, cached by Twitter card scrapers, sitting in someone else's blog post, or pasted into someone's notes file all permanently-redirect to the new form. Index reconverges in a search-engine refresh cycle. | |
| 123 | +- `/sama/v2/verify` continues to report **7 ✓ / 7**. | |
| 124 | +- One new pattern — the regex-as-redirect — surfaces a reusable shape for future URL refactors. If the site renames `/sama/v2/example-crud` to `/sama/v2/examples/crud` next month, the same shape applies. | |
| 125 | + | |
| 126 | +## Companion postmortem | |
| 127 | + | |
| 128 | +This is the plan. The postmortem will follow after the merge with: | |
| 129 | + | |
| 130 | +- The actual file diff (likely tight — most line changes are mechanical `s/syntaxai\/tdd\.md/tdd.md/g` substitutions). | |
| 131 | +- Whether the regex caught everything (especially in places `grep` missed — embedded HTML strings, multi-line URLs, etc.). | |
| 132 | +- The `/sama/v2/verify` output before and after the merge. | |
| 133 | +- Anything the anti-fudge clauses caught that the plan missed. | |
| 134 | + | |
| 135 | +If the refactor lands cleanly with the regex absorbing all 49 references — that's the data point: pattern-as-redirect is a reusable shape, and the next URL refactor needs ten lines plus a sed pass. | |
public/images/git-url-anatomy.png
+0
−0
public/images/git-url-anatomy.svg
+45
−0
| @@ -0,0 +1,45 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 600" width="1200" height="600"> | |
| 2 | + <rect width="1200" height="600" fill="#0a0a0a"/> | |
| 3 | + | |
| 4 | + <!-- Header --> | |
| 5 | + <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace"> | |
| 6 | + <text x="80" y="46" font-size="20" font-weight="600" fill="#909090">/GIT/ URL anatomy — one segment is doing no work</text> | |
| 7 | + <text x="80" y="92" font-size="32" font-weight="700" fill="#e8e8e8">isAllowedRepo() already enforces a single tenant.</text> | |
| 8 | + <text x="80" y="120" font-size="15" fill="#7a7a7a">The owner segment is checked, rejected if anything else, and never read again. It's policy overhead, not data.</text> | |
| 9 | + </g> | |
| 10 | + | |
| 11 | + <!-- BEFORE label + URL --> | |
| 12 | + <text x="80" y="172" font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="14" font-weight="600" fill="#b8794a" letter-spacing="2">BEFORE</text> | |
| 13 | + | |
| 14 | + <rect x="80" y="184" width="1040" height="60" fill="#1a1a1a" stroke="#2a2a2a" stroke-width="1.5" rx="6"/> | |
| 15 | + <text x="100" y="222" font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="18" xml:space="preserve" fill="#e8e8e8">https://tdd.md/GIT/<tspan fill="#b8794a" font-weight="700" text-decoration="line-through">syntaxai/</tspan>tdd.md/blob/main/src/b32_sama_v2_verify.ts</text> | |
| 16 | + | |
| 17 | + <!-- Caret pointer + caption under the struck segment. | |
| 18 | + "https://tdd.md/GIT/" = 19 chars × ~10.8px ≈ 205px starting at x=100, | |
| 19 | + so "syntaxai/" sits centered around x=100 + 205 + 49 = 354. --> | |
| 20 | + <text x="354" y="262" text-anchor="middle" font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="12" fill="#b8794a">↑ redundant — always "syntaxai", validated then ignored</text> | |
| 21 | + | |
| 22 | + <!-- isAllowedRepo snippet --> | |
| 23 | + <rect x="80" y="288" width="1040" height="116" fill="#101010" stroke="#1f1f1f" stroke-width="1" rx="6"/> | |
| 24 | + <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="14"> | |
| 25 | + <text x="100" y="312" fill="#6a6a6a">// src/d21_handlers_repo_browse.ts:26</text> | |
| 26 | + <text x="100" y="334" fill="#c8c8c8"><tspan fill="#a07a5a">const</tspan> isAllowedRepo = (owner: <tspan fill="#7ec77e">string</tspan>, repo: <tspan fill="#7ec77e">string</tspan>): <tspan fill="#7ec77e">boolean</tspan> =></text> | |
| 27 | + <text x="120" y="354" fill="#c8c8c8">owner === <tspan fill="#6a8db5">LIVE_REPO_OWNER</tspan> && <tspan fill="#6a6a6a">// "syntaxai" — checked but never user-supplied in practice</tspan></text> | |
| 28 | + <text x="120" y="374" fill="#c8c8c8">repo === <tspan fill="#6a8db5">LIVE_REPO_NAME</tspan> && <tspan fill="#6a6a6a">// "tdd.md"</tspan></text> | |
| 29 | + <text x="120" y="394" fill="#c8c8c8">SAFE_OWNER_REPO.test(owner) && SAFE_OWNER_REPO.test(repo);</text> | |
| 30 | + </g> | |
| 31 | + | |
| 32 | + <!-- AFTER label + URL --> | |
| 33 | + <text x="80" y="446" font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="14" font-weight="600" fill="#7ec77e" letter-spacing="2">AFTER</text> | |
| 34 | + | |
| 35 | + <rect x="80" y="458" width="1040" height="60" fill="#1a1a1a" stroke="#7ec77e" stroke-width="1.5" rx="6"/> | |
| 36 | + <text x="100" y="496" font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="18" fill="#e8e8e8">https://tdd.md/GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts</text> | |
| 37 | + | |
| 38 | + <!-- Counts strip --> | |
| 39 | + <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="13" fill="#8a8a8a"> | |
| 40 | + <text x="80" y="552">49 references touched · 10 source files · 7 content files · 1 regex 301-redirect</text> | |
| 41 | + </g> | |
| 42 | + | |
| 43 | + <!-- Watermark --> | |
| 44 | + <text x="1120" y="584" text-anchor="end" font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="12" fill="#5a5a5a">https://tdd.md</text> | |
| 45 | +</svg> | |
public/images/git-url-redirect.png
+0
−0
public/images/git-url-redirect.svg
+67
−0
| @@ -0,0 +1,67 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 600" width="1200" height="600"> | |
| 2 | + <rect width="1200" height="600" fill="#0a0a0a"/> | |
| 3 | + | |
| 4 | + <!-- Header --> | |
| 5 | + <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace"> | |
| 6 | + <text x="80" y="46" font-size="20" font-weight="600" fill="#909090">Shipping the URL change — one regex, not 49 redirects</text> | |
| 7 | + <text x="80" y="92" font-size="32" font-weight="700" fill="#e8e8e8">Inbound links survive. SEO juice carries over.</text> | |
| 8 | + <text x="80" y="120" font-size="15" fill="#7a7a7a">A single regex in d21_handlers_fallback.ts rewrites every old URL to the new shape with a 301. No hand-maintained map.</text> | |
| 9 | + </g> | |
| 10 | + | |
| 11 | + <!-- Old URL box (320 wide, 60..380) --> | |
| 12 | + <rect x="60" y="170" width="320" height="100" fill="#1a1a1a" stroke="#b8794a" stroke-width="1.5" rx="6"/> | |
| 13 | + <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace"> | |
| 14 | + <text x="80" y="195" font-size="12" fill="#b8794a" letter-spacing="1">INBOUND · cached externally</text> | |
| 15 | + <text x="80" y="223" font-size="13" fill="#c8c8c8">GET /GIT/syntaxai/tdd.md/</text> | |
| 16 | + <text x="80" y="241" font-size="13" fill="#c8c8c8">blob/main/src/</text> | |
| 17 | + <text x="80" y="259" font-size="13" fill="#c8c8c8">b32_sama_v2_verify.ts</text> | |
| 18 | + </g> | |
| 19 | + | |
| 20 | + <!-- Arrow 1: 380..430 --> | |
| 21 | + <line x1="386" y1="220" x2="424" y2="220" stroke="#8a8a8a" stroke-width="1.5"/> | |
| 22 | + <polygon points="420,215 432,220 420,225" fill="#8a8a8a"/> | |
| 23 | + | |
| 24 | + <!-- Regex matcher box (340 wide, 430..770) --> | |
| 25 | + <rect x="430" y="170" width="340" height="100" fill="#1a1a1a" stroke="#4a8a8a" stroke-width="1.5" rx="6"/> | |
| 26 | + <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace"> | |
| 27 | + <text x="450" y="195" font-size="12" fill="#4a8a8a" letter-spacing="1">FALLBACK HANDLER · one regex</text> | |
| 28 | + <text x="450" y="223" font-size="13" fill="#c8c8c8">/^\/GIT\/syntaxai\/tdd\.md\/</text> | |
| 29 | + <text x="450" y="241" font-size="13" fill="#c8c8c8">(.+)$/</text> | |
| 30 | + <text x="450" y="261" font-size="12" fill="#8a8a8a">→ rewrite /GIT/tdd.md/$1, 301</text> | |
| 31 | + </g> | |
| 32 | + | |
| 33 | + <!-- Arrow 2: 770..820 --> | |
| 34 | + <line x1="776" y1="220" x2="814" y2="220" stroke="#8a8a8a" stroke-width="1.5"/> | |
| 35 | + <polygon points="810,215 822,220 810,225" fill="#8a8a8a"/> | |
| 36 | + | |
| 37 | + <!-- New URL box (320 wide, 820..1140) --> | |
| 38 | + <rect x="820" y="170" width="320" height="100" fill="#1a1a1a" stroke="#7ec77e" stroke-width="1.5" rx="6"/> | |
| 39 | + <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace"> | |
| 40 | + <text x="840" y="195" font-size="12" fill="#7ec77e" letter-spacing="1">301 · Location header</text> | |
| 41 | + <text x="840" y="223" font-size="13" fill="#c8c8c8">/GIT/tdd.md/blob/main/</text> | |
| 42 | + <text x="840" y="241" font-size="13" fill="#c8c8c8">src/</text> | |
| 43 | + <text x="840" y="259" font-size="13" fill="#c8c8c8">b32_sama_v2_verify.ts</text> | |
| 44 | + </g> | |
| 45 | + | |
| 46 | + <!-- Curl example --> | |
| 47 | + <rect x="60" y="296" width="1080" height="146" fill="#101010" stroke="#1f1f1f" stroke-width="1" rx="6"/> | |
| 48 | + <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="13"> | |
| 49 | + <text x="80" y="322" fill="#6a6a6a">$ curl -I https://tdd.md/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts</text> | |
| 50 | + <text x="80" y="344" fill="#7ec77e">HTTP/2 301</text> | |
| 51 | + <text x="80" y="362" fill="#c8c8c8">location: /GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts</text> | |
| 52 | + <text x="80" y="380" fill="#6a6a6a">cache-control: public, max-age=86400</text> | |
| 53 | + <text x="80" y="408" fill="#6a6a6a">$ curl -L https://tdd.md/GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts</text> | |
| 54 | + <text x="80" y="426" fill="#7ec77e">HTTP/2 200</text> | |
| 55 | + </g> | |
| 56 | + | |
| 57 | + <!-- Anti-fudge --> | |
| 58 | + <g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace"> | |
| 59 | + <text x="80" y="476" font-size="16" font-weight="600" fill="#e8e8e8">Why one regex, not 49 entries</text> | |
| 60 | + <text x="80" y="500" font-size="13" fill="#8a8a8a">A hand-maintained URL map would need updating every time a new file is added (49 grows to 50, 60, 80…).</text> | |
| 61 | + <text x="80" y="520" font-size="13" fill="#8a8a8a">The regex matches the path PATTERN, so any future URL — even ones that don't exist yet — automatically redirects.</text> | |
| 62 | + <text x="80" y="540" font-size="13" fill="#8a8a8a">Cost: 1 commit. Lifetime maintenance: 0. The anti-fudge clause in /goal forbids the hand-maintained alternative.</text> | |
| 63 | + </g> | |
| 64 | + | |
| 65 | + <!-- Watermark --> | |
| 66 | + <text x="1120" y="584" text-anchor="end" font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace" font-size="12" fill="#5a5a5a">https://tdd.md</text> | |
| 67 | +</svg> | |
src/a31_blog.ts
+6
−0
| @@ -12,6 +12,12 @@ export interface BlogEntry { | ||
| 12 | 12 | } |
| 13 | 13 | |
| 14 | 14 | export const ALL_POSTS: BlogEntry[] = [ |
| 15 | + { | |
| 16 | + slug: "sama-v2-git-url-refactor-plan", | |
| 17 | + title: "Shortening /GIT/ URLs: a single-tenant URL has a redundant segment", | |
| 18 | + description: "Every link on the site that points at source code goes through /GIT/:owner/:repo/... and the owner is always 'syntaxai', the repo is always 'tdd.md', and the handler 404s anything else. The user-visible URL does structural work for a multi-tenant case that doesn't exist. This post is the implementation PLAN for dropping the owner segment: /GIT/syntaxai/tdd.md/blob/main/src/b32_sama_v2_verify.ts → /GIT/tdd.md/blob/main/src/b32_sama_v2_verify.ts. The interesting design decision is the redirect strategy — 49 references to the old form across 10 source files + 7 content files, and the temptation is to hand-maintain a redirect table. The right shape is ONE regex in the fallback handler that matches the path PATTERN, not the path values, and 301s to the new form. Cost: one commit. Lifetime maintenance: zero. Includes two visualizations (URL anatomy + redirect flow) and walks the SAMA layer surface to show the refactor is contained to Layer 3 routing/rendering — no Adapter, no Core, no Pure changes. Anti-fudge clauses called out: no hand-maintained URL list, no removal of LIVE_REPO_OWNER (still needed for git operations + Forgejo proxy), no touching of git-protocol URLs (which agents and humans have copy-pasted into clone commands), no alias mode (both URLs working forever lets the old form quietly remain canonical), no verifier change. Postmortem to follow after the /goal fires and the refactor merges — pattern-as-redirect promises to be a reusable shape for future URL refactors (the next time /sama/v2/example-crud becomes /sama/v2/examples/crud, the same ten lines apply).", | |
| 19 | + date: "2026-05-25", | |
| 20 | + }, | |
| 15 | 21 | { |
| 16 | 22 | slug: "sama-v2-sitemap-implementation-plan", |
| 17 | 23 | title: "Building /sitemap.xml under SAMA v2 — a Claude Code /goal walkthrough", |
src/d21_handlers_fallback.ts
+26
−0
| @@ -48,6 +48,32 @@ export const appFetch = async (req: Request): Promise<Response> => { | ||
| 48 | 48 | } |
| 49 | 49 | const url = urlR.value; |
| 50 | 50 | |
| 51 | + // Static images under /images/<name>.<ext>. Convention: every new | |
| 52 | + // site image lives at public/images/ and is served from /images/. | |
| 53 | + // The whitelist of extensions + the strict filename pattern blocks | |
| 54 | + // path traversal (no slashes after /images/, no leading dots). | |
| 55 | + const imagesMatch = url.pathname.match( | |
| 56 | + /^\/images\/([A-Za-z0-9][A-Za-z0-9._-]*)\.(svg|png|webp|jpg|jpeg|gif)$/, | |
| 57 | + ); | |
| 58 | + if (imagesMatch) { | |
| 59 | + const file = Bun.file(`./public/images/${imagesMatch[1]}.${imagesMatch[2]}`); | |
| 60 | + if (await file.exists()) { | |
| 61 | + const ext = imagesMatch[2]!; | |
| 62 | + const contentType = | |
| 63 | + ext === "svg" ? "image/svg+xml" : | |
| 64 | + ext === "png" ? "image/png" : | |
| 65 | + ext === "webp" ? "image/webp" : | |
| 66 | + ext === "gif" ? "image/gif" : | |
| 67 | + "image/jpeg"; | |
| 68 | + return new Response(file, { | |
| 69 | + headers: { | |
| 70 | + "Content-Type": contentType, | |
| 71 | + "Cache-Control": "public, max-age=3600", | |
| 72 | + }, | |
| 73 | + }); | |
| 74 | + } | |
| 75 | + } | |
| 76 | + | |
| 51 | 77 | // Admin edit/delete on multi-segment slugs (company/about, docs/spec/grammar |
| 52 | 78 | // etc.). Bun's `:slug` param can't span "/" so anything with two-or-more |
| 53 | 79 | // segments after the type slot ends up here. Single-segment is handled |