Change Block Markup Without Breaking Existing Posts

Changing the save() output of a static WordPress block can invalidate every existing instance because the editor compares stored markup with the block’s current serialization. The safe pattern is to keep a deprecated definition that can recognize the old markup, migrate its attributes, and save the new structure when the post is updated. This tutorial changes a callout block from a single paragraph to a semantic wrapper with a heading while preserving content already stored in posts. The same approach supports several historical versions when a block has evolved repeatedly.
Understand what WordPress validates
A static block stores HTML plus a comment delimiter containing attributes. When the editor parses a post, it runs the current save() implementation and compares its expected markup with the saved HTML. A changed element, class, attribute source, or wrapper can make the old instance invalid.
Do not “fix” this by disabling validation or editing the database blindly. Treat saved block markup as a persisted format with versions, much like a database schema.
Capture a real legacy fixture
Copy an exact block instance from the Code editor before changing implementation:
<!-- wp:adz/callout {"tone":"info"} -->
<p class="wp-block-adz-callout is-info">Back up before updating.</p>
<!-- /wp:adz/callout -->
Keep this fixture in automated tests. Handwritten approximations often miss whitespace, class ordering, or attribute serialization that determines whether recovery works.
Write the new save implementation
The new version wraps the message in an aside and introduces a title attribute.
export default function save( { attributes } ) {
const { message, title, tone } = attributes;
const blockProps = useBlockProps.save( {
className: `is-${ tone }`,
} );
return (
<aside { ...blockProps }>
<h2 className="wp-block-adz-callout__title">{ title }</h2>
<p className="wp-block-adz-callout__message">{ message }</p>
</aside>
);
}
Define message, tone, and the new title attribute in block.json, giving the title a sensible default. Current code should describe only the current format; legacy behavior belongs in a deprecation.
Describe the old format exactly
A deprecation is an older block definition with its own attributes, save function, and optional migration. WordPress tries deprecations in array order until one validates.
const v1 = {
attributes: {
message: {
type: 'string',
source: 'html',
selector: 'p',
},
tone: {
type: 'string',
default: 'info',
},
},
save( { attributes } ) {
const { message, tone } = attributes;
return (
<p className={ `wp-block-adz-callout is-${ tone }` }>
{ message }
</p>
);
},
migrate( attributes ) {
return {
...attributes,
title: 'Important',
};
},
};
export default [ v1 ];
Keep deprecated save functions self-contained. Importing a helper that later changes can silently break old versions. Snapshot small utility logic inside the deprecation when long-term compatibility matters.
Use isEligible only for non-markup migrations
Most markup changes need no isEligible; failed current validation causes WordPress to try deprecated versions. Use isEligible when old and new markup both validate but attributes still require migration. An overly broad eligibility function can repeatedly migrate current blocks, so test it with both formats.
Test parsing, validation, and serialization
- Parse the legacy fixture with the block parser.
- Confirm it is recognized by the deprecated definition without a validation error.
- Confirm migration adds the expected title and preserves message and tone.
- Serialize the migrated block and compare it with a current-format fixture.
- Open an old post in the editor, make an unrelated change, save, and reload.
- Verify the front-end result before and after resaving.
Include empty strings, custom classes, alignments, duplicated blocks, reusable blocks, and content copied between sites. Content shapes reveal assumptions faster than one ideal fixture.
Decide whether a dynamic block is a better fit
If markup must change frequently for accessibility or design-system reasons, server-side rendering may reduce future serialization migrations. A dynamic block can keep a minimal fallback in saved content while PHP produces current front-end markup. It still needs stable attributes and compatibility tests, and editor previews require attention, but the front-end structure is no longer frozen into every post.
Roll out with evidence and a rollback path
Inventory the block with WP-CLI or a database-safe content search, then test representative legacy posts in staging. Release the deprecation before any bulk content rewrite. Monitor editor console errors and support reports. Do not remove a deprecated version merely because it is old; remove it only when you can prove no supported content still relies on it.
For a bulk normalization, use a separate resumable migration command, preserve backups, and record changed post IDs. Plugin activation is the wrong place for an unbounded rewrite.
Accessibility fixes can justify an urgent markup change, but urgency does not remove compatibility work. Ship the corrected current implementation and the legacy deprecation together, then verify that migrated output improves semantics without losing editor content. If an old format is actively unsafe to render, prefer a controlled server-side fallback and an audited migration over silently discarding or reshaping user-authored data.
Treat serialized markup as a public contract
A static block’s HTML belongs to published content, not just to today’s component implementation. Real legacy fixtures, exact deprecated definitions, narrow migrations, and serialization tests let the block evolve without asking editors to recover invalid content manually. WordPress’s block deprecation reference documents the API; the essential practice is keeping every supported historical format testable for as long as it exists in real posts.
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.


