The WordPress Migration I Thought Was About Rows—Until Attribution Broke

I recently finished a WordPress migration workflow that looked ordinary on the surface: export content from one installation and import it into another. The rows moved quickly. The hard part was everything a row only points at—its author, publication history, taxonomies, custom fields, images, and the difference between a draft date and a public date. That work changed how I define a successful migration. If a post arrives with the right body but the wrong attribution or timeline, the transfer is technically complete and editorially false. The real deliverable is a portable content contract that preserves meaning across two different sites.
The first export was valid but not portable
The source system could safely export internal IDs, but those IDs meant nothing on the destination. Staff member 418 on one site might be a page, an attachment, or no record at all on another. I had to export stable human-facing identity alongside internal references, then resolve it under strict rules during import.
I used exact, published staff identity as the safe path and treated ambiguity as a warning instead of guessing. That feels conservative until you imagine attaching a clinical article to the wrong reviewer. A migration that quietly invents certainty is more dangerous than one that asks for review.
Dates carry editorial meaning
Dates were another trap. Published content needs its canonical publication and modified timestamps, while drafts must retain the date editors expect to see when they resume work. Normalizing every row to the import time would have made the destination look fresh while erasing history.
The importer now validates each incoming date, preserves draft dates deliberately, and rejects impossible values without discarding the rest of the report. This is less glamorous than moving blocks or images, but it is the difference between a migration and a rewrite of the editorial record.
Partial failure needs its own product design
A browser request can time out after the server has already created a post. Retrying a blank-ID row can then create a duplicate. I added a duplicate-protection boundary based on the portable fields available to the importer and made failures visible per row. The operator can see what was created, updated, skipped, warned, or failed.
The useful rule is simple: never let an HTTP response be the only proof that a write happened. Imports need durable identifiers, idempotent behavior, and a report that survives the request that started them.
Compatibility belongs in the profile. I did not change the default export into a special-purpose format. I added an explicit migration profile. The normal same-site workflow kept its existing behavior, while the portable profile included attribution, canonical dates, taxonomies, and the custom fields required by the destination theme.
That boundary matters for maintenance. A specialized workflow should announce its assumptions instead of silently changing the format used by existing operators and older releases.
My migration checklist now starts before the file
- List every relationship whose numeric ID changes between sites.
- Choose a stable identity and define what counts as ambiguous.
- Write date rules separately for drafts and published content.
- Make every write safe to retry after a timeout.
- Report warnings separately from hard failures.
- Test the untouched client file, not only a clean fixture.
I also keep the source values during the observation window. Destructive cleanup can wait until the destination has been reviewed and the reconciliation report is quiet.
The business risk is historical accuracy
Migration budgets often focus on volume: how many posts, how many images, how long the transfer takes. The risk I now surface first is meaning. Incorrect authorship can undermine trust. Changed dates can affect compliance and search behavior. Missing relationships can make a page factually incomplete even when it renders without errors.
That is why I test representative content types, edge cases, and reruns before I call a migration ready. The best result is boring: the destination tells the same story as the source, and the operator can explain every exception.
How I qualified the real file. Clean fixtures proved the parser, but the untouched export proved the workflow. I previewed the complete file without writes, checked its row and column counts, and inspected values known to be awkward: blank destination IDs, repeated names, remote images, escaped schema fragments, and relationships that could not resolve on the target. I then used disposable local records for a bounded real import and cleaned them up after comparing the report with WordPress state.
This distinction is important. A fixture tells me whether the behavior I imagined works. A customer-shaped file tells me which behaviors I failed to imagine. I keep both in the test strategy and never “repair” the source file privately just to help the importer pass.
The rule I carried into the next migration
Before the next migration, I would write a one-page portability contract and have both an editor and a developer approve it. The developer sees IDs, retries, and validation; the editor sees attribution, chronology, and relationships. Putting those views together early is far cheaper than discovering after launch that all the rows moved but the record did not.
Photo by RDNE Stock project on Pexels.
Written by
Adrian Saycon
A developer with a passion for emerging technologies, Adrian Saycon focuses on transforming the latest tech trends into great, functional products.


