---
title: Migrate from WordPress
description: Import your WordPress content into EmDash with a step-by-step guide.
---

import { Aside, Card, CardGrid, Steps, Tabs, TabItem } from "@astrojs/starlight/components";

EmDash provides a complete migration path from WordPress. Import your posts, pages, media, and taxonomies through the admin dashboard—no CLI required.

## Before You Begin

<CardGrid>
	<Card title="Export your content" icon="document">
		In WordPress, go to **Tools → Export** and download a complete export file (.xml).
	</Card>
	<Card title="Back up your site" icon="warning">
		Keep your WordPress site running until you verify the migration succeeded.
	</Card>
</CardGrid>

## Import Methods

EmDash supports three methods for importing WordPress content:

| Method           | Best for                       | Includes drafts | Requires auth |
| ---------------- | ------------------------------ | --------------- | ------------- |
| WXR file upload  | Complete migrations            | Yes             | No            |
| WordPress.com    | WordPress.com hosted sites     | Yes             | OAuth         |
| REST API (probe) | Checking content before export | No              | Optional      |

The WXR file upload is recommended for most migrations. It captures all content, including drafts, custom fields, and private posts.

## WXR File Import

<Steps>

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. The file is parsed in your browser.

4. **Review detected content**

   The wizard shows what was found:

   ```
   Found in export:
   ├── Posts: 127 → posts [New collection]
   ├── Pages: 12  → pages [Add fields]
   └── Media: 89 attachments
   ```

5. **Configure mappings**

   Toggle which post types to import. EmDash automatically:
   - Creates new collections for unmapped post types
   - Adds missing fields to existing collections
   - Warns about field type conflicts

6. **Execute the import**

   Click **Import Content**. Progress displays as each item is processed.

7. **Import media (optional)**

   After content imports, choose whether to download media files. EmDash:
   - Downloads from your WordPress URLs
   - Deduplicates by content hash
   - Rewrites URLs in your content automatically

</Steps>

<Aside type="tip">
	Re-running the import is safe. Items are matched by WordPress ID, so you won't create duplicates.
</Aside>

## Content Conversion

### Gutenberg to Portable Text

EmDash converts Gutenberg blocks to [Portable Text](https://github.com/portabletext/portabletext), a structured content format.

| Gutenberg Block  | Portable Text                | Notes                         |
| ---------------- | ---------------------------- | ----------------------------- |
| `core/paragraph` | `block` style="normal"       | Inline marks preserved        |
| `core/heading`   | `block` style="h1-h6"        | Level from block attributes   |
| `core/image`     | `image` block                | Media reference updated       |
| `core/list`      | `block` with `listItem` type | Ordered and unordered         |
| `core/quote`     | `block` style="blockquote"   | Citation included             |
| `core/code`      | `code` block                 | Language attribute preserved  |
| `core/embed`     | `embed` block                | URL and provider stored       |
| `core/gallery`   | `gallery` block              | Array of image references     |
| `core/columns`   | `columns` block              | Nested content preserved      |
| Unknown blocks   | `htmlBlock`                  | Raw HTML preserved for review |

Unknown blocks are stored as `htmlBlock` with the original HTML and block metadata. You can review and convert these manually or create custom Portable Text components to render them.

### Classic Editor Content

HTML from the Classic Editor is converted to Portable Text blocks. Inline styles (`<strong>`, `<em>`, `<a>`) become marks on spans.

### Status Mapping

| WordPress Status | EmDash Status |
| ---------------- | --------------- |
| `publish`        | `published`     |
| `draft`          | `draft`         |
| `pending`        | `pending`       |
| `private`        | `private`       |
| `future`         | `scheduled`     |
| `trash`          | `archived`      |

## Taxonomy Import

Categories and tags import as taxonomies with hierarchy preserved:

```
WordPress:                    EmDash:
├── Categories (hierarchical) ├── taxonomies table
│   ├── News                  │   ├── category/news
│   │   ├── Local             │   ├── category/local (parent: news)
│   │   └── World             │   ├── category/world (parent: news)
│   └── Sports                │   └── category/sports
└── Tags (flat)               └── content_taxonomies junction
    ├── featured                  ├── tag/featured
    └── breaking                  └── tag/breaking
```

## Custom Fields and ACF

WordPress post meta and ACF fields are analyzed during import:

<Steps>

1. **Analysis phase**

   The wizard detects custom fields and suggests EmDash field types:

   ```
   Custom Fields:
   ├── subtitle (string, 45 posts)
   ├── _yoast_wpseo_title → seo.title (string, 127 posts)
   ├── _thumbnail_id → featuredImage (reference, 89 posts)
   └── price (number, 23 posts)
   ```

2. **Field mapping**

   Internal WordPress fields (starting with `_edit_`, `_wp_`) are hidden by default. SEO plugin fields map to an `seo` object.

3. **Type inference**

   EmDash infers field types from values:
   - Numeric strings → `number`
   - `"1"`, `"0"`, `"true"`, `"false"` → `boolean`
   - ISO dates → `date`
   - Serialized PHP/JSON → `json`
   - WordPress IDs (e.g., `_thumbnail_id`) → `reference`

</Steps>

<Aside>
	ACF repeater fields and flexible content import as JSON. Create matching Portable Text or array
	fields in EmDash to structure this data.
</Aside>

## URL Redirects

After import, EmDash generates a redirect map:

```json
{
	"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" }
	]
}
```

Apply these redirects to:

- Cloudflare redirect rules
- Your hosting platform's redirect config
- Astro's `redirects` option in `astro.config.mjs`

## Concept Mapping Reference

Use this table when adapting WordPress patterns to EmDash:

| WordPress               | EmDash                             | Notes                          |
| ----------------------- | ------------------------------------ | ------------------------------ |
| `register_post_type()`  | Collection in admin UI               | Created via dashboard or API   |
| `register_taxonomy()`   | Taxonomy or array field              | Depends on complexity          |
| `register_meta()`       | Field in collection schema           | Typed, not key-value           |
| `WP_Query`              | `getCollection(filters)`             | Runtime queries                |
| `get_post()`            | `getEntry(collection, id)`           | Returns entry or null          |
| `wp_insert_post()`      | `POST /_emdash/api/content/{type}` | REST API                       |
| `the_content`           | `<PortableText value={...} />`       | Portable Text rendering        |
| `add_shortcode()`       | Portable Text custom block           | Custom component renderer      |
| `register_block_type()` | Portable Text custom block           | Same as shortcodes             |
| `add_menu_page()`       | Plugin admin page                    | Under `/_emdash/admin/`      |
| `add_action/filter()`   | Plugin hooks                         | `hooks.content:beforeSave`     |
| `wp_options`            | `ctx.kv`                             | Key-value store                |
| `wp_postmeta`           | Collection fields                    | Structured, not key-value      |
| `$wpdb`                 | `ctx.storage`                        | Direct storage access          |
| Categories/Tags         | Taxonomies                           | Hierarchical support preserved |

## API Import (Advanced)

The WordPress import is available through the admin dashboard and the REST API. Use the admin dashboard import wizard for the best experience — it provides field mapping, conflict resolution, and progress tracking.

The import API endpoints are under `/_emdash/api/import/wordpress/` for programmatic access.

## Troubleshooting

### "XML parsing error"

The export file may be corrupted or incomplete. Re-export from WordPress.

### Media download failures

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

### Field type conflicts

If an existing collection has a field with an incompatible type, the import wizard shows the conflict. Either:

- Rename the EmDash field
- Change the WordPress field mapping
- Delete and recreate the collection

### Large exports

For exports over 100MB, consider:

1. Export post types separately in WordPress
2. Import each file sequentially
3. Use the CLI with `--resume` for reliability

## Next Steps

- **[Content Import](/migration/content-import/)** — Other import sources and methods
- **[Plugin Porting](/migration/plugin-porting/)** — Migrate WordPress plugin functionality
- **[Working with Content](/guides/working-with-content/)** — Query and render your imported content
