Moving WordPress custom fields to a headless CMS means designing the destination content model first, mapping each field group into it, and exporting only after that mapping exists. WPBuildAI does this against the rendered pages rather than the database, for one reason that saves the most time: the rendered page shows what each field actually displays, while the database shows a key and a value with no clue whether either is still in use. Custom fields hold the structured half of a WordPress site, the specifications, prices, opening hours, locations, related items and attachments that the post body never mentions, and they are the second most common reason a migrated page arrives incomplete. Here is how to move them properly.
Why is exporting the values not enough?
Because a value without its model is a key nobody can interpret.
Export the postmeta from a mature WordPress site and you get rows: a post ID, a key, a value. What you do not get is which field group the key belonged to, what type it was meant to be, whether it was required, what it was labelled in the editor, how it was formatted for display, or whether anything still renders it.
So a key called price_2 arrives with a number in it and no way to tell whether it was a sale price, a per unit price, a leftover from a template three redesigns ago, or a value one editor typed into the wrong box in 2022. Multiply that by forty keys and the export becomes an archaeology exercise rather than an import.
The model is the missing half. Designing it first turns the export from a puzzle into a straightforward transformation, which is why the order of operations matters more here than the tooling does.
Where do you find out what each field really does?
Three sources, and you need all three because each answers a different question.
| Source | What it tells you | What it cannot tell you | How to read it |
|---|---|---|---|
| Field group definitions | What exists, and its intended type | Whether anything uses it | The fields plugin’s own settings screens |
| The database or REST API | Which posts carry values | How the value is displayed | The REST API or WP-CLI |
| The rendered pages | What visitors actually see | Fields that are stored but hidden | A crawl of the live site |
The third row is the one that decides the scope. Fields that exist, hold values, and appear nowhere on any public page are usually the largest group on a site with history, and migrating them means carrying dead weight into a system you are about to design carefully.
The comparison worth running: list the fields that render, list the fields that exist, and treat the difference as a deletion candidate rather than a migration item.
How do you design the destination model?
From the rendered pages backwards, grouping fields by what they describe rather than by how WordPress happened to store them.
Open a page type and look at what it displays. A product page shows a name, a price, a set of specifications, some images, and perhaps a list of related products. Those are the entities. The forty postmeta keys behind them are an implementation detail of the old system, and reproducing them one for one in a new one imports the mess along with the content.
So the model is a small number of content types with clearly typed fields: text, number, date, reference, image, list. Platforms like Sanity let you define that explicitly, and Astro documents connecting several of them as ordinary configuration.
Then write the mapping as a document before touching data: this WordPress key becomes that field in that type, with this transformation. That mapping file is the artifact the whole exercise depends on, and it is the thing that makes the import re-runnable when the first attempt is wrong.
What order should the migration run in?
Five steps, and the order is what prevents rework rather than being a matter of taste.
| Step | What it produces | Why it comes here | Verdict |
|---|---|---|---|
| 1. Inventory rendered fields | What is actually in use | Scope, before any modelling | Do this before quoting, not after |
| 2. Design the content model | Types and fields in the destination | The export needs a shape to land in | Design work, not data work |
| 3. Write the mapping document | Key to field, with transformations | Makes the import re-runnable | The artifact the whole job depends on |
| 4. Import primary content | Entries with their simple fields | References need IDs to exist first | Expect to run it more than once |
| 5. Resolve relationships | Cross references between entries | IDs are only known after step 4 | The step people underestimate |
Step five is the one people underestimate. Relationship fields store WordPress post IDs, and those IDs do not exist in the destination, so every reference has to be translated from an old ID to a new one. That translation only becomes possible once the primary content is in, which is why it is a separate pass rather than part of the first import.
What breaks that nobody expects?
Four things, all of them silent, which is why they surface weeks later.
Serialized values. WordPress stores repeaters and grouped fields as serialized strings, so a field that looks like a list in the editor arrives as an encoded blob. It needs decoding before it means anything, and a naive import stores the blob as text.
Fields on taxonomies and users. Field groups can attach to categories, tags and author profiles rather than to posts, and an export that walks only posts misses them entirely. The symptom is a category page that lost its description block.
Media references. Image fields store attachment IDs, not URLs, so the export contains numbers that resolve to nothing once WordPress is gone. Resolve them to file paths during the export, while the installation is still running.
Conditional fields. A field shown only when another field has a certain value produces sparse data, and a destination model that requires it will reject half your entries. Model those as optional, and record why.
The XML export carries some of this and not reliably, which is why the REST API or WP-CLI is the better route for structured content.
How do you verify it worked?
By comparing rendered output rather than by counting records, because a record count matching proves only that rows moved.
Take twenty pages across your page types, including the most complex one and the oldest one. For each, put the old rendered page and the new one side by side and check that every visible value appears: each specification, each price, each date, each related item, each downloadable file.
Then check the empty cases deliberately. Pages where a field was not filled should render cleanly rather than showing a label with nothing after it, and pages where a conditional field did not apply should not show its container at all.
Then spot check relationships in both directions. If product A lists product B as related, confirm B exists and is the right B, since an off by one in ID translation produces plausible looking wrong answers rather than errors.
The broader parity discipline this belongs to is in how to migrate a website without losing SEO, and the other WordPress specific parts of a move are in migrating WordPress to an AI build, part by part.
How long does this part take?
Two to five days on a typical business site, and it is the least predictable line in a migration quote because it scales with how much accumulated mess is in the field definitions rather than with page count.
The inventory is half a day: run the crawl, list what renders, list what exists, and produce the difference. That number is the first honest estimate anybody can give you, and it is why a supplier who has not done it is quoting blind.
Modelling is a day for a site with three or four page types, longer if the structures genuinely differ per type. This is design work rather than data work, and it is where an experienced pair of eyes saves the most, because a model that mirrors the old mess costs you again every time somebody edits it afterwards.
The mapping document and first import are a day. Relationship resolution and verification are another.
What makes it longer: repeaters nested inside repeaters, fields attached to taxonomies as well as posts, and any site where two different fields plugins were used across different eras. Each of those is discovered during the inventory rather than during the import, which is the argument for doing the inventory before quoting rather than after.
When is a headless CMS the wrong destination for these?
When there are only a few fields on one page type. A site with a subtitle and a hero image per service page does not need a content platform, and putting those values in the page itself, or in a small data file in the repository, is simpler, cheaper and has nothing to maintain.
It is also wrong when nobody edits the structured data. Fields that were set once at build time and never touched are configuration rather than content, and configuration belongs in the codebase where it is versioned with everything else.
And it is wrong when the fields are really an application. Inventory levels, booking availability, member entitlements and order state are operational data, and a content platform is a poor database. Those belong in whatever system actually owns them, with the site reading from it.
Headless earns its cost when several people regularly edit structured records that many pages depend on: a product catalogue, a location directory, a staff list that appears in four places. That is a content operation, and the decision framework for it is in do you still need a CMS after leaving WordPress.
Key takeaways: moving structured content out
Design the destination model before exporting anything. Values without a model are keys nobody can interpret, and the model has to be designed rather than inferred from postmeta.
Scope from the rendered pages. Fields that exist, hold values, and display nowhere are usually the largest group on a mature site, and they are deletion candidates rather than migration items.
Run it in order: inventory, model, mapping document, primary import, then relationships. References can only be translated once the destination IDs exist.
Verify by comparing rendered pages rather than record counts. Twenty pages across your page types, checking every visible value, plus the empty cases and both directions of each relationship.
Before you decide: common questions
How do you migrate WordPress custom fields to a headless CMS? Design the destination content model first, map each field group to it field by field, then export values into that shape. WPBuildAI maps against the rendered page rather than the database, because that is where you can see what each field actually displays. Exporting values with no model waiting for them produces a list of keys nobody can use.
Why can I not just export the fields and import them? Because an export gives you keys and values with no indication of how they were displayed, grouped, or validated. A field called price_2 tells you nothing about whether it was a sale price, a per unit price, or a leftover from a template three redesigns ago. The model is the missing half, and it has to be designed rather than inferred.
How do you find every field that is actually in use? Compare three sources. The field group definitions show what exists. The database shows which posts carry values. The rendered pages show which of those values are visible to anyone. Fields that exist and are populated and appear nowhere on the public site are the largest group on most mature sites, and they should not be migrated.
What about relationships between posts? They are the hardest part. Relationship fields store post IDs, and those IDs will not exist in the destination, so the mapping has to translate them into whatever reference the new system uses. Do this after the primary content is imported and IDs are known, and verify by spot checking that related items still resolve to the right entries.
Is a headless CMS not overkill for a few fields? Frequently yes. A site with three custom fields on one page type does not need a content platform, and putting that content in the page itself or in a small data file is simpler and cheaper. Headless earns its cost when several people edit structured records regularly, not when a template needs a subtitle.