Redirect old WordPress ?page_id=/?p= links to their new paths

Both sites used WordPress's default plain permalinks -- no
permalink_structure was ever set -- so every inbound link anyone has
bookmarked, linked from another site, or has indexed in search is of the
form ?page_id=N or ?p=N. Without a redirect those all land on the
homepage with no indication of where the content went.

scripts/export-wp.mjs now also writes redirects.map: a full ID -> new
path table for every published page/post (not just the ones referenced
from other content, since any of them could have outside inbound links),
in the "id path" format Apache's txt RewriteMap wants. Regenerated on
every export run alongside the content itself.

The :443 vhost looks up page_id/p from the query string against that
map and 301s to the resolved path, stripping the old query string.
Unmapped IDs (deleted pages, a stray revision) fall through to the
homepage rather than a bare 404. The redirect target is host-relative,
so it works correctly under any ServerAlias this vhost answers for
(including the still-dormant www.vicidial.com/vicidial.com aliases,
with no changes needed once DNS is eventually pointed here).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 20:50:31 -04:00
co-authored by Claude Sonnet 5
parent 8d8eb0e60f
commit efa372b231
2 changed files with 73 additions and 0 deletions
+13
View File
@@ -30,6 +30,7 @@ const UPLOADS_DEST = path.join(ROOT, 'src/assets/uploads');
const PAGES_DEST = path.join(ROOT, 'src/content/pages');
const POSTS_DEST = path.join(ROOT, 'src/content/posts');
const NAV_DEST = path.join(ROOT, 'src/data/nav.json');
const REDIRECTS_DEST = path.join(ROOT, 'redirects.map');
const turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
@@ -137,6 +138,17 @@ async function buildLinkMap(conn) {
return new Map(rows.map((r) => [r.ID, r.post_type === 'post' ? `/blog/${r.post_name}` : `/${r.post_name}`]));
}
// Apache RewriteMap (txt type) file: "<id><space><path>" per line, one for every published
// page/post, not just the ones referenced from other content -- anyone on the web could have
// bookmarked or linked to any of them via the old ?page_id=N / ?p=N plain-permalink URLs.
async function writeRedirectMap(linkMap) {
const lines = [...linkMap.entries()]
.sort(([a], [b]) => a - b)
.map(([id, target]) => `${id} ${target}`);
await fs.writeFile(REDIRECTS_DEST, `${lines.join('\n')}\n`);
console.log(`Wrote ${linkMap.size} redirect entries to ${REDIRECTS_DEST}`);
}
async function copyAttachments() {
// Copy the whole uploads tree rather than just the 82 attachment rows: WordPress generates
// multiple resized variants per image (e.g. "-1008x1024.png") that content <img> tags/srcsets
@@ -241,6 +253,7 @@ async function main() {
await copyAttachments();
const linkMap = await buildLinkMap(conn);
const unresolved = new Set();
await writeRedirectMap(linkMap);
await exportPostType(conn, 'page', PAGES_DEST, { attachmentPaths, thumbnails, linkMap, unresolved });
await exportPostType(conn, 'post', POSTS_DEST, { attachmentPaths, thumbnails, linkMap, unresolved });