Skip to content
Get a free audit

Currency

Project Spark 8 min read

Migrating From Yoast Without Corrupting a Single Byte

Lessons from a lossless Yoast SEO migration, data faithfulness, template variables, locale traps, and why importers must never "improve" your data.

Bernard

The most dangerous feature in any SEO plugin is the importer, because it touches every piece of SEO data a business has accumulated for years, and every “improvement” it makes is silent data corruption. Project Spark’s Yoast migration went through nine fix iterations before we could certify it byte-for-byte lossless, and each iteration is a lesson for anyone building or running a migration.

This post tells the story in the order it happened: the contract we agreed with the client, the five ways our own code violated it despite good intentions, the certification suite that finally closed the question, and the checklist that transfers to any data migration, SEO or otherwise.

Key takeaway: A data importer has exactly one job, move data unchanged. Every sanitizer, normalizer, locale switch, and “smart cleanup” you add to an import path is a bug you haven’t been reported yet.

Why import bugs are the worst bugs

Before the specifics, it is worth stating why this feature deserved nine iterations of attention. Import corruption has three properties that make it uniquely damaging. It is silent: nothing errors, nothing warns; the data is simply different afterwards. It is complete: an importer touches every record, so a subtle defect applies site-wide in one run. And it is delayed: the business discovers the problem weeks later, in search results, when a title renders wrongly, long after anyone associates the symptom with the migration.

Years of accumulated editorial judgement live in SEO metadata: hand-tuned titles, carefully phrased descriptions, deliberate separator choices. The client’s directive was blunt and correct: imported data must arrive exactly as the previous plugin had it. No change, no translation, ever.

The contract: no change, no translation, ever

That directive became a written engineering contract, enforced by tests. What violated it over time reads like a museum of well-intentioned mistakes:

  1. A brand-stripping heuristic. Early code detected “mismatched” brand suffixes in titles (Hebrew title, Latin brand) and stripped them, deleting user content and changing separators. The intention was tidiness: remove boilerplate the render layer would re-add anyway. The effect was destruction of editorial choices, discovered by the client as separator changes in live search results. Removed entirely, and the render layer took over all suffix handling (Part 7).
  2. A locale switch that translated content. Rendering Yoast values during an admin request used the admin’s profile language, so Hebrew pages imported English brand fragments. We had to pin the content’s locale around resolution, then remove an over-broad version of that fix, then reinstate a narrow version when the bug class returned. Locale handling took three passes to get right, and the third pass survived because the second failure taught us precisely which operation needed pinning: only the calls that ask the source plugin to render values, nothing wider.
  3. A standard sanitizer that mutated text. WordPress’s common text sanitizer strips tags, collapses whitespace, and removes line breaks. Reasonable for form input; catastrophic for an importer. The user-visible report that exposed it said, in effect, “the importer is still changing our data”, and the diff showed missing emphasis tags and collapsed whitespace runs. Byte-faithful values now pass through untouched, with security handled at render time by output escaping. The lesson generalizes: sanitizers encode assumptions about untrusted form input, and applying them to trusted stored data silently enforces those assumptions on content that never agreed to them.
  4. WordPress core’s own slash-stripping. The metadata API silently strips backslashes on write, a platform behavior, documented but easy to forget. Any value with a \ was corrupted until we added the canonical slash-compensation on every write path. Then the sister-hunt began: the same platform behavior affected four other write surfaces (metabox saves, quick edit, wizard forms, AI apply), each fixed and each covered by its own round-trip tests. One bug is never one bug; it is one instance of a class.
  5. Template variables leaking through. Yoast titles can be templates (%%title%% %%sep%% %%sitename%%). Our first instinct, resolve the variables ourselves, was wrong twice over: our resolver ran in the wrong locale context (see mistake 2) and could never replicate every variable, separator option, and premium customization. The final design inverts the responsibility: when the source plugin is active, the importer calls Yoast’s own rendering API, so Yoast resolves its own variables and separators, including premium custom separators we could never guess. Values that arrive with variables still unresolved are rejected and surfaced to the operator rather than stored as broken text.

Certification, not assumptions

After the ninth fix, we didn’t declare victory, we certified it: automated tests that push Hebrew, Arabic, CJK, emoji, HTML entities, line breaks, tabs, backslashes, and quotes through the complete import pipeline and assert byte-identical recovery. Thirteen categories of hostile characters, through every write surface, on every release.

Certification changed the nature of support conversations permanently. When the client later reported a mangled description (a 💰 emoji turned to ?), those tests let us prove within minutes that the corruption happened upstream of our plugin, turning a blame dispute into a database-charset diagnosis. The full story of that investigation is Part 8; the point here is that the certification suite is what made a five-minute answer possible. Without tests, “our importer is faithful” is an opinion. With them, it is a reproducible fact.

The pattern across all nine iterations

Reading the five mistakes as one sequence reveals the deeper pattern: each fix removed a transformation, and no fix ever added one. The heuristic was removed. The locale switch was removed, then narrowed to the single call that required it. The sanitizer was removed. The platform’s slash-stripping was neutralized. The variable resolution was delegated back to its source. Nine iterations, and the importer ended up doing strictly less than when it started, because every piece of cleverness between source and destination had proven to be a corruption vector wearing a helpful expression.

That asymmetry is the post’s real lesson. Migration code improves by subtraction. If your importer’s changelog shows features accumulating, more normalization options, more cleanup passes, more smart defaults, it is moving in the wrong direction, and its bug reports simply have not arrived yet.

Running the migration: coexistence and cutover

A migration is not only an importer; it is a period of time. During verification, the previous SEO plugin remained active and authoritative while Project Spark held the imported copy, which meant two SEO plugins installed simultaneously, a situation that normally produces duplicate meta tags and conflicting signals. The plugin’s coexistence layer (Part 19) detects the competitor and defers per-feature, so the site never emitted doubled markup during the window.

That defer-while-verifying design converted the cutover from an event into a decision: compare imported values against the source at leisure, spot-check rendered output, and only then deactivate the old plugin, watching each deferred feature come alive with data already proven identical. Re-running the import was safe throughout, because the importer fills empty fields without overwriting existing values by default; idempotence is what makes verification a process instead of a leap.

How to audit a migration you ran years ago

Most readers of this post have already migrated, possibly years ago, possibly with one of the defects described above. The good news: import corruption is auditable after the fact, because the symptoms have signatures. Titles whose separators differ from what editors remember choosing indicate suffix-stripping or template-default substitution. English fragments inside non-English metadata indicate locale leakage at import time. Missing emphasis tags, collapsed whitespace, or vanished line breaks in descriptions indicate a sanitizer in the import path. Question marks where emoji should be indicate character-set narrowing, though that one may predate the migration entirely.

The audit procedure is mundane and effective: sample twenty pages across content types, compare the live metadata against any surviving source (the old plugin’s database fields often remain in place after deactivation, which makes comparison possible long after the migration), and classify every difference. Differences you cannot attribute to a deliberate editorial change are importer artifacts. Whether to fix them depends on scale, but knowing they exist changes how much you trust the next migration, and which questions you ask the next vendor. The single most revealing question: “Does your importer modify data in any way, and can you show me the tests that prove it does not?” A vendor with certification answers in one sentence. A vendor without it answers in paragraphs.

The transferable checklist

  • Prefer the source system’s own rendering API over reimplementing its logic.
  • Treat empty values as user intent, not gaps to fill with rendered defaults.
  • Never run form-input sanitizers on trusted imported data.
  • Test the full write-read round trip with hostile characters, not just happy-path Latin text.
  • Compensate for platform-level mutations (slash-stripping, encoding coercion) on every write path, then audit for sister surfaces with the same exposure.
  • Make imports idempotent and non-destructive by default, so re-running is verification rather than risk.
  • Keep the old system active but deferred until the new data is proven; cut over by decision, not by hope.
  • Write the fidelity requirement into the project agreement itself. “Imported data must be byte-identical to source, verified by automated tests” is one sentence in a statement of work, and it converts every future dispute about changed data from a negotiation into a measurement. Contracts shape engineering; this clause shaped ours more than any other.

Next: Part 7, The Invisible-Character Bug

FAQ

How do I migrate from Yoast without losing SEO data?

Use an importer that copies values verbatim, resolves Yoast template variables through Yoast’s own API, and verifies results with round-trip tests including non-Latin characters and emoji.

Why did my SEO titles change after a plugin migration?

Common causes: the importer resolved template variables in the wrong language context, applied a text sanitizer, or stripped brand suffixes with a cleanup heuristic. All are importer bugs, the data should never change.

Should an importer sanitize imported data?

No. Sanitize at output time with escaping. Input sanitizers mutate legitimate content (HTML, line breaks, backslashes) and break byte-fidelity.

What is byte-for-byte data fidelity?

The guarantee that a value read back from storage is the identical byte sequence that was submitted, verified by automated tests across scripts, emoji, and special characters.

Can I run two SEO plugins during a migration?

Yes, if one defers its output while the other remains authoritative. Per-feature deference prevents duplicate meta tags during the verification window and lets you cut over only after the imported data is proven identical.

How do I verify a migration didn’t change my data?

Diff a sample of records byte-by-byte between source and destination, including titles with non-Latin text, emoji, and special characters. Any changed byte, whitespace included, is a defect worth investigating.

Keep reading in this series

Keep reading

Migrating From Yoast Without Corrupting a Single Byte

Lessons from a lossless Yoast SEO migration — data faithfulness, template variables, locale traps, and why importers must never "improve" your data.

Bernard

Building a Hebrew-First SEO Plugin (“Project Spark”) — Part 6 of 20

The most dangerous feature in any SEO plugin is the importer, because it touches every piece of SEO data a business has accumulated for years — and every “improvement” it makes is silent data corruption. Project Spark’s Yoast migration went through nine fix iterations before we could certify it byte-for-byte lossless, and each iteration is a lesson for anyone building or running a migration.

This post tells the story in the order it happened: the contract we agreed with the client, the five ways our own code violated it despite good intentions, the certification suite that finally closed the question, and the checklist that transfers to any data migration — SEO or otherwise.

Key takeaway: A data importer has exactly one job — move data unchanged. Every sanitizer, normalizer, locale switch, and “smart cleanup” you add to an import path is a bug you haven’t been reported yet.

Why import bugs are the worst bugs

Before the specifics, it is worth stating why this feature deserved nine iterations of attention. Import corruption has three properties that make it uniquely damaging. It is silent: nothing errors, nothing warns; the data is simply different afterwards. It is complete: an importer touches every record, so a subtle defect applies site-wide in one run. And it is delayed: the business discovers the problem weeks later, in search results, when a title renders wrongly — long after anyone associates the symptom with the migration.

Years of accumulated editorial judgement live in SEO metadata: hand-tuned titles, carefully phrased descriptions, deliberate separator choices. The client’s directive was blunt and correct: imported data must arrive exactly as the previous plugin had it. No change, no translation, ever.

The contract: no change, no translation, ever

That directive became a written engineering contract, enforced by tests. What violated it over time reads like a museum of well-intentioned mistakes:

  1. A brand-stripping heuristic. Early code detected “mismatched” brand suffixes in titles (Hebrew title, Latin brand) and stripped them — deleting user content and changing separators. The intention was tidiness: remove boilerplate the render layer would re-add anyway. The effect was destruction of editorial choices, discovered by the client as separator changes in live search results. Removed entirely, and the render layer took over all suffix handling (Part 7).
  2. A locale switch that translated content. Rendering Yoast values during an admin request used the admin’s profile language, so Hebrew pages imported English brand fragments. We had to pin the content’s locale around resolution — then remove an over-broad version of that fix — then reinstate a narrow version when the bug class returned. Locale handling took three passes to get right, and the third pass survived because the second failure taught us precisely which operation needed pinning: only the calls that ask the source plugin to render values, nothing wider.
  3. A standard sanitizer that mutated text. WordPress’s common text sanitizer strips tags, collapses whitespace, and removes line breaks. Reasonable for form input; catastrophic for an importer. The user-visible report that exposed it said, in effect, “the importer is still changing our data” — and the diff showed missing emphasis tags and collapsed whitespace runs. Byte-faithful values now pass through untouched, with security handled at render time by output escaping. The lesson generalizes: sanitizers encode assumptions about untrusted form input, and applying them to trusted stored data silently enforces those assumptions on content that never agreed to them.
  4. WordPress core’s own slash-stripping. The metadata API silently strips backslashes on write — a platform behavior, documented but easy to forget. Any value with a \ was corrupted until we added the canonical slash-compensation on every write path. Then the sister-hunt began: the same platform behavior affected four other write surfaces (metabox saves, quick edit, wizard forms, AI apply), each fixed and each covered by its own round-trip tests. One bug is never one bug; it is one instance of a class.
  5. Template variables leaking through. Yoast titles can be templates (%%title%% %%sep%% %%sitename%%). Our first instinct — resolve the variables ourselves — was wrong twice over: our resolver ran in the wrong locale context (see mistake 2) and could never replicate every variable, separator option, and premium customization. The final design inverts the responsibility: when the source plugin is active, the importer calls Yoast’s own rendering API, so Yoast resolves its own variables and separators — including premium custom separators we could never guess. Values that arrive with variables still unresolved are rejected and surfaced to the operator rather than stored as broken text.

Certification, not assumptions

After the ninth fix, we didn’t declare victory — we certified it: automated tests that push Hebrew, Arabic, CJK, emoji, HTML entities, line breaks, tabs, backslashes, and quotes through the complete import pipeline and assert byte-identical recovery. Thirteen categories of hostile characters, through every write surface, on every release.

Certification changed the nature of support conversations permanently. When the client later reported a mangled description (a 💰 emoji turned to ?), those tests let us prove within minutes that the corruption happened upstream of our plugin — turning a blame dispute into a database-charset diagnosis. The full story of that investigation is Part 8; the point here is that the certification suite is what made a five-minute answer possible. Without tests, “our importer is faithful” is an opinion. With them, it is a reproducible fact.

The pattern across all nine iterations

Reading the five mistakes as one sequence reveals the deeper pattern: each fix removed a transformation, and no fix ever added one. The heuristic was removed. The locale switch was removed, then narrowed to the single call that required it. The sanitizer was removed. The platform’s slash-stripping was neutralized. The variable resolution was delegated back to its source. Nine iterations, and the importer ended up doing strictly less than when it started — because every piece of cleverness between source and destination had proven to be a corruption vector wearing a helpful expression.

That asymmetry is the post’s real lesson. Migration code improves by subtraction. If your importer’s changelog shows features accumulating — more normalization options, more cleanup passes, more smart defaults — it is moving in the wrong direction, and its bug reports simply have not arrived yet.

Running the migration: coexistence and cutover

A migration is not only an importer; it is a period of time. During verification, the previous SEO plugin remained active and authoritative while Project Spark held the imported copy — which meant two SEO plugins installed simultaneously, a situation that normally produces duplicate meta tags and conflicting signals. The plugin’s coexistence layer (Part 19) detects the competitor and defers per-feature, so the site never emitted doubled markup during the window.

That defer-while-verifying design converted the cutover from an event into a decision: compare imported values against the source at leisure, spot-check rendered output, and only then deactivate the old plugin — watching each deferred feature come alive with data already proven identical. Re-running the import was safe throughout, because the importer fills empty fields without overwriting existing values by default; idempotence is what makes verification a process instead of a leap.

How to audit a migration you ran years ago

Most readers of this post have already migrated — possibly years ago, possibly with one of the defects described above. The good news: import corruption is auditable after the fact, because the symptoms have signatures. Titles whose separators differ from what editors remember choosing indicate suffix-stripping or template-default substitution. English fragments inside non-English metadata indicate locale leakage at import time. Missing emphasis tags, collapsed whitespace, or vanished line breaks in descriptions indicate a sanitizer in the import path. Question marks where emoji should be indicate character-set narrowing — though that one may predate the migration entirely.

The audit procedure is mundane and effective: sample twenty pages across content types, compare the live metadata against any surviving source (the old plugin’s database fields often remain in place after deactivation, which makes comparison possible long after the migration), and classify every difference. Differences you cannot attribute to a deliberate editorial change are importer artifacts. Whether to fix them depends on scale — but knowing they exist changes how much you trust the next migration, and which questions you ask the next vendor. The single most revealing question: “Does your importer modify data in any way, and can you show me the tests that prove it does not?” A vendor with certification answers in one sentence. A vendor without it answers in paragraphs.

The transferable checklist

  • Prefer the source system’s own rendering API over reimplementing its logic.
  • Treat empty values as user intent, not gaps to fill with rendered defaults.
  • Never run form-input sanitizers on trusted imported data.
  • Test the full write-read round trip with hostile characters, not just happy-path Latin text.
  • Compensate for platform-level mutations (slash-stripping, encoding coercion) on every write path — then audit for sister surfaces with the same exposure.
  • Make imports idempotent and non-destructive by default, so re-running is verification rather than risk.
  • Keep the old system active but deferred until the new data is proven; cut over by decision, not by hope.
  • Write the fidelity requirement into the project agreement itself. “Imported data must be byte-identical to source, verified by automated tests” is one sentence in a statement of work — and it converts every future dispute about changed data from a negotiation into a measurement. Contracts shape engineering; this clause shaped ours more than any other.

FAQ

How do I migrate from Yoast without losing SEO data?

Use an importer that copies values verbatim, resolves Yoast template variables through Yoast’s own API, and verifies results with round-trip tests including non-Latin characters and emoji.

Why did my SEO titles change after a plugin migration?

Common causes: the importer resolved template variables in the wrong language context, applied a text sanitizer, or stripped brand suffixes with a cleanup heuristic. All are importer bugs — the data should never change.

Should an importer sanitize imported data?

No. Sanitize at output time with escaping. Input sanitizers mutate legitimate content (HTML, line breaks, backslashes) and break byte-fidelity.

What is byte-for-byte data fidelity?

The guarantee that a value read back from storage is the identical byte sequence that was submitted — verified by automated tests across scripts, emoji, and special characters.

Can I run two SEO plugins during a migration?

Yes, if one defers its output while the other remains authoritative. Per-feature deference prevents duplicate meta tags during the verification window and lets you cut over only after the imported data is proven identical.

How do I verify a migration didn’t change my data?

Diff a sample of records byte-by-byte between source and destination, including titles with non-Latin text, emoji, and special characters. Any changed byte — whitespace included — is a defect worth investigating.


Next: Part 7: The Invisible-Character Bug in Hebrew SEO Titles

Keep reading

Newsletter

Get the next playbook in your inbox.

One short, no-fluff email per fortnight.

Ready when you are

Let's map the next 90 days of growth.

Book a no-pressure call. We'll review your funnel, share quick wins, and outline what compounding growth could look like for your business.

Get a free audit

A note on cookies

We use cookies to measure how this site performs and to make it better. Analytics cookies only run if you accept.

Read our privacy policy