Block Deprecations and Migrations
Deprecations allow you to update block structure while maintaining backward compatibility with previously saved content.
Why Deprecations
When you change:
- Attribute structure
- Save function output (HTML)
- Attribute sources or selectors
- Block markup structure
You need deprecations to ensure old content still loads and displays correctly.
Basic Deprecation
import { registerBlockType } from '@wordpress/blocks';
registerBlockType( 'my-plugin/card', {
// Current version
attributes: {
title: { type: 'string' },
content: { type: 'string' },
// New attribute (wasn't in v1)
subtitle: { type: 'string' },
},
save: ( { attributes } ) => {
const { title, subtitle, content } = attributes;
return (
<div className="card">
<h2>{ title }</h2>
{ subtitle && <h3>{ subtitle }</h3> }
<p>{ content }</p>
</div>
);
},
// Previous versions
deprecated: [
{
// Version 1: no subtitle
attributes: {
title: { type: 'string' },
content: { type: 'string' },
},
save: ( { attributes } ) => {
const { title, content } = attributes;
return (
<div className="card">
<h2>{ title }</h2>
<p>{ content }</p>
</div>
);
},
},
],
} );
Deprecation Properties
| Property | Type | Description |
|---|---|---|
attributes |
object | Attribute definitions for this version |
supports |
object | Block supports for this version |
save |
function | Save function for this version |
migrate |
function | Transform old attributes to new |
isEligible |
function | Check if block matches this deprecation |
Attribute Migration
Transform old attributes to new structure:
deprecated: [
{
attributes: {
text: { type: 'string' },
textColor: { type: 'string' },
},
migrate: ( oldAttributes ) => {
// Transform to new structure
return {
content: oldAttributes.text,
style: {
color: {
text: oldAttributes.textColor,
},
},
};
},
save: ( { attributes } ) => {
// Old save function
return <p style={ { color: attributes.textColor } }>{ attributes.text }</p>;
},
},
],
Returning Inner Blocks
When migration creates inner blocks:
migrate: ( oldAttributes, innerBlocks ) => {
const newAttributes = {
title: oldAttributes.heading,
};
// Create new inner blocks from old content
const newInnerBlocks = [
createBlock( 'core/paragraph', {
content: oldAttributes.content,
} ),
...innerBlocks,
];
return [ newAttributes, newInnerBlocks ];
},
isEligible
Check if a block instance matches this deprecation:
deprecated: [
{
attributes: {
content: { type: 'string' },
align: { type: 'string' },
},
isEligible: ( attributes ) => {
// This deprecation applies if 'align' attribute exists
return typeof attributes.align !== 'undefined';
},
migrate: ( attributes ) => {
return {
content: attributes.content,
alignment: attributes.align, // Renamed attribute
};
},
save: ( { attributes } ) => {
return <div className={ `align-${ attributes.align }` }>{ attributes.content }</div>;
},
},
],
Complex Eligibility
isEligible: ( attributes, innerBlocks ) => {
// Check attributes
if ( attributes.version && attributes.version >= 2 ) {
return false;
}
// Check inner blocks structure
if ( innerBlocks.some( block => block.name === 'core/paragraph' ) ) {
return true;
}
return false;
},
Multiple Deprecations
Order matters – WordPress tries each deprecation in array order:
deprecated: [
// Most recent deprecation first
{
// Version 2 -> 3 migration
attributes: { /* v2 attributes */ },
save: /* v2 save */,
migrate: /* v2 -> v3 migration */,
},
{
// Version 1 -> 2 migration (then 2->3 will run)
attributes: { /* v1 attributes */ },
save: /* v1 save */,
migrate: /* v1 -> v2 migration */,
},
],
Complete Example: Multi-Version Migration
const v1Attributes = {
text: { type: 'string' },
color: { type: 'string' },
};
const v1Save = ( { attributes } ) => {
return (
<div style={ { color: attributes.color } }>
{ attributes.text }
</div>
);
};
const v2Attributes = {
content: { type: 'string' },
textColor: { type: 'string' },
};
const v2Save = ( { attributes } ) => {
return (
<div className={ `has-${ attributes.textColor }-color` }>
{ attributes.content }
</div>
);
};
// Current version (v3)
const v3Attributes = {
content: {
type: 'string',
source: 'html',
selector: 'p',
},
style: {
type: 'object',
},
};
registerBlockType( 'my-plugin/text-block', {
attributes: v3Attributes,
save: ( { attributes } ) => {
const { content, style } = attributes;
return (
<div { ...useBlockProps.save() }>
<p>{ content }</p>
</div>
);
},
deprecated: [
// v2 -> v3
{
attributes: v2Attributes,
save: v2Save,
migrate: ( { content, textColor } ) => {
return {
content,
style: {
color: { text: textColor },
},
};
},
},
// v1 -> v2 (will then migrate to v3)
{
attributes: v1Attributes,
save: v1Save,
migrate: ( { text, color } ) => {
return {
content: text,
textColor: color,
};
},
},
],
} );
Supports Changes
When supports change, include old supports in deprecation:
deprecated: [
{
attributes: { /* same */ },
supports: {
// Old supports - no color support
align: true,
},
save: ( { attributes } ) => {
// Old save without color classes
return <div>{ attributes.content }</div>;
},
},
],
// Current block has new supports
supports: {
align: true,
color: {
text: true,
background: true,
},
},
HTML Structure Changes
When markup changes:
// Old save wrapped content in <section>
const oldSave = ( { attributes } ) => {
return (
<section className="old-wrapper">
<p>{ attributes.content }</p>
</section>
);
};
// New save uses <div>
const newSave = ( { attributes } ) => {
return (
<div className="new-wrapper">
<p>{ attributes.content }</p>
</div>
);
};
deprecated: [
{
attributes: { content: { type: 'string' } },
save: oldSave,
// No migrate needed if attributes unchanged
},
],
InnerBlocks Migration
Convert old content to inner blocks:
deprecated: [
{
attributes: {
items: {
type: 'array',
default: [],
},
},
migrate: ( attributes ) => {
const innerBlocks = attributes.items.map( ( item ) =>
createBlock( 'my-plugin/item', {
title: item.title,
content: item.content,
} )
);
return [
{}, // New attributes (empty, items now in inner blocks)
innerBlocks,
];
},
save: ( { attributes } ) => {
return (
<ul>
{ attributes.items.map( ( item, i ) => (
<li key={ i }>{ item.title }</li>
) ) }
</ul>
);
},
},
],
Dynamic Block Deprecations
For dynamic blocks, deprecations still matter for the save function:
registerBlockType( 'my-plugin/dynamic', {
attributes: {
postId: { type: 'number' },
showMeta: { type: 'boolean', default: true }, // New attribute
},
// Save returns null for dynamic blocks
save: () => null,
deprecated: [
{
attributes: {
postId: { type: 'number' },
// No showMeta in old version
},
// Even dynamic blocks need old save
save: () => null,
migrate: ( attributes ) => {
return {
...attributes,
showMeta: true, // Default for migrated blocks
};
},
},
],
} );
Testing Deprecations
Create test cases for each deprecation:
// In your test file
import { registerBlockType, createBlock, serialize } from '@wordpress/blocks';
import { deprecated } from './deprecations';
describe( 'Block Deprecations', () => {
it( 'migrates v1 to current', () => {
const v1Block = createBlock( 'my-plugin/card', {
text: 'Hello',
color: 'red',
} );
// Trigger migration
const [ newAttributes ] = deprecated[ 1 ].migrate( v1Block.attributes );
expect( newAttributes.content ).toBe( 'Hello' );
expect( newAttributes.style.color.text ).toBe( 'red' );
} );
} );
Avoiding Deprecations
Minimize deprecations by:
- Use
metasource sparingly: Meta changes don’t need deprecations - Plan attribute structure: Think about future additions
- Use object attributes: Easier to extend than flat attributes
- Save minimal HTML: Less markup = fewer breaking changes
- Consider dynamic blocks: No HTML to deprecate
Best Practices
-
Keep all deprecations: Never remove old deprecations.
-
Test migrations: Verify old content migrates correctly.
-
Order by version: Newest deprecation first.
-
Include old save: Must match exactly what old version saved.
-
Handle all attributes: Migrate all attributes, not just changed ones.
-
Document versions: Comment which version each deprecation handles.
-
Use isEligible carefully: Only when save functions look identical.
-
Test in real content: Try with actual posts using old block versions.