# Migrate from WordPress to Agility

> Source: https://agilitycms.com/docs/developers/migrate-from-wordpress

> Last verified: 2026-10-03 against the WordPress REST API Handbook and the Advanced Custom Fields (ACF) documentation. WordPress sites vary with their plugins and themes, so check your own site's endpoints before you rely on a detail here.

This guide applies the phases in [Migrating to Agility](/docs/developers/migrating-to-agility) to a WordPress site: export with the WordPress REST API, map WordPress content to Agility models, lists and pages, import with the Management SDK, and redirect old permalinks. For the same migration driven by an AI agent through the Agility MCP Server, see [Migrating Content into Agility with an AI Agent](/docs/developers/migrating-content-with-an-ai-agent), which also uses WordPress as its example.

## 1. Export with the WordPress REST API

Every WordPress site exposes its own REST API, under `/wp-json/` on the site's domain. The routes you need for a content migration:

| Content | Route | Useful fields |
| --- | --- | --- |
| Posts | `/wp-json/wp/v2/posts` | `id`, `title`, `content`, `excerpt`, `slug`, `date_gmt`, `modified_gmt`, `author`, `featured_media`, `categories`, `tags`, `link`, `meta` |
| Pages | `/wp-json/wp/v2/pages` | The post fields, plus `parent` (the parent page's ID) and `menu_order` |
| Media | `/wp-json/wp/v2/media` | `source_url`, `alt_text`, `caption`, `mime_type`, `media_details`, `post` |
| Categories | `/wp-json/wp/v2/categories` | `id`, `name`, `slug`, `parent`, `description`, `link`, `count` |
| Tags | `/wp-json/wp/v2/tags` | `id`, `name`, `slug`, `link` |
| Users (authors) | `/wp-json/wp/v2/users` | The public profile fields |
| Post types and taxonomies | `/wp-json/wp/v2/types`, `/wp-json/wp/v2/taxonomies` | Lists what else the site exposes |

**Custom post types and custom taxonomies** appear in the API only if they were registered with `show_in_rest => true`. Check `/wp-json/wp/v2/types` for the list. A type that isn't there needs that flag set in WordPress, or another export route.

**ACF fields** appear under an `acf` key on posts, pages, users and terms, but only for field groups that have **Show in REST API** turned on in their group settings. This support was added in ACF 5.11. ACF returns light formatting by default; pass `acf_format=standard` if you want fully formatted values, such as complete image data.

### Page through everything

- `per_page` defaults to 10 and is capped at 100. Loop on `page`, and use the `X-WP-TotalPages` response header to know when to stop. `X-WP-Total` gives the total count, which you will need for verification.
- Lists return published items by default (`status=publish`). To export drafts, scheduled or private items, request them with the `status` parameter as an authenticated user, for example with an Application Password (available since WordPress 5.6).
- `_fields=id,slug,title,link` returns only the fields you list, which makes inventory calls fast. `_embed` includes linked resources such as the author, terms and featured image in one response.
- `modified_after` returns only items changed since a date. You will use it for the final sync at cutover.

```ts
// Read every item from a WordPress collection, 100 at a time.
async function fetchAll(site: string, route: string, params = "") {
	const items: any[] = []
	for (let page = 1; ; page++) {
		const res = await fetch(`${site}/wp-json/wp/v2/${route}?per_page=100&page=${page}${params}`)
		if (!res.ok) break
		items.push(...(await res.json()))
		if (page >= Number(res.headers.get("X-WP-TotalPages"))) break
	}
	return items
}

const posts = await fetchAll("https://example.com", "posts")
const categories = await fetchAll("https://example.com", "categories")
```

### Body content: blocks, shortcodes and builders

`content.rendered` is the HTML WordPress produces for the post. The block editor stores each block in the post content as HTML wrapped in comment delimiters, with the block's settings as JSON inside the comment, for example `<!-- wp:image {"sizeSlug":"large"} -->`. If you only need the body as HTML, the rendered content is enough. If you want to turn particular blocks into Agility fields or components, plan how you will get at the stored block markup for your site before you design the mapping.

Also look for leftovers that don't make sense outside WordPress: unrendered shortcodes in square brackets, page-builder wrapper markup, and image URLs under `/wp-content/uploads/`.

## 2. Map WordPress to Agility

| WordPress | Agility | Notes |
| --- | --- | --- |
| Posts | A content model (for example `BlogPost`) with a list container, plus a dynamic page that gives each post its own URL | See [Generating Dynamic Pages From Content](/docs/editors/generating-dynamic-pages-from-content). Use the slug as the path. |
| Pages | Pages in the sitemap, built from a page model and components | `parent` becomes the page's place in the sitemap tree. For many similar pages, a list plus a dynamic page can be simpler. |
| Custom post types | One content model and list per type | |
| Categories | A `Category` content model and list | Posts point at categories with linked content: a dropdown for one category, a search list box for several. Keep `parent` as a linked field or flatten it. |
| Tags | A `Tag` content model and list | Linked from posts with a search list box. |
| Users | An `Author` content model and list | Import only the public fields your site shows. |
| Media library | The Agility media library | Upload files, then reference the returned `cdn.aglty.io` URL. |
| Featured image | An Image field | Look up `featured_media` in the media export. |
| `content.rendered` | An HTML field, a Markdown field, or fields and components | See the section above. |
| `excerpt.rendered` | A Multi-Line Text field | The rendered excerpt is HTML, so strip the tags. |
| `date_gmt` | A Date/Time field | The GMT value avoids time-zone ambiguity. |
| `slug` | A Text field used in the dynamic page's path | Keeping slugs keeps URLs stable. |
| `id` | A `SourceID` Text field | Makes re-runs update items instead of duplicating them. |

### ACF field types

| ACF field type | Agility field type |
| --- | --- |
| Text, Email | Text |
| Text Area | Multi-Line Text |
| WYSIWYG Editor | HTML |
| Number, Range | Number, or Decimal for fractional values |
| True / False | True/False |
| Select, Radio Button, Button Group | Drop-down List |
| Checkbox | Linked content to a small list, or a Text field holding the values |
| URL, Link | URL |
| Image | Image |
| File | File |
| Gallery | Gallery |
| Date Picker, Date Time Picker | Date/Time |
| Post Object, Relationship, Page Link | Linked Content |
| Taxonomy, User | Linked Content to your `Category`, `Tag` or `Author` list |
| Group | The group's fields directly on the model, optionally under a Tab |
| Repeater, Flexible Content | Nested linked content (a list of child items) or components on a page |

The field types are described in [Fields](/docs/developers/fields) and [Linked Content Field Types](/docs/developers/linked-content-field-types). Remember that a linked-content field stores its selection in two companion fields, `_ValueField` (the linked item's content ID) and `_TextField`. Read back an item you set by hand in the editor and copy that shape.

## 3. Import with the Management SDK

Import in this order, keeping a map from WordPress ID to Agility content ID (or asset URL) at each step:

1. Authors, categories and tags.
2. Media: download each file and upload it with `assetMethods.upload`, keyed by WordPress media ID.
3. Posts and custom post types, filling linked fields from the maps.
4. Pages.

Before saving, rewrite the body HTML: point image `src` attributes at the uploaded `cdn.aglty.io` URLs, and change internal links from old permalinks to the new paths so readers don't go through a redirect.

```ts
// Map one WordPress post to an Agility content item.
// existing: SourceID -> contentID map built by paging through the container first.
function toAgilityItem(post: any, existing: Map<string, number>, categoryIds: Map<number, number>, imageUrls: Map<number, string>) {
	const categoryId = categoryIds.get(post.categories[0])
	return {
		contentID: existing.get(String(post.id)) ?? -1, // update if it exists
		properties: {definitionName: "BlogPost", referenceName: "BlogPosts"},
		fields: {
			Title: post.title.rendered,
			Slug: post.slug,
			Body: rewriteLinks(post.content.rendered), // your link and image rewriting
			PublishDate: post.date_gmt,
			SourceID: String(post.id),
			Category_ValueField: categoryId ? String(categoryId) : "",
			// Set Category and Category_TextField, and the image field, using the
			// exact shapes you read back from a sample item.
		},
	}
}
```

Save with `contentMethods.saveContentItems` in sequential batches, never in parallel, and check the result for `-1` entries (failed items). The full script, including the lookup of existing items, asset upload and batch publishing, is in [Migrating Content into Agility with an AI Agent](/docs/developers/migrating-content-with-an-ai-agent#step-5-scale-up-with-a-management-sdk-script). Throughput guidance is in [Migrating to Agility](/docs/developers/migrating-to-agility#throughput-batches-limits-and-tokens).

For pages: if there are only a handful, build them by hand in the sitemap. For many, use the SDK's page methods (see [Pages](/docs/javascript/management-sdk/pages)).

The [Agility CLI](/docs/developers/cli) doesn't read WordPress. It is useful after the import, to copy the finished models and content from a migration sandbox instance into a blank production instance.

## 4. Redirect old permalinks

Every WordPress post, page and term has a `link` field with its public URL. Use it to build the URL map:

1. Export `link` for posts, pages, categories and tags (and any custom post types).
2. Work out each item's new Agility URL. If the path is unchanged, no redirect is needed.
3. For every path that changes, add a **301** redirect from the old path to the new one.
4. Add redirects for other URLs your site exposed that won't exist on the new site, such as archive pages or uploaded files that other sites link to.

Load the redirects into Agility's URL Redirections list. The import file has three columns, `OriginURL`, `DestinationURL` and `HTTPCode` (301 or 302); see [URL Redirections](/docs/editors/url-redirections). From code, the Management API's `POST /api/v1/instance/{guid}/url-redirections` creates or updates up to 250 redirects per call and reports any it skipped, for example an origin URL that already exists. Your front end must apply the redirects; see [Implementing URL Redirects with Next.js](/docs/nextjs/implementing-url-redirects-with-next-js).

The SEO side of this, including metadata and sitemaps, is in [Keep Your Search Rankings Through a Migration](/docs/developers/keep-search-rankings-through-a-migration).

## Checklist

- [ ] Every post type and taxonomy you need is listed in `/wp-json/wp/v2/types` and `/wp-json/wp/v2/taxonomies`.
- [ ] ACF field groups have **Show in REST API** turned on, and `acf` appears in the responses.
- [ ] Counts in Agility match `X-WP-Total` for each type (minus anything skipped on purpose).
- [ ] No body still contains shortcodes, builder markup or `/wp-content/uploads/` URLs.
- [ ] Every changed URL has a 301 redirect, and the redirects work on the new site.
- [ ] A sample of posts and pages looks right in preview, then the content is published.

## Related

- [Migrating to Agility](/docs/developers/migrating-to-agility)
- [Migrating Content into Agility with an AI Agent](/docs/developers/migrating-content-with-an-ai-agent)
- [Migration Cutover Runbook](/docs/developers/migration-cutover-runbook)
- [Keep Your Search Rankings Through a Migration](/docs/developers/keep-search-rankings-through-a-migration)
- [Content Items (Management SDK)](/docs/javascript/management-sdk/content-items)

---

## Related pages

- [Migrating to Agility](https://agilitycms.com/docs/developers/migrating-to-agility.md)
- [Migrate from Contentful to Agility](https://agilitycms.com/docs/developers/migrate-from-contentful.md)
- [Migrate from Sitecore to Agility](https://agilitycms.com/docs/developers/migrate-from-sitecore.md)
- [Migration Cutover Runbook](https://agilitycms.com/docs/developers/migration-cutover-runbook.md)
- [Keep Your Search Rankings Through a Migration](https://agilitycms.com/docs/developers/keep-search-rankings-through-a-migration.md)

Index of every docs page: https://agilitycms.com/docs/llms.txt
