Rewrite WordPress cross-page links to site-relative paths

Content linked between pages using absolute WordPress URLs -- 100
occurrences across 39 files, many against wp.vicidial.com, the old
WordPress backend host that won't outlive this migration, the rest
against www.vicidial.com in ?page_id=N form.

The export now resolves those IDs against the published page/post set and
emits /slug (or /blog/slug) instead. All 100 resolved; links to
vicihost.com are a separate site and deliberately untouched.

Verified every internal href in the built output corresponds to a page
that actually exists -- no broken links.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 19:33:45 -04:00
co-authored by Claude Opus 5
parent b5c33ed683
commit 73399911cc
40 changed files with 118 additions and 84 deletions
+37 -3
View File
@@ -82,6 +82,32 @@ function rewriteImageUrls(html, fromDir) {
);
}
// Content links between pages are stored as absolute WordPress URLs, many of them against
// wp.vicidial.com -- the old WordPress backend host, which won't outlive this migration.
// Turn them into site-relative paths. IDs with no published page/post behind them (deleted
// pages, a stray revision) are left alone: they are already broken on the live site.
function rewriteInternalLinks(html, linkMap, unresolved) {
return html.replace(
/https?:\/\/(?:www\.|wp\.)?vicidial\.(?:com|net)\/\?(?:page_id|p)=(\d+)/gi,
(match, id) => {
const target = linkMap.get(Number(id));
if (!target) {
unresolved.add(id);
return match;
}
return target;
}
);
}
async function buildLinkMap(conn) {
const [rows] = await conn.execute(
`SELECT ID, post_type, post_name FROM ${TABLE_PREFIX}posts
WHERE post_status = 'publish' AND post_type IN ('page', 'post')`
);
return new Map(rows.map((r) => [r.ID, r.post_type === 'post' ? `/blog/${r.post_name}` : `/${r.post_name}`]));
}
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
@@ -91,7 +117,7 @@ async function copyAttachments() {
console.log(`Copied uploads tree to ${UPLOADS_DEST}`);
}
async function exportPostType(conn, postType, destDir, { attachmentPaths, thumbnails }) {
async function exportPostType(conn, postType, destDir, { attachmentPaths, thumbnails, linkMap, unresolved }) {
const [rows] = await conn.execute(
`SELECT ID, post_title, post_name, post_date, post_content, post_excerpt
FROM ${TABLE_PREFIX}posts WHERE post_type = ? AND post_status = 'publish' ORDER BY post_date DESC`,
@@ -107,6 +133,7 @@ async function exportPostType(conn, postType, destDir, { attachmentPaths, thumbn
html = wpautop(html);
}
html = rewriteImageUrls(html, destDir);
html = rewriteInternalLinks(html, linkMap, unresolved);
const markdown = turndown.turndown(html);
let excerpt = fixLiteralNewlines(row.post_excerpt || '');
@@ -174,8 +201,15 @@ async function main() {
const thumbnails = new Map(thumbRows.map((r) => [r.post_id, r.meta_value]));
await copyAttachments();
await exportPostType(conn, 'page', PAGES_DEST, { attachmentPaths, thumbnails });
await exportPostType(conn, 'post', POSTS_DEST, { attachmentPaths, thumbnails });
const linkMap = await buildLinkMap(conn);
const unresolved = new Set();
await exportPostType(conn, 'page', PAGES_DEST, { attachmentPaths, thumbnails, linkMap, unresolved });
await exportPostType(conn, 'post', POSTS_DEST, { attachmentPaths, thumbnails, linkMap, unresolved });
if (unresolved.size) {
console.warn(`Left ${unresolved.size} link(s) pointing at WordPress IDs with no published page: ${[...unresolved].join(', ')}`);
}
await exportNav(conn);
await conn.end();