Contents
Introduction — Why attribute migrations matter
When a block evolves, its attribute schema often changes: keys are renamed, types change, data moves from HTML to JSON, or attributes are replaced by inner blocks. Without a planned migration strategy, older saved posts break in the editor or lose data. WordPress block deprecations are the supported mechanism to provide backwards compatibility: they teach Gutenberg how to recognize old saved markup, extract the old attributes, and convert them into the new attribute shape so the current editor UI can handle existing content safely.
How block deprecations work (conceptual)
When Gutenberg loads a post, it compares the saved markup for each block against the blocks current save output. If theres no match, Gutenberg tries each deprecated version (in the order listed) to see if any deprecated save matches the saved content. If a deprecated save matches, Gutenberg uses that deprecated versions attribute definitions to parse attributes from the saved content, then runs that deprecated versions migrate function (if present). The migrate function returns attributes in the new shape, and the block is hydrated in the editor using the current edit implementation.
Key points
- save must reproduce prior saved markup — the deprecated save implementation should match exactly the HTML that older posts contain so Gutenberg can detect and parse it.
- attributes — deprecated attributes describe how to read data from the matched HTML (sources like attribute, html, text, query, etc.).
- migrate(attributes, innerBlocks) — called after parsing attributes from the old markup it should return attributes shaped for the current block version.
- order matters — list deprecations from newest to oldest or otherwise in an order where more likely matches are first Gutenberg uses the first match it finds.
Anatomy of a deprecation entry
A typical deprecation entry used in registerBlockType(…) looks like this:
{ attributes: { // how the old markup stored values same form as registerBlockType attributes }, save: (props) =gt { // render output that exactly matches the previously saved HTML for that version }, migrate: (oldAttributes, innerBlocks) =gt { // return a new attributes object compatible with the current block version }, }
You supply an array of such entries on your current block registration:
registerBlockType( my-plugin/my-block, { // current attrs, edit, save... deprecated: [ / deprecated versions here / ], } )
When to use deprecations
- Renaming an attribute key.
- Changing attribute type (e.g., string → array/object).
- Switching the attribute source (e.g., from HTML content to a JSON attribute or vice-versa).
- Changing save markup (for example refactoring the DOM structure or class names).
- Replacing attributes with innerBlocks or moving data into block markup that requires different parsing.
Suppose version 1 saved a button with a buttonText attribute. In version 2 you renamed it to label. Provide a deprecation that defines the old attribute and a migrate function that renames it.
// new block registration (version 2) registerBlockType( acme/button, { title: Acme Button, category: common, attributes: { label: { type: string, default: Click }, // current attribute color: { type: string } }, edit: EditComponent, save: SaveComponent, deprecated: [ { // Version 1 attributes attributes: { buttonText: { type: string }, color: { type: string } }, save: ( props ) =gt { const { attributes } = props return ( // This must match the HTML older posts have wp.element.createElement( button, { className: acme-button, style: { color: attributes.color } }, attributes.buttonText ) ) }, migrate: ( oldAttrs ) =gt { // Rename buttonText -> label return { label: oldAttrs.buttonText Click, color: oldAttrs.color } } } ] } )
Explanation: Gutenberg will match the old saved button markup with the deprecated save. It will parse the old attributes (buttonText and color), call migrate to return {label, color}, and then hydrate the current block using the new attributes.
Example 2 — Changing attribute source: text inside element → attribute
Older code stored a title inside an element (source: html or text). New code wants to store the title as an HTML attribute (source: attribute on a tag). Provide a deprecated attributes definition that reads the old HTML, and a migrate function that maps it into the new attribute.
// New block attributes: attributes: { title: { type: string }, // will now be saved as attribute data-title subtitle: { type: string } // unchanged }, deprecated: [ { attributes: { // Old version: title came from text insidetitle: { type: string, source: html, selector: h2 }, subtitle: { type: string, source: text, selector: .sub } }, save: ( props ) =gt { const { attributes } = props return ( wp.element.createElement( div, { className: legacy }, wp.element.createElement( h2, null, attributes.title ), wp.element.createElement( div, { className: sub }, attributes.subtitle ) ) ) }, migrate: ( oldAttrs ) =gt { // current version expects title to be stored as an attribute on the wrapper: return { title: oldAttrs.title, subtitle: oldAttrs.subtitle } } } ]
In the current versions save function you will output the wrapper with a data attribute:
save: ( props ) =gt { const { attributes } = props return ( wp.element.createElement( div, { className: acme-card, data-title: attributes.title }, wp.element.createElement( div, { className: subtitle }, attributes.subtitle ) ) ) }
Example 3 — Changing type: string gt array (comma-separated → array)
If an attribute used to be a comma-separated string and now is an array shape, write a migrate function to split and clean items.
deprecated: [ { attributes: { tags: { type: string } // old: a,b,c }, save: ( props ) =gt { return wp.element.createElement( div, null, props.attributes.tags ) }, migrate: ( oldAttrs ) =gt { const raw = oldAttrs.tags const array = raw.split( , ) .map( s =gt s.trim() ) .filter( Boolean ) return { tags: array } } } ] // current attributes: tags: { type: array, default: [] }
Example 4 — Moving data into innerBlocks (approach)
If an older version stored a list of items as a JSON attribute and the new block uses innerBlocks (child blocks for each item), migration requires:
- Reading the JSON attribute for items in the deprecated attributes.
- Transforming it into child block instances to be used in the editor.
Important note: migrate receives both attributes and innerBlocks. To convert attribute data into innerBlocks, return attributes in the new shape and allow the blocks edit to transform attributes into real inner blocks (or create innerBlocks programmatically). In some cases, you may prefer a two-step approach: use migrate to convert the JSON into a new attribute that signals a conversion is needed, then transform into inner blocks inside edit() if innerBlocks is empty. This is safer because editing contexts vary.
Pattern (safe):
- Deprecated entry reads old JSON attribute itemsJson.
- Migrate converts itemsJson → items (array) and sets a flag convertToInnerBlocks: true.
- The current edit() sees convertToInnerBlocks === true and programmatically creates inner blocks (using wp.data or createBlock) then clears the flag.
// deprecated entry { attributes: { itemsJson: { type: string } // old: JSON string }, save: ( props ) =gt { return wp.element.createElement( div, null, props.attributes.itemsJson ) }, migrate: ( oldAttrs ) =gt { let items = [] try { items = JSON.parse( oldAttrs.itemsJson [] ) } catch ( e ) { items = [] } return { items: items, // new attribute as structured array convertToInnerBlocks: true // flag for edit() to convert } } } // In edit(): if convertToInnerBlocks, create child blocks using wp.blocks.createBlock(...) and dispatch insertBlocks
Example 5 — Deprecated save for exact HTML matching
A very common reason Gutenberg fails to match an old block is small differences in whitespace, class order, or markup. The deprecated save function should produce markup as close as possible to what older saves produced. If older versions used string concatenation or different wrappers, reproduce that in the deprecated save.
// imagine older save usedTextsave: ( props ) =gt { return wp.element.createElement( div, { className: cta }, wp.element.createElement( span, null, props.attributes.ctaText ) ) }
Server-side rendered (dynamic) blocks and deprecations
Dynamic blocks typically have save() return null and are rendered by PHP via a render_callback. However, older dynamic block versions may have serialized different attributes or different markup in their comments. When a dynamic blocks save returns null, the block is still saved as a block comment with attributes (). Deprecations for dynamic blocks still help when a previous version included inner markup or when you changed the attribute schema. Strategy:
- Include deprecated definitions with the old attribute schemas so Gutenberg can parse old comment JSON into the old attributes.
- Provide migrate to convert attribute shapes into the current schema.
- Ensure PHP render_callback understands the new attributes for display on the front end.
If an older version produced actual HTML in post content (e.g., you accidentally stored server-rendered HTML instead of the usual comment wrapper), your deprecated save must reproduce that HTML to match and parse attributes out of it.
Block.json and deprecations
If you register blocks via block.json and register_block_type_from_metadata, you can include a deprecated array in your block.json metadata. Each deprecated entry can contain an attributes object, a save implementation (as JS, typically provided via your script), and optionally migrate. In practice you will usually put the migrate code and save implementations in your JS file while listing the deprecated attribute schema in block.json. Check the version of WordPress and Gutenberg you support — behavior and tooling around block.json continue to improve, so test with your target environment.
Testing migrations locally
- Create representative legacy content: either a saved post with the old block markup, or use the editors code editor to paste old block HTML.
- Open the post in the block editor Gutenberg should match the saved markup against deprecated save implementations and migrate attributes.
- Inspect block attributes in the editor: open the browser console and do:
const blocks = wp.data.select(core/block-editor).getBlocks() console.log( blocks )
- Confirm the migrated attributes match expectations and that the block renders in the editor UI.
- Save the post and inspect post_content to ensure the new save output is produced for new saves.
Debugging tips
- Missing or inaccurate deprecated save markup is the most common problem — reproduce exact HTML structure and class names.
- Use the editor console to inspect parsed attributes before and after migration.
- Use temporary debug code in migrate() to console.log oldAttrs to confirm what Gutenberg read from the saved HTML.
- Watch for differences in whitespace, self-closing tags, and HTML entity encoding — these can affect matching.
- Remember selectors in attribute definitions (selector: .title) must match the same structure older saves used.
Best practices and recommendations
- Keep deprecated saves minimal but exact. Reproduce the legacy HTML to allow parsing. Do not try to be clever in deprecated save — matching accuracy is primary.
- Prefer migrate functions that are deterministic and idempotent. Running migrate multiple times (or editing after partial conversion) shouldn’t corrupt data.
- Flag complex migrations. If converting attributes into innerBlocks, use a conversion flag attribute and do the heavy lifting in edit() where you can create blocks via APIs safely.
- Don’t delete deprecated entries immediately. Keep deprecations for several releases to give older posts time to be opened/edited and migrated.
- Document breaking changes in your plugin changelog so integrators know why deprecations were added.
- Test across WordPress versions you support — block parsing behavior can differ between editor versions.
Pitfalls to avoid
- Relying on migrate to fabricate data — prefer to preserve original values and transform them predictably.
- Changing save markup in a way that old deprecated saves cannot match (e.g., removing wrapper elements entirely without adding deprecation entries).
- Assuming migrate can fully create innerBlocks in all contexts — sometimes edit() needs to finalize conversion.
- Removing deprecated code immediately after a single release — this causes content to be stranded until a user updates the post in the editor.
Advanced tips
- Use short-lived flags for big conversions — add an attribute like migratedToV2 and have edit() perform the expensive conversion once, then clear the flag.
- Batch post updates with WP-CLI — if you need to proactively update many posts’ content to the new save output, you can write a script that loads each post, runs the block parser, migrates block attributes programmatically (using parse and serialize helpers) and saves the updated content. Take care: such mass changes should be performed with backups and tests.
- Unit test migrate functions — write Jest tests for pure migrate functions (they take oldAttrs and innerBlocks and return newAttrs) to guard against regressions.
Example: full registerBlockType with multiple deprecations
Complete example showing multiple deprecations this is a simplified pattern demonstrating how to stack migrations:
registerBlockType( acme/hero, { title: Acme Hero, attributes: { title: { type: string }, ctaLabel: { type: string }, imageId: { type: number } }, edit: HeroEdit, save: HeroSave, deprecated: [ // v2: replaced buttonText with ctaLabel { attributes: { title: { type: string, source: html, selector: .hero-title }, buttonText: { type: string }, imageId: { type: number } }, save: ( props ) =gt { return wp.element.createElement( section, { className: hero }, wp.element.createElement( h2, { className: hero-title }, props.attributes.title ), wp.element.createElement( button, { className: hero-cta }, props.attributes.buttonText ) ) }, migrate: ( old ) =gt { return { title: old.title, ctaLabel: old.buttonText, imageId: old.imageId } } }, // v1: stored title as attribute on wrapper { attributes: { titleAttr: { type: string, source: attribute, selector: .hero, attribute: data-title }, buttonText: { type: string } }, save: ( props ) =gt { return wp.element.createElement( section, { className: hero, data-title: props.attributes.titleAttr }, wp.element.createElement( button, { className: hero-cta }, props.attributes.buttonText ) ) }, migrate: ( old ) =gt { return { title: old.titleAttr, ctaLabel: old.buttonText } } } ] } )
Maintaining compatibility across releases
Treat deprecations as part of your public contract with content. Once a block has been shipped and content exists on user sites, you should keep deprecations long enough to allow content to be opened in the editor. Monitor telemetry or user reports to identify how often older versions are encountered, then plan safe removal only after sufficient time.
Checklist before releasing a breaking block change
- Write deprecated entries that precisely reflect prior saved markup.
- Include migrate functions that map legacy attributes to the new shape.
- Test with real legacy post content in the editor across WordPress versions you support.
- Ensure server-side rendering (PHP) handles the new attributes.
- Keep deprecations in place for multiple releases document the change in changelog.
- Consider providing tooling (WP-CLI command or admin tool) to migrate many posts if needed.
Further reading and resources
Summary
Block deprecations are the correct and supported way to migrate block attributes and saved markup between versions. The core idea is straightforward: reproduce old save output, parse the old attributes, and provide a migrate function to produce the new attribute shape. With careful deprecation entries, deterministic migrate functions, and appropriate testing, you can evolve blocks without losing user content or breaking the editor.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |