Blog

Gutenberg Block Deprecation: Update Without Breaking Content

A practical guide to safely updating Gutenberg blocks using deprecation in block.json, with steps, examples, and honest trade-offs.

Summary

Updating a Gutenberg block often breaks existing posts that use the old version. This article shows you how to use the deprecated property in block.json to maintain backward compatibility. You'll learn the exact steps to capture the current block markup, define one or more deprecated versions, and map attributes correctly. We'll cover both static and dynamic blocks, with practical examples. The article also pushes back on the assumption that deprecation is always the best approach, discussing when a clean break might be better. By the end, you'll be able to update your blocks confidently without breaking your users' content.

The Breaking Change Scenario

You launched a custom testimonial block six months ago. It outputs a simple <div> with a quote and an author name. Now your client wants a new design: the author should appear above the quote, with a different CSS class. You update the block's save function and render_callback. You test on a fresh post—it looks great. Then you navigate to an old post that uses the block. Disaster: the quote text is gone, the author is in the wrong place, and the styling is off. You've just broken every page that uses the block.

This is the classic "breaking change" problem in Gutenberg block development. Blocks are essentially data structures combined with markup. When you change the markup, the editor can't automatically map old content to the new structure. The result is either a validation error (the block becomes invalid) or—worse—silent corruption where the block renders incorrectly.

What Is Block Deprecation?

Block deprecation is Gutenberg's built-in mechanism for handling version changes. By defining a deprecated array in your block's block.json, you tell the editor: "If you encounter a block that matches one of these older versions, transform it into the current version." Each deprecated entry specifies the previous attributes, supports, and save function (or render_callback). When the editor loads an old block, it runs through the deprecated array in order and applies the first matching transformation.

This feature is often underused because developers assume they'll never need to change a block's markup. But in real-world projects, requirements evolve. If you skip deprecation, you either force users to delete and reinsert blocks (poor experience) or maintain two separate versions of the block (messy). The official WordPress developer handbook covers this in the Block Editor Handbook, but practical walkthroughs are lacking.

Step 1: Capture the Current State

Before making any changes, record the exact save output (or render_callback for dynamic blocks) and the attributes your block currently uses. Think of it as taking a snapshot. For static blocks, save the JSX returned by the save function. For dynamic blocks, save the PHP markup generated by render_callback.

Create a new file in your plugin called deprecated.js (or similar) and store the old save function there. Alternatively, keep the deprecated versions directly in the block's main JavaScript file. The key is to preserve this code exactly as it was when the block was first deployed.

Step 2: Define Your Deprecated Versions

In your block.json, add a deprecated array. Each entry is an object that can include:

  • attributes (object): The previous attribute definitions.
  • supports (object): Any previous support settings that changed.
  • save (function or string): The previous save function. For JavaScript-only blocks, you'll import the old function. For PHP-rendered dynamic blocks, you can use migrate and render_callback instead.
  • migrate (function): A function that maps old attributes to new ones (optional).

Example:

"deprecated": [
  {
    "attributes": {
      "quote": { "type": "string", "source": "html", "selector": ".quote" },
      "author": { "type": "string", "source": "html", "selector": ".author" }
    },
    "supports": {},
    "save": "() => <div className=\"testimonial-legacy\"><p className=\"quote\">{attributes.quote}</p><p className=\"author\">{attributes.author}</p></div>"
  }
]

Note: The save function in block.json is typically defined in JavaScript. If you're using an external script, you'll need to enqueue it and reference the function name. Alternatively, you can inline the function as a string (though this isn't recommended for complex blocks).

Step 3: Map Attributes

Often, you change not only the markup but also the attribute names or sources. For example, you might switch from storing the author as a plain string to a rich text field. In such cases, use the migrate property to transform old attributes to new ones.

migrate: (attributes) => {
  return {
    quote: attributes.quote,
    author: { content: attributes.author, level: 2 }
  };
}

If you don't provide a migrate function, the editor will simply pass the old attributes directly to the new block. That might cause errors if attribute names changed.

Step 4: Test with Real Content

After defining the deprecated version, test thoroughly. Create a new post, insert the old block (you can simulate by pasting the serialized block code from an existing post), and verify that it converts to the new version without validation errors. Also test editing and saving the converted block. Repeat for multiple deprecated versions if you have them.

For dynamic blocks, the process is similar but with a twist: the save function for a dynamic block typically returns null (the block renders via PHP). In the deprecated entry, you can either set save to the previous static markup that was used before you switched to dynamic rendering, or use a render_callback in PHP that handles both old and new attribute structures. This is more complex but doable.

Caveats and Trade-offs

Deprecation is powerful, but it has downsides. Each deprecated version adds code to your plugin. Over time, you can end up with a chain of five or six legacy versions that are rarely used but must be maintained. The WordPress core team recommends keeping at least two versions back, but beyond that, you might consider a clean break if the number of affected posts is small.

Another nuance: the order of deprecated entries matters. The editor iterates through the array from index 0 upward and uses the first match. If two deprecated versions are similar, the wrong one might be applied. Always list the most recent deprecated version first (the one that directly precedes the current version).

Finally, deprecation does not handle content that was edited using a site-wide style change (e.g., via theme.json). If your block's appearance relied on global styles that have since changed, the deprecation won't adjust for that. You may need to add a migration script that runs on save or via a plugin update hook.

When Deprecation Isn't the Answer

Most tutorials frame deprecation as mandatory. In reality, there are situations where a clean break is better. If your block is new and used in only a handful of posts, manually updating those few instances might be faster than writing and testing deprecation code. Similarly, if the block's underlying data model is fundamentally different (e.g., you're merging two blocks into one), deprecation may not be flexible enough. In that case, write a one-time migration script that runs when the plugin updates, converting old blocks to the new format.

Another contrarian point: deprecation should not be used as a substitute for good design. If you anticipate frequent changes, design your block with versioning in mind from the start—e.g., by storing a version attribute and using conditional rendering. This approach, discussed in Beyond Basic Blocks, is lighter than deprecation but requires foresight.

Conclusion

Block deprecation is an essential tool for any serious Gutenberg developer. It allows you to evolve your blocks without breaking users' content. The key steps are: capture the current state, define the deprecated version in block.json, map attributes if needed, and test with real content. But remember that deprecation comes with maintenance costs. Sometimes a clean break or a versioned block design is more pragmatic. Use deprecation strategically, not automatically, and your blocks will remain robust through many updates.

For a broader perspective on building maintainable plugins, see Building Robust WordPress Plugins. And if you're new to block development, Beyond Basic Blocks will help you get started.

Sources (5)