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:32 -04:00
co-authored by Claude Sonnet 5
parent 0703699929
commit af9deb5ec8
2 changed files with 77 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
5 /features
7 /contact-us
11 /about-us
21 /blog/21
28 /blog/inbound-calls-outbound-calls-and-email-blended
36 /blog/agents-anywhere
67 /comparisons
69 /vicihost-vs-justgocloud
75 /vicihost-vs-ytel
82 /vicihost-vs-five9
94 /contact-form-error
96 /contact-form-success
107 /blog/agent-avatar-soundboards
131 /blog/high-level-data-encryption
157 /skills-based-routing-with-agent-ranking
164 /outbound-agent-controlled-broadcast-and-predictive-dialing
168 /integrated-call-recording
171 /three-way-calling-within-the-agent-screen
174 /scheduled-callbacks-agent-only-and-anyone
179 /web-configurable-ivrs-and-voicemail-boxes
182 /virtually-limitless-campaigns-lists-ivrs-inbound-queues
185 /immediate-or-scheduled-website-callbacks
188 /real-time-reports-with-click-to-listen-to-monitor-agent-phone-calls
191 /easy-importing-of-calling-lists-through-the-web-by-api-or-scheduled-by-ftp
203 /integrated-web-based-agent-phone-included-requires-no-agent-setup
206 /local-toll-free-and-international-inbound-phone-numbers-available
216 /full-usa-canada-and-uk-regulatory-compliance
219 /cellphone-filtering-for-tcpa-compliance
223 /ability-to-have-recordings-automatically-transferred-to-an-external-ftp-site
226 /quality-control-module-available
230 /pbx-features-allow-you-to-use-your-system-as-your-office-pbx
235 /included-support-time
238 /no-hidden-fees
241 /99-9-up-time-guarantee
244 /scalable-to-hundreds-of-logged-in-agents
247 /dedicated-dialing-server-hardware
250 /single-server-guaranteed-to-handle-at-least-25-agents-at-31-line-ratio
253 /moving-hosted-to-on-site-or-on-site-to-hosted
256 /month-to-month-terms-no-long-term-contracts
259 /agent-scripting-with-customer-data
262 /system-wide-per-campaign-and-inbound-dnc-lists
274 /internal-chat-and-broadcast-messaging-to-agents
286 /available-languages
294 /zoiper-webphone
301 /blog/crm-integration
324 /auto-generate-call-lists-based-on-dropped-inbound-queued-calls
327 /computer-ip-address-access-restrictions-for-web-resources
330 /dozens-of-standard-reports-which-can-be-emailed-out-on-a-set-schedule
350 /share-lead-data-across-vicidial-clusters-instantly-when-calls-are-transferred
354 /remote-api-control-of-agent-screens
372 /gdpr-compliance
376 /inbound-and-advanced-forecasting-reports
380 /inbound-post-call-customer-survey
383 /inbound-queue-preserve-place-in-line
386 /custom-data-field-forms-with-switchable-forms
390 /dnc-com-lead-filtering-and-inbound-call-filtering
395 /inbound-queue-closing-time-features
398 /outbound-callerid-groups-per-state-or-per-areacode
417 /call-quota-lead-ranking-for-multiple-contact-attempts
423 /multiple-configurable-options-for-leaving-messages-on-customer-voicemail-boxes
437 /two-factor-authentication-for-admin-web-users
441 /multi-country-international-dnc-list-filtering
452 /24-hour-called-count-limit-campaign-feature
475 /your-caller-id-numbers-and-spamscam-labels
+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' });
@@ -136,6 +137,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
@@ -251,6 +263,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 });