Use the EmDash admin to analyze a WordPress site, prepare compatible collections, import content, copy media, and rewrite URLs. Keep the WordPress site available until the imported site has passed verification.
Before you begin
Prepare the following items:
- A backup of the WordPress database and
wp-content/uploads. - An EmDash site with storage configured if you plan to copy media.
- An EmDash administrator account. WordPress import requires the
import:executepermission. - Either a complete WordPress eXtended RSS (WXR) export or the EmDash Exporter plugin installed on the WordPress site.
Record the current permalink structure, canonical origin, redirects, menus, active post types, taxonomies, and any plugin-owned fields that the new site must preserve.
Choose an import method
| Method | Use it when | Included data |
|---|---|---|
| WXR upload | You can export from Tools → Export | Data represented in the export, including posts, pages, custom post types, taxonomy terms, reusable blocks, authors, and attachment URLs |
| EmDash Exporter | You control the WordPress site and can install the exporter | Authenticated content plus supported comments, menus, site settings, SEO fields, taxonomies, and media metadata |
Entering a site URL without the exporter only probes the public WordPress REST API. The probe can detect WordPress and count public posts, pages, and media, but it cannot perform a direct REST import.
Export a WXR file
In WordPress, open Tools → Export, select All content, and download the XML export file. A WXR file points to media on the WordPress origin; it does not contain the attachment bytes.
Connect the EmDash Exporter
In WordPress, open Tools → EmDash Migration and generate a migration key. Paste the key into the EmDash import page.
You can also enter the WordPress site URL. When the exporter is detected, authorize EmDash through the WordPress application-password screen. On local HTTP development sites, enter the WordPress username and application password manually because the authorization callback requires HTTPS.
Import through the admin
-
Open Import WordPress in the EmDash sidebar, or visit
/_emdash/admin/import/wordpress. -
Upload the WXR file, paste the exporter migration key, or enter the WordPress URL.
-
Review the analysis. For each WordPress post type, confirm the target collection, required fields, and compatibility with any existing fields.
-
Map WordPress authors to EmDash users. An unmapped source author can be represented by a guest byline, so presentation credit does not depend on granting an EmDash login.
-
For an exporter import, choose whether to import menus, site title and tagline, logo and favicon, and supported SEO values. These switches are not part of the WXR path.
-
Start the import. EmDash creates missing compatible collections and fields before it creates entries.
-
If attachments were found, run the media step. EmDash downloads the source bytes, stores them through the configured storage adapter, and rewrites matching content URLs.
Conversion behavior
Statuses
The importer maps WordPress statuses as follows:
| WordPress | EmDash |
|---|---|
publish | published |
draft | draft |
pending | draft |
private | draft |
future, trash, or an unknown status | draft |
Scheduled dates and WordPress visibility rules are not reconstructed from these statuses. Review every imported draft before publication.
Rich text and reusable blocks
Gutenberg markup is converted to Portable Text. Classic HTML is processed by the same converter. Inspect complex blocks, embeds, shortcodes, page-builder markup, and plugin-defined blocks in the rendered EmDash site.
WXR wp_block entries are imported as sections. They are not imported into an ordinary content collection.
Collections and fields
The default post-type mappings include post to posts and page to pages. Custom post types use a sanitized collection slug. The prepare step adds the standard title, Portable Text content, excerpt, and featured-image fields that the analysis requires.
Existing fields with compatible types are reused. A type mismatch blocks that collection mapping; the importer does not coerce an existing field to another type.
Custom fields and SEO data
WXR analysis reports non-internal post-meta keys and suggests field names and types, but the WXR database import writes the standard imported fields rather than copying every arbitrary meta key. Treat the analysis as a checklist for fields that may need a separate conversion.
The EmDash Exporter path can copy custom meta and Advanced Custom Fields (ACF) values when the target collection has matching fields. It can also create and populate the supported featured-image and Yoast or Rank Math SEO fields when their import switches are enabled. Review serialized PHP values, repeaters, flexible content, and plugin-specific structures rather than assuming their WordPress representation matches an EmDash field type.
Authors and bylines
A mapped WordPress author becomes the entry owner. Presentation credits use bylines. If no user mapping exists, EmDash creates or reuses a guest byline based on the WordPress author identity.
Taxonomies
Categories and tags are imported into the matching EmDash taxonomy definitions. The exporter path can create definitions for custom taxonomies. A WXR custom taxonomy without a matching EmDash definition is reported as missing and its assignments are skipped.
Media
Media deduplication compares a SHA-1 hash of the downloaded bytes. An existing media row with the same hash is reused. Filenames and WordPress IDs are not the deduplication key.
The source site must remain reachable while EmDash downloads attachments. Redirects from an attachment URL are validated before they are followed.
Retry a partial import
The admin import is designed to skip existing entries, but its matching boundary matters:
- Existing content is matched by collection, slug, and locale, not by WordPress ID.
- If a source slug changed between attempts, inspect both entries and resolve the duplicate manually.
- Rerunning the media step reuses already stored byte-identical files by content hash.
- The exporter flow sends bounded chunks. Restarting the browser flow reruns content pages and rebuilds relationship maps from skipped entries.
The admin does not persist a WXR resume file. The --resume flag on emdash import wordpress belongs to a separate CLI workflow that writes converted JSON files to disk; it does not resume an admin database import.
Verify before cutover
Check the imported site against the source:
- Count entries by post type and status.
- Open representative Gutenberg, Classic Editor, shortcode, and page-builder content.
- Confirm featured images and inline media resolve from EmDash URLs.
- Check authors, bylines, categories, tags, custom taxonomies, and translations.
- Test menus, comments, site identity, and SEO fields when imported through the exporter.
- Crawl old public URLs and prepare redirects for every changed route.
- Verify drafts remain inaccessible to logged-out visitors.
Keep the backup and WordPress deployment until the new site has run successfully in production and rollback is no longer required.
Troubleshooting
The XML file cannot be parsed
Export the file again from Tools → Export and confirm the download completed. Do not edit the XML with a tool that changes its encoding.
A collection is marked incompatible
An existing field has the same slug but a different type. Change the post-type mapping or reconcile the field under Content Types before retrying. The prepare step adds missing fields but does not change existing field types.
Media URLs fail to download
Check that the source attachment is reachable without a WordPress session. Media behind authentication, removed files, and redirects to private network addresses fail individually and remain in the media error list.
Adapt the theme
Use getEmDashCollection() for archive routes and getEmDashEntry() for a slug route. entry.id is the route identifier and is normally the slug; entry.data.id is the stored content ID used by relationship helpers.
The following route renders an imported post:
---
import { decodeSlug, getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");
const { entry: post, error } = await getEmDashEntry("posts", slug);
if (error) return new Response("Could not load the post", { status: 500 });
if (!post) return Astro.redirect("/404");
---
<article>
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article>
Read Porting WordPress themes for route and component mapping. Read Content import for the endpoint contracts and exact retry boundaries.