How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

How-to Migration

Migrate a WordPress site's content to pages and blogs

Read the posts and pages from WordPress's REST API, write them into a Core dna blog and pages through Core dna's, keep their dates and categories, and produce the redirect map as you go.

Context

Context. A WordPress site is moving to Core dna. Its posts belong in a blog and its pages in pages, with their original dates, categories and URLs mapped. Copying by hand stops being an option after a few dozen posts. Both systems have a REST API, so the migration is a script that reads one and writes the other, and that can be run on staging until the result is right and then once more for real. Content only: layouts and templates are built first, and the script fills them.

Steps

  1. Build the target first: the blog, its post template and layout, the page layouts, and the categories and tags, so that every imported record lands somewhere that renders.
  2. Map the taxonomy. Create each WordPress category as a Core dna category and keep a table of old id to new id. The script uses it to tag each post.
  3. Read WordPress page by page from /wp-json/wp/v2/posts and /wp-json/wp/v2/pages, a hundred at a time.
  4. Clean the HTML on the way: WordPress shortcodes such as [caption] mean nothing to Core dna, and block comments (<!-- wp:paragraph -->) are noise.
  5. Write each post into the blog, published at its original date, with its categories.
  6. Write each old URL and its new path to the redirect file as you go (bulk-import redirects).
  7. Bring the images across after the text: post bodies still point at the old site's media until they are imported (import a legacy site's externally hosted images).
  8. Check counts on both sides, a sample of posts side by side, and the redirect test over the whole map.

Code

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.

Why this and not that

Importing WordPress's XML export file means parsing its format; its REST API returns clean JSON with the rendered HTML, and it can be read from a live site without anyone exporting anything. Writing through Core dna's REST API keeps every record valid, revisioned and indexed exactly as if an editor had created it, which a database import could not. Running against staging first is the point of a script: run it, look, delete, fix, run again, and only then run it once on production. Keep the posts' original dates by publishing at that date, or every post appears to have been written on the migration day and the blog's order is lost.

Go deeper

Was this page helpful?

Your answer goes to the docs team.