JavaScript
// migrate-posts.mjs: node migrate-posts.mjs (run against staging first)
import { appendFileSync } from 'node:fs';
const WP = 'https://old.example.com';
const CDNA = process.env.SITE_URL; // https://staging.example.com
const KEY = process.env.CDNA_API_KEY; // never commit the key
const BLOG_ID = 3; // the target blog
const BLOG_PATH = '/blog'; // its URL, to build the redirect targets
const CATEGORY_MAP = { 5: 41, 7: 42, 12: 43 }; // WordPress category id -> Core dna category id
const clean = html => html
.replace(/<!--\s*\/?wp:[^>]*-->/g, '') // block editor comments
.replace(/\[(\/?)caption[^\]]*\]/g, '') // caption shortcodes
.trim();
for (let page = 1; ; page++) {
const res = await fetch(`${WP}/wp-json/wp/v2/posts?per_page=100&page=${page}&_fields=id,slug,date_gmt,title,content,categories,link`);
if (res.status === 400) break; // past the last page
const posts = await res.json();
if (!posts.length) break;
for (const post of posts) {
const body = {
slug: post.slug,
published: post.date_gmt + '+00:00', // keep the original date
categories: post.categories.map(id => CATEGORY_MAP[id]).filter(Boolean),
revisions: { name: post.title.rendered, text: clean(post.content.rendered) }
};
const out = await fetch(`${CDNA}/cdna-api/blogs/post/${BLOG_ID}`, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (!out.ok) { console.error(out.status, post.slug, await out.text()); continue; }
appendFileSync('redirects.csv', `${new URL(post.link).pathname},${BLOG_PATH}/${post.slug}\n`);
}
}
Pages follow the same loop against /wp-json/wp/v2/pages, written with POST /cdna-api/pages/page with their parent and layout, and their HTML in the layout's body area (pages in the API reference). Check the new path of the first imported post on the site before trusting the redirect file: a blog's post URLs follow the blog's own path.