Skip to content

Instantly share code, notes, and snippets.

@paulo-carvajal
Created August 4, 2026 08:39
Show Gist options
  • Select an option

  • Save paulo-carvajal/2dbb789a95014245c420491eec517acd to your computer and use it in GitHub Desktop.

Select an option

Save paulo-carvajal/2dbb789a95014245c420491eec517acd to your computer and use it in GitHub Desktop.
5 WordPress block deprecation patterns that cover most real cases

WordPress Block Deprecation Patterns

5 real-world deprecation patterns that cover most cases when maintaining custom Gutenberg blocks.

Pattern 1: Markup-only change

When only the rendered HTML changed but attributes stayed the same. No migrate() needed.

// Current version (v2) - saves <div class="new-wrapper">
save: ({ attributes }) => {
  return (
    <div className="new-wrapper">
      <h2>{ attributes.title }</h2>
    </div>
  );
},

// Deprecation for v1 - saved <section class="old-wrapper">
deprecated: [
  {
    attributes: {
      title: {
        type: 'string',
        source: 'html',
        selector: 'h2',
      },
    },
    save: ({ attributes }) => {
      return (
        <section className="old-wrapper">
          <h2>{ attributes.title }</h2>
        </section>
      );
    },
    // No migrate() needed - attributes didn't change
  },
],

Pattern 2: Attribute rename or reshape

When you renamed an attribute or changed its structure. Use isEligible() to detect old content and migrate() to rewrite it.

// Current version (v2)
attributes: {
  heading: {
    type: 'string',
    source: 'html',
    selector: 'h2',
  },
  showSubtitle: {
    type: 'boolean',
    default: false,
  },
},

deprecated: [
  {
    // v1 had 'title' instead of 'heading', and 'subtitle' as string
    attributes: {
      title: {
        type: 'string',
        source: 'html',
        selector: 'h2',
      },
      subtitle: {
        type: 'string',
        source: 'html',
        selector: 'p',
      },
    },
    isEligible: (attributes) => {
      return attributes.title !== undefined && attributes.heading === undefined;
    },
    migrate: (attributes) => {
      return {
        heading: attributes.title,
        showSubtitle: attributes.subtitle !== '' && attributes.subtitle !== undefined,
        subtitle: attributes.subtitle,
      };
    },
  },
],

Pattern 3: Attribute source change

When you moved an attribute from markup to block comment (or vice versa), or changed the selector. The attributes schema in the deprecation must match what's actually in the database.

// Current version (v2) - reads from block comment
attributes: {
  linkUrl: {
    type: 'string',
    source: 'attribute',
    attribute: 'href',
    selector: 'a',
  },
  linkTarget: {
    type: 'string',
    default: '_self',
  },
},

deprecated: [
  {
    // v1 stored linkUrl in markup with different selector
    attributes: {
      linkUrl: {
        type: 'string',
        source: 'attribute',
        attribute: 'data-link',
        selector: 'div.custom-block',
      },
      linkTarget: {
        type: 'string',
        source: 'attribute',
        attribute: 'data-target',
        selector: 'div.custom-block',
      },
    },
    // No migrate() needed if attribute names stayed the same
    // Just need the old save() to reconstruct the old markup
    save: ({ attributes }) => {
      return (
        <div className="custom-block"
             data-link={ attributes.linkUrl }
             data-target={ attributes.linkTarget }>
          <a href={ attributes.linkUrl }>Click</a>
        </div>
      );
    },
  },
],

Pattern 4: InnerBlocks structure change

When the nesting structure changed. migrate() must remap the inner blocks array.

// Current version (v2) - expects [heading, content] as direct children
save: ({ attributes }) => {
  return (
    <div className="card-v2">
      <InnerBlocks
        template={[
          ['core/heading', { level: 2 }],
          ['core/paragraph'],
        ]}
      />
    </div>
  );
},

deprecated: [
  {
    // v1 wrapped content in a group block
    save: ({ attributes }) => {
      return (
        <div className="card-v1">
          <InnerBlocks
            template={[
              ['core/heading', { level: 2 }],
              ['core/group', {}, [
                ['core/paragraph'],
              ]],
            ]}
          />
        </div>
      );
    },
    migrate: (attributes, innerBlocks) => {
      // Find the group block and extract its children
      const groupBlock = innerBlocks.find(block => block.name === 'core/group');
      if (!groupBlock) return { attributes, innerBlocks };
      
      // Flatten: [heading, group] → [heading, ...group.children]
      const newInnerBlocks = innerBlocks.flatMap(block => {
        if (block.name === 'core/group') {
          return block.innerBlocks;
        }
        return block;
      });
      
      return { attributes, innerBlocks: newInnerBlocks };
    },
  },
],

Pattern 5: Stacked deprecations (ordered correctly)

WordPress tries each deprecation in order. Put the newest old format first to avoid unnecessary checks.

// Current version (v3)
attributes: {
  title: { type: 'string' },
  subtitle: { type: 'string' },
  showAuthor: { type: 'boolean', default: false },
},

deprecated: [
  // v2 → v3: renamed 'author' to 'showAuthor'
  {
    attributes: {
      title: { type: 'string' },
      subtitle: { type: 'string' },
      author: { type: 'boolean' },
    },
    isEligible: (attrs) => attrs.author !== undefined && attrs.showAuthor === undefined,
    migrate: (attrs) => ({
      title: attrs.title,
      subtitle: attrs.subtitle,
      showAuthor: attrs.author,
    }),
  },
  
  // v1 → v2: added 'subtitle', no 'showAuthor' at all
  {
    attributes: {
      title: { type: 'string' },
    },
    isEligible: (attrs) => attrs.subtitle === undefined && attrs.author === undefined,
    migrate: (attrs) => ({
      title: attrs.title,
      subtitle: '',
      showAuthor: false,
    }),
  },
  
  // v0 → v1: very old format with different structure
  {
    attributes: {
      heading: { type: 'string' }, // was 'heading' not 'title'
    },
    isEligible: (attrs) => attrs.heading !== undefined && attrs.title === undefined,
    migrate: (attrs) => ({
      title: attrs.heading,
      subtitle: '',
      showAuthor: false,
    }),
  },
],

Usage notes

  • Always test each deprecation with real saved content from the database
  • Order matters: newest old format first, oldest last
  • isEligible() is optional but recommended for clarity and performance
  • If you add a new deprecation later, insert it at the top of the array
  • Keep old save() functions intact - they reconstruct the markup to extract attributes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment