syntaxai/tdd.md · main · scripts / migrate-blog-urls.ts
// One-shot migration: rewrite /blog/<slug> → /blog/<yyyy-mm>/<slug>
// across every .md and .ts file in content/ + src/, using ALL_POSTS
// as the single source of truth for each post's date.
//
// Sorts replacements by slug length DESCENDING so longer slugs
// substitute first — prevents the prefix-collision footgun where
// /blog/foo would match inside /blog/foo-bar before /blog/foo-bar
// got its turn.
//
// Safe to re-run: replacement looks for `/blog/<slug>` literal, and
// after one pass each occurrence becomes `/blog/<yyyymm>/<slug>` —
// which doesn't contain `/blog/<slug>` as a substring (the 7th char
// differs). No double-prefixing.
import { Glob } from "bun";
import { ALL_POSTS } from "../src/a31_blog.ts";
const root = "/var/home/scri/Documents/tdd.md";
const replacements: Array<[string, string]> = ALL_POSTS
.map((p) => {
const yyyymm = p.date.slice(0, 7);
return [`/blog/${p.slug}`, `/blog/${yyyymm}/${p.slug}`] as [string, string];
})
// Longest first to avoid /blog/foo matching inside /blog/foo-bar.
.sort((a, b) => b[0].length - a[0].length);
const targets: string[] = [];
for await (const path of new Glob("content/**/*.md").scan({ cwd: root })) {
targets.push(path);
}
for await (const path of new Glob("src/**/*.ts").scan({ cwd: root })) {
// Don't migrate the helper itself or its sibling test — they
// legitimately reference the old shape (regex pattern + test cases).
if (path === "src/b32_blog_date_url_redirect.ts") continue;
if (path === "src/b32_blog_date_url_redirect.test.ts") continue;
targets.push(path);
}
for await (const path of new Glob("goals/*.md").scan({ cwd: root })) {
// Don't touch the migrate-historical-goals or this PR's goal file —
// those describe historical state and would drift.
if (path === "goals/blog-date-prefix.md") continue;
targets.push(path);
}
let totalFilesChanged = 0;
let totalReplacements = 0;
for (const target of targets) {
const fullPath = `${root}/${target}`;
const original = await Bun.file(fullPath).text();
let modified = original;
let fileReplacements = 0;
for (const [oldStr, newStr] of replacements) {
const before = modified;
modified = modified.replaceAll(oldStr, newStr);
if (modified !== before) {
// Count how many replacements happened on THIS pair.
const occurrences = before.split(oldStr).length - 1;
fileReplacements += occurrences;
}
}
if (modified !== original) {
await Bun.write(fullPath, modified);
totalFilesChanged += 1;
totalReplacements += fileReplacements;
console.log(` ${target}: ${fileReplacements} substitutions`);
}
}
console.log(`\nDone. ${totalFilesChanged} files modified, ${totalReplacements} substitutions.`);