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
+60
View File
@@ -0,0 +1,60 @@
5 /features
7 /contact-us
11 /about-us
21 /blog/21
28 /blog/inbound-calls-outbound-calls-and-email-blended
94 /contact-form-error
96 /contact-form-success
123 /vicihost-agent-avatar-soundboards
151 /consulting
155 /remote-installations
159 /customization
164 /system-planning
167 /integration
171 /hardware
173 /smb-hardware
177 /enterprise-hardware
179 /accessories
194 /partners
204 /hosting
210 /open-source
219 /training
223 /kiax2-tutorial
237 /zoiper-classic-tutorial
255 /legal
258 /gplv2-license
260 /agplv2-license
262 /trademark-policy
275 /blog/most-popular-open-source-contact-center
312 /third-party-add-ons
322 /vicihost-high-level-data-encryption
328 /zoiper-webphone
332 /queuemetrics-reporting
338 /dnc-com-integration
343 /cepstral-text-to-speech
350 /orecx-audio-and-screen-recording
355 /sangoma-call-progress-analysis
360 /rankminer-call-analytics
365 /newsroom
373 /vicidial-partners-with-rankminer-and-its-customer-insight-agent-insight-products-to-increase-call-center-effectiveness
382 /amfeltec-corporation-announces-completion-of-its-line-of-system-timers
387 /vicidial-wins-big-with-sangoma-call-progress-analyzer
390 /netborder-call-progress-analysis-engine-integrates-with-vicidial-so-agents-can-spend-more-time-with-paying-customers
406 /blog/vicidial-is-used-in-many-different-industries-and-organizations
427 /vicidial-declared-one-of-the-most-promising-contact-center-technologies-in-2016-by-cio-review-magazine
453 /demo-website-and-free-trial
459 /vicidial-placed-on-gartners-softwareadvice-leaders-list-for-call-centers
466 /video-what-is-the-vicidial-open-source-contact-center-solution
470 /blog/video-what-is-the-vicidial-open-source-contact-center-solution
495 /privacy-policy
502 /vicidial-screenshots
530 /vicidial-placed-on-softwareadvices-frontrunners-list-for-call-centers
540 /vicidial-placed-on-getapps-top-20-list-for-call-centers
544 /vicidial-declared-a-top-20-open-source-solution-provider
553 /vicidial-named-to-insights-success-10-most-innovative-contact-center-solution-providers-to-watch-in-2018
566 /wordpress-call-me-plugin
569 /vicihost-crm-integrations
578 /khomp-answering-machine-detection
583 /the-vicidial-mobile-apps-for-android-and-ios
625 /vicidial-ranks-high-in-2021-lists-from-getapp-softwareadvice-and-capterra
656 /vicidial-white-paper
+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 PAGES_DEST = path.join(ROOT, 'src/content/pages');
const POSTS_DEST = path.join(ROOT, 'src/content/posts'); const POSTS_DEST = path.join(ROOT, 'src/content/posts');
const NAV_DEST = path.join(ROOT, 'src/data/nav.json'); 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' }); 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}`])); 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() { async function copyAttachments() {
// Copy the whole uploads tree rather than just the 82 attachment rows: WordPress generates // 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 // multiple resized variants per image (e.g. "-1008x1024.png") that content <img> tags/srcsets
@@ -241,6 +253,7 @@ async function main() {
await copyAttachments(); await copyAttachments();
const linkMap = await buildLinkMap(conn); const linkMap = await buildLinkMap(conn);
const unresolved = new Set(); const unresolved = new Set();
await writeRedirectMap(linkMap);
await exportPostType(conn, 'page', PAGES_DEST, { attachmentPaths, thumbnails, linkMap, unresolved }); await exportPostType(conn, 'page', PAGES_DEST, { attachmentPaths, thumbnails, linkMap, unresolved });
await exportPostType(conn, 'post', POSTS_DEST, { attachmentPaths, thumbnails, linkMap, unresolved }); await exportPostType(conn, 'post', POSTS_DEST, { attachmentPaths, thumbnails, linkMap, unresolved });