How to Migrate from WordPress to EmDash CMS

How to Migrate from WordPress to EmDash CMS

EmDash has a built-in WordPress import wizard — this isn't a manual copy-paste migration. It reads a standard WordPress export file, converts Gutenberg blocks to EmDash's structured content format, and preserves your taxonomy hierarchy, custom fields, and media. This walks through the exact process.

Table of Contents
  1. Before You Begin
  2. Choosing an Import Method
  3. Step-by-Step: WXR File Import
  4. What Happens to Your Content Structurally
  5. Gutenberg Blocks Become Portable Text
  6. Classic Editor Content
  7. Status Mapping
  8. Taxonomy Hierarchy Is Preserved
  9. Custom Fields and ACF
  10. Handling URL Redirects
  11. Troubleshooting
  12. "XML parsing error"
  13. Media download failures
  14. Field type conflicts
  15. Large exports (over 100MB)
  16. Frequently Asked Questions
  17. Will my WordPress plugins and their functionality carry over?
  18. Is the import safe to run more than once?
  19. Do I need to take my WordPress site offline during migration?
  20. What if some content doesn't convert cleanly?
  21. The Bottom Line

Before You Begin

  • Export your content: in WordPress, go to Tools → Export and download a complete export file (.xml).
  • Back up your site and keep WordPress running until you've verified the migration succeeded.

Choosing an Import Method

EmDash supports three import methods, and for most migrations, the WXR file upload is the right one — it captures everything, including drafts and private posts, with no authentication required:

  • WXR file upload — best for complete migrations; includes drafts; no auth required.
  • WordPress.com OAuth — best specifically for WordPress.com-hosted sites; includes drafts; requires OAuth.
  • REST API probe — best for checking content before export; published content only; auth optional.
Read also:

Step-by-Step: WXR File Import

  1. Export from WordPress. In your WordPress admin, go to Tools → Export → All content → Download Export File.
  2. Open the Import wizard. In EmDash, go to Admin → Settings → Import → WordPress.
  3. Upload your export file. Drag and drop your .xml file or click to browse — it's parsed directly in your browser.
  4. Review detected content. The wizard shows exactly what it found:
Found in export:
├── Posts: 127 → posts [New collection]
├── Pages: 12  → pages [Add fields]
└── Media: 89 attachments
emdashkits.com
  1. Configure mappings. Toggle which post types to import — EmDash automatically creates new collections for unmapped post types, adds missing fields to existing collections, and warns about field type conflicts before you proceed.
  2. Execute the import. Click Import Content and watch progress as each item processes.
  3. Import media (optional). After content imports, choose whether to download media files — EmDash downloads from your WordPress URLs, deduplicates by content hash, and rewrites URLs in your content automatically.
Re-running the import is safe. Items are matched by WordPress ID, so you won't create duplicates.

What Happens to Your Content Structurally

Gutenberg Blocks Become Portable Text

EmDash converts Gutenberg blocks to Portable Text, a structured content format, preserving inline marks, heading levels, image references, list types, quote citations, code language attributes, and embed URLs. Blocks EmDash doesn't recognize are preserved as raw HTML for manual review rather than silently dropped.

Classic Editor Content

If your site still has Classic Editor (pre-Gutenberg) posts, that HTML converts to Portable Text blocks too, with inline styles like bold, italic, and links becoming marks on the corresponding text spans.

Status Mapping

WordPress statuses map directly to EmDash's equivalent states:

  • publish → published
  • draft → draft
  • pending → pending
  • private → private
  • future → scheduled
  • trash → archived

Taxonomy Hierarchy Is Preserved

Hierarchical WordPress categories import with their parent-child structure intact, and flat tags import as flat taxonomy terms — nothing gets flattened or restructured during the move.

Custom Fields and ACF

During the analysis phase, EmDash detects custom fields and infers appropriate EmDash field types from the actual values — numeric strings become `number`, `"1"`/`"0"`/`"true"`/`"false"` become `boolean`, ISO dates become `date`, and WordPress IDs like `_thumbnail_id` become `reference` fields. SEO plugin fields (Yoast, etc.) map into a dedicated `seo` object automatically. Internal WordPress fields prefixed `_edit_` or `_wp_` are hidden by default rather than cluttering your new schema.

ACF repeater fields and flexible content import as raw JSON — you'll want to create matching Portable Text or array fields in EmDash afterward to give that data proper structure rather than leaving it as opaque JSON.

Handling URL Redirects

After import, EmDash generates a redirect map so old WordPress URLs keep working:

{
  "redirects": [
    { "from": "/?p=123", "to": "/posts/hello-world" },
    { "from": "/2024/01/hello-world/", "to": "/posts/hello-world" },
    { "from": "/category/news/", "to": "/categories/news" }
  ],
  "feeds": [
    { "from": "/feed/", "to": "/rss.xml" },
    { "from": "/feed/atom/", "to": "/atom.xml" }
  ]
}
emdashkits.com

Apply this to Cloudflare redirect rules, your hosting platform's redirect configuration, or Astro's own `redirects` option in `astro.config.mjs` — skipping this step is the single most common cause of a real SEO ranking drop after a CMS migration, so don't treat it as optional.

Troubleshooting

"XML parsing error"

Your export file may be corrupted or incomplete — re-export from WordPress and try again.

Media download failures

Some images may be behind authentication or have since moved. The import continues regardless, and failed URLs are logged for manual handling afterward.

Field type conflicts

If an existing EmDash collection has a field with an incompatible type, the wizard flags the conflict directly — rename the EmDash field, change the WordPress field mapping, or delete and recreate the collection.

Large exports (over 100MB)

  • Export post types separately in WordPress rather than one giant file.
  • Import each file sequentially instead of all at once.
  • Use the EmDash CLI with `--resume` for reliability on very large migrations.

Frequently Asked Questions

Will my WordPress plugins and their functionality carry over?

Content and data from plugin fields generally import as custom fields, but plugin behavior itself doesn't transfer automatically. EmDash's plugin system is WordPress-inspired specifically to give custom functionality a defined porting path, but that's a separate step from the content import.

Is the import safe to run more than once?

Yes — items are matched by their original WordPress ID, so re-running an import after fixing a field conflict won't create duplicate content.

Do I need to take my WordPress site offline during migration?

No — the import reads from an export file (or probes via API), so your WordPress site can stay live and serving visitors throughout the entire migration and verification process.

What if some content doesn't convert cleanly?

Blocks EmDash doesn't recognize are preserved as raw HTML rather than lost, so you can review and manually convert them, or build a custom Portable Text component to render that specific HTML block type going forward.

The Bottom Line

EmDash's WordPress import is a genuine, guided migration path — not a manual re-entry project — handling posts, pages, media, taxonomies, custom fields, and redirects through one wizard. The most important step most teams skip is applying the generated redirect map; do that before flipping DNS. See our 10-minute EmDash setup guide if you haven't created your EmDash project yet.

Share

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

TL;DR — A custom session-cookie login flow appeared to succeed on localhost (the OTP verified, the response looked fine) but every subsequent request to a login-gated page treated the visitor as logged out. Identical code worked fine on the live HTTPS site. The cookie's Secure attribute was hardcoded to true — and per the cookie spec, browsers never store or send a Secure cookie over a plain, non-HTTPS connection.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Fix
  4. Lessons learned

Diagnostic

Check the actual Set-Cookie response header and the browser's own cookie storage panel — on localhost over http://, the cookie is sent by the server but never actually stored by the browser.

Root cause

// before -- assumes the app is always served over HTTPS
setCookie("session", token, { secure: true, httpOnly: true });
emdashkits.com

A cookie config that quietly assumes "we're always on HTTPS" breaks the instant you test over plain HTTP, which local dev servers commonly are.

Read also:

Fix

// after -- derive secure from the actual request protocol
const isHttps = request.url.startsWith("https://");
setCookie("session", token, { secure: isHttps, httpOnly: true });
emdashkits.com

Lessons learned

  • Any Secure-flagged cookie needs to key off the real request scheme, not an assumption baked in once at cookie-creation time.
  • "Works in production, silently fails in local dev" is a strong signal to check cookie flags before anything else in an auth flow.
  • Check other cookies in the same codebase for the same hardcoded assumption — if one cookie has this bug, sibling cookies set the same way are worth auditing too.
Share
Previous Article

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Comments

Write a comment

Related Articles

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

July 16, 2026

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

Google Analytics (GA4) Not Tracking Anything and No Console Errors: Check Your CSP First

TL;DR — GA4's gtag.js snippet was installed correctly, the page loaded with no visible JavaScript error, and GA4's real-time report still showed zero activity. The CMS's default Content-Security-Policy had no allowance for analytics domains and no config option to add one — so every request to Google's tracking endpoints was blocked at the browser level before it could fail loudly.

Table of Contents
  1. Diagnostic
  2. Root cause
  3. Lessons learned

Diagnostic

Check the browser's dedicated CSP violation reporting, not the regular console error list — CSP blocks are reported through their own channel, not thrown as normal script errors, so "no console errors" doesn't mean nothing was blocked.

Root cause

The CSP's script-src and connect-src directives had no entry for googletagmanager.com or google-analytics.com, and the CMS exposed no configuration surface to add one — the only way in was patching the CSP directives directly.

// patch-package: add analytics domains to the existing CSP directives
scriptSrc.push("https://www.googletagmanager.com");
connectSrc.push("https://www.google-analytics.com", "https://www.googletagmanager.com");
emdashkits.com
Read also:

Lessons learned

  • "No console errors" is not proof nothing was blocked — CSP violations live in their own reporting surface and are easy to miss if you're only scanning for red error text.
  • Before adding any third-party script tag to a site with a CSP already in place, check the CSP's directives first rather than assuming a silently-empty analytics dashboard means a snippet-installation mistake.
Share
Previous Article

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Next Article

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Comments

Write a comment

Related Articles

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

July 16, 2026

Login Works in Production but Fails on localhost: The secure Cookie Flag Gotcha

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

July 16, 2026

Native Module Build Fails on Shared Hosting (node-gyp, Old glibc/Python): The .npmrc Fix

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)

July 16, 2026

Uploaded Images Disappear After Every Deploy on Shared Hosting (and Other Reverse-Proxy Gotchas)