syntaxai/tdd.md · commit d8934f9

/goal pending: blog-date-prefix

Data-driven URL refactor — portability-boundary test of the b32_<old>_url_redirect pattern. Helper imports ALL_POSTS and computes new URL from each post's date field. Clock from this commit; postmortem predicts ≤30 min.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-25 16:20:14 +01:00
parent
5b59081
commit
d8934f9368bb6efb63c94317d14e075eea9ae8c8

1 file changed · +71 −0

added goals/blog-date-prefix.md +71 −0
@@ -0,0 +1,71 @@
1+---
2+slug: blog-date-prefix
3+title: Move /blog/<slug> → /blog/<yyyy-mm>/<slug> — data-driven refactor portability test
4+date: 2026-05-25
5+branch: blog-date-prefix
6+pr_number: null
7+merge_sha: null
8+status: pending
9+related_posts: [sama-v2-second-url-refactor-postmortem]
10+---
11+
12+Goal: Execute the THIRD URL refactor under the b32_<old>_url_redirect pattern — this time DATA-DRIVEN. Move every blog post URL from /blog/<slug> to /blog/<yyyy-mm>/<slug>, where the yyyy-mm prefix is computed from each post's `date` field in ALL_POSTS. Inbound links 301 to the new shape. **Primary purpose: portability-boundary test** — PR #53 confirmed cost-flattening for FIXED-ENUM refactors (4 slugs hard-coded in the regex). This /goal tests whether the same pattern works when the new URL depends on per-entity DATA, not a fixed enumeration. The postmortem of PR #53 predicted ≤30 minutes wall-clock for this shape; the clock from `git checkout -b blog-date-prefix` to deploy+live-verify is the measurement.
13+
14+Done when:
15+- All blog post URLs work under the new shape:
16+ * /blog/2026-05/sama-v2-second-url-refactor-postmortem → 200
17+ * /blog/2026-05/sama-v2-on-ramp-gap → 200
18+ * ... (every entry in ALL_POSTS, ~28 posts)
19+- Old URLs are permanent redirects:
20+ * curl -I https://tdd.md/blog/sama-v2-on-ramp-gap → HTTP/2 301, location: /blog/2026-05/sama-v2-on-ramp-gap
21+ * Same shape for every existing post slug
22+- The Layer-1 helper at src/b32_blog_date_url_redirect.ts IS data-driven (not a fixed enum):
23+ import { ALL_POSTS } from "./a31_blog.ts";
24+ const OLD_PATTERN = /^\/blog\/([a-z0-9-]+)$/;
25+ export const rewriteOldBlogUrl = (pathname): string | null => {
26+ const m = OLD_PATTERN.exec(pathname);
27+ if (m === null) return null;
28+ const slug = m[1]!;
29+ const post = ALL_POSTS.find((p) => p.slug === slug);
30+ if (post === undefined) return null;
31+ return `/blog/${post.date.slice(0, 7)}/${slug}`;
32+ };
33+ Sibling test src/b32_blog_date_url_redirect.test.ts covers: matches existing slugs, returns null for unknown slugs, returns null for already-new-form URLs, returns null for non-/blog paths, handles different month dates correctly.
34+- Layer 3 wiring:
35+ * src/d21_handlers_fallback.ts: 301 redirect block placed adjacent to the existing rewriteOldGitUrl + rewriteOldSamaDisciplineUrl blocks (third instance of the pattern).
36+ * src/d21_app.ts Bun routes: replace "/blog/:slug" with "/blog/:yyyymm/:slug"; the blog landing /blog stays at /blog (no change).
37+ * src/d21_handlers_blog.ts (or wherever the blog detail handler lives): /blog/:yyyymm/:slug handler extracts the slug, looks up ALL_POSTS, validates yyyymm matches the post's date prefix (404 if mismatched to prevent spoofing), then renders.
38+ * Sitemap handler in src/d21_app.ts: ALL_POSTS mapping must emit /blog/<yyyy-mm>/<slug> URLs (not /blog/<slug>).
39+ * Blog index handler at /blog: row links emit the new URL shape.
40+- A migration script at scripts/migrate-blog-urls.ts (or inline in this PR): for each post in ALL_POSTS sorted by slug length DESCENDING (to avoid prefix collisions where one slug is a prefix of another), iterate every .md file in content/ + every .ts file in src/, replace `/blog/<slug>` (literal) with `/blog/<yyyymm>/<slug>`. After: grep for `/blog/<known-slug>` not followed by `/<another-segment>` returns 0 in content/ + src/ (excluding helper + helper test + this goal file).
41+- All 419+ tests still pass; new helper test adds 6-8 cases. Tests that hard-code blog URLs in assertions get updated to the new shape mechanically.
42+- /sama/v2/verify still reports 7/7 ✓ (anti-fudge).
43+- This goal file (goals/blog-date-prefix.md) flipped from status: pending → shipped in the final commit before deploy, with merge_sha filled.
44+- Deployed; live-verify with curl:
45+ * /blog → 200 with HTML index, row links use /blog/<yyyy-mm>/<slug>
46+ * /blog/2026-05/sama-v2-on-ramp-gap → 200 with content
47+ * /blog/sama-v2-on-ramp-gap → 301 with Location /blog/2026-05/sama-v2-on-ramp-gap
48+ * /sitemap.xml | grep -E '/blog/[0-9]{4}-[0-9]{2}/' | wc -l → matches the count of ALL_POSTS
49+ * /sama/v2/verify → 7/7 ✓
50+- HYPOTHESIS TEST CLAIM: wall-clock from first commit on branch to live-verify pass is ≤30 minutes. Wall-clock recorded in the postmortem post that follows. Boundary test: confirm pattern portability when data-driven, OR document the cost when it isn't portable.
51+
52+Constraints (anti-fudge):
53+- Helper imports ALL_POSTS from a31_blog.ts (Layer-1 importing Layer-0 is allowed per SAMA). No filesystem reads, no fetch, no I/O — the data is already resolved at module load.
54+- Sort replacements by slug length DESC in the migration to avoid prefix collisions (if one slug is `foo` and another is `foo-bar`, replace `/blog/foo-bar` first so the subsequent `/blog/foo` substitution doesn't accidentally match inside the first one).
55+- The yyyymm extracted from `date` MUST match what the new handler validates against — both compute via `date.slice(0, 7)` for consistency.
56+- Don't change /blog (the landing page) — only /blog/<slug> moves.
57+- If a future post slug ever conflicts with a literal yyyy-mm string (e.g. someone names a slug `2026-05`), the new pattern would 404; out of scope (anti-fudge says don't pre-solve hypotheticals).
58+- No alias mode — 301 forces consolidation.
59+- Site language English-only.
60+- GitHub flow via flatpak-spawn.
61+- Do NOT change any §4 verifier logic.
62+- PR body MUST include this verbatim /goal under a "## /goal" heading per feedback_goal_authoring_workflow.md.
63+
64+Load-bearing files to read FIRST:
65+- src/b32_sama_discipline_url_redirect.ts (the second template — close kin to this one, just with fixed enum where this one has data-lookup)
66+- src/b32_git_url_redirect.ts (the original template — same shape)
67+- src/a31_blog.ts (ALL_POSTS — the data source the helper imports)
68+- src/d21_handlers_fallback.ts (where the new redirect block lands)
69+- src/d21_app.ts (Bun route /blog/:slug to be replaced with /blog/:yyyymm/:slug; sitemap handler ALL_POSTS mapping)
70+- The blog detail handler — find it via grep for "ALL_POSTS.find" or "blogSlugHandler"
71+- /blog/sama-v2-second-url-refactor-postmortem (the hypothesis being tested — both subclaims #1 fixed-enum and #2 data-driven were predicted there)