Block Contains Unexpected or Invalid Content: Fix It

Block Contains Unexpected or Invalid Content: Fix It

When a block contains unexpected or invalid content, the editor is telling you one specific thing: the HTML stored in post_content no longer matches what that block’s save() function produces today. Nothing is corrupt, nothing is lost, and the fix is usually three clicks — but which three clicks depends on why the two stopped matching, and the warning itself does not tell you that.

The block editor re-runs every block’s save function when it loads a post, then compares the result against the markup already in the database. A mismatch of a single attribute, a changed wrapper tag, or an apostrophe stored as ' instead of ' is enough to fail the comparison. That is why this warning appears on posts you have not touched in a year: something else changed — a plugin update, a theme switch, a sanitiser rewriting your markup on save.

The short version, if you are mid-deadline

Open the browser console (F12) before you click anything. Find the line beginning Block validation: Block validation failed for and read the Expected and Actual blocks underneath it. If the difference is cosmetic — a class name, an entity, an empty attribute — click Attempt Block Recovery and update. If the Actual output is missing content that is in Expected, do not click recovery: use Convert to HTML instead, or roll back to a revision.

Why a block contains unexpected or invalid content

Static blocks store their own output. When you save a paragraph, the editor serialises it to HTML wrapped in block comments and writes the whole thing into post_content:

<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">Hello.</p>
<!-- /wp:paragraph -->

The comment carries the attributes. The HTML between the comments is a cached copy of what save() returned at the moment you pressed Update. On the next load, the editor parses the attributes out of the comment, feeds them back through the block’s current save function, and checks whether it gets the same HTML. If it does, the block is valid. If it does not, you get the warning.

That comparison is done in JavaScript, in your browser, and it logs its reasoning. Open the console on the affected post and you will see something close to this:

Block validation: Block validation failed for `core/html`.

Expected:
<p>Terms &amp; conditions apply</p>

Actual:
<p>Terms & conditions apply</p>

Expected is what the current save function wants. Actual is what is in the database. In that example the whole problem is one ampersand that was stored raw and is now being escaped — harmless, and recovery will fix it cleanly. The console is the only place that tells you which it is, which is why reading it first is not optional.

Dynamic blocks — the ones registered with a PHP render_callback and no save() output — cannot fail this way. They store nothing but the comment, so there is nothing to compare. That asymmetry matters later when we get to prevention.

Three warnings that look the same and are not

Half the time people spend on this error is spent fixing the wrong problem, because the editor has three different failure notices and they get described interchangeably in support threads. Check which one you actually have before doing anything.

What the editor saysWhat is actually wrongWhat fixes it
“This block contains unexpected or invalid content.”The block type is registered and working. Its stored HTML does not match its current save outputRecovery, Resolve, or Convert to HTML. Covered below
“Your site doesn’t include support for the namespace/block block.”The block type is not registered at all. The plugin or theme that provided it is deactivated, deleted, or failing to load its editor scriptReactivate the plugin. Do not click recovery or convert — you will lose the block’s settings. Fix registration first
“This block has encountered an error and cannot be previewed.”A JavaScript exception was thrown while rendering the block’s edit component. A script conflict or a genuine bugCheck the console for the stack trace. Usually a plugin conflict rather than a content problem
The middle row is the one people most often misdiagnose as invalid content. The distinguishing detail is whether the notice names the block type.

Converting an unregistered block is a one-way trip

If the notice names a block type your site does not support, the attributes in the block comment are the only record of its configuration — slider settings, query parameters, form field definitions. Convert it to HTML and you keep the rendered markup but throw the attributes away. Reactivate the plugin instead, even if you intend to remove it later; export or screenshot the settings first, then remove it deliberately.

What actually breaks the match

In rough order of how often I find each one on client sites:

A plugin update changed a block’s markup without a deprecation. The author moved a wrapper from <p> to <div>, or renamed an attribute, and did not add an entry to the block’s deprecated array. Every post using that block goes invalid at once, which is the tell: if a hundred posts broke on Tuesday, look at what updated on Tuesday.
Something rewrote the stored HTML after the editor wrote it. Image optimisers and lazy-load plugins that filter content on save, security plugins that strip attributes, and migration scripts that run a search-replace across post_content all produce markup the save function would never have produced. Front-end filters on the_content are harmless here; it is the ones touching the saved value that cause this.
Someone hand-edited the HTML. Using Edit as HTML on a single block, or the full Code editor, lets you write markup the block cannot reproduce. Adding an id to a paragraph, nesting a <div> inside a heading, or adding an inline style all fail validation on the next load. Hand-written metadata.bindings JSON is the modern version of this: malformed JSON in the block comment is reported as an invalid block, not as a bad binding.
Block markup moved between sites running different versions. Copying a pattern from a newer site into an older one is a documented source of validation failures — core’s own patterns changed markup format between releases, and a 7.x pattern pasted into a 6.x site fails against the older save function. Same story for a staging site a few plugin versions ahead of production.
Character encoding and entities. Custom HTML blocks are the worst offenders: an & stored raw where the serialiser now wants &amp;, a straight apostrophe where it wants &#039;, or a boolean attribute stored as allowfullscreen where the editor writes allowfullscreen="". Cosmetic, annoying, and safe to recover.
A theme switch that changed what a block supports. Block supports — colour, spacing, typography — are opted into partly by the theme. Turning one off in theme.json can change the classes and inline styles a block emits, so the stored copy stops matching. This is the subtle one, because nothing in the plugin list changed.

If you cannot tell which of these it is, the ordinary isolation method works: deactivate plugins in halves and reload the post, watching whether the warning survives. Troubleshooting a WordPress plugin conflict step by step covers doing that without taking a live site down. Switching temporarily to a default block theme answers the theme.json question in the same way.

Fixing one block without losing the content

Four routes, in the order I would try them. None of them writes to the database until you click Update, so you can explore freely and leave without saving.

1. Attempt Block Recovery

The button on the warning itself. It discards the stored HTML and regenerates it from the attributes in the block comment. When the attributes are complete — which they are for a cosmetic entity or class-name mismatch — this is exactly right and the result is identical to what you had.

It is the wrong choice when the content lives in the HTML rather than in the attributes, and the attributes are incomplete. The classic failure is a heading that somehow ended up containing a nested paragraph: recovery regenerates the heading from its attributes and the nested content simply is not in them, so you get an empty heading. Read the console first and you will know which case you are in before you click.

2. Resolve, and read the diff

In the block’s ellipsis menu, Resolve opens a side-by-side comparison of the stored markup and what the editor would write, with removed content highlighted. It is the console output in a nicer frame, and it ends in the same two choices: Convert to Blocks to accept the regenerated version, or Convert to HTML to keep the raw markup as-is.

Use this when the console told you something was being dropped and you want to see exactly what before deciding.

3. Convert to HTML

Turns the block into a Custom HTML block containing the stored markup verbatim. Nothing is lost and the front end renders identically — but you have traded a real block for an opaque lump of HTML. It will not pick up theme styling changes, colour palette updates, or responsive settings, and the next person to edit the page gets a code box instead of a text field.

Treat it as a holding position for content you cannot afford to regenerate: convert, publish, then rebuild the block properly when you have time. It is also the honest answer for one-off hand-written markup that was never really a block in the first place.

4. Roll back to a revision

Worth remembering that the error is about stored content, so an earlier copy of that content is a legitimate fix — particularly when the damage was done by a search-replace or a sanitiser rather than by a code change. Restoring an earlier version of a post using revisions walks through comparing and restoring one.

Two caveats. Restoring brings back the markup that matched the old save function, so if the cause was a plugin update, the restored version will fail validation too. And revisions are capped — check WP_POST_REVISIONS in wp-config.php before you count on there being enough history.

When it hit every post at once

There is no WP-CLI command that finds invalid blocks, and there cannot be a straightforward one: validation means running the block’s JavaScript save function and diffing the output, so the answer only exists inside a loaded editor. Anything claiming to scan for broken blocks server-side is pattern-matching markup, not validating it.

What you can do from the shell is build the list of posts that use the suspect block, so you know the size of the job:

# Posts and pages containing a given block type
wp db query "SELECT ID, post_type, post_title FROM wp_posts
  WHERE post_content LIKE '%wp:acme/slider%'
  AND post_status IN ('publish','draft','pending','private')
  ORDER BY post_modified DESC" --skip-column-names

# Just the count, to decide between fixing by hand and asking the vendor
wp db query "SELECT COUNT(*) FROM wp_posts
  WHERE post_content LIKE '%wp:acme/slider%'" --skip-column-names

Substitute your table prefix and the real block name — you will find it in the block comment, or in the console line that named the failing block type. If that count is in the hundreds, stop and contact the plugin author before you start clicking: a missing deprecation is their bug, and a point release that adds one fixes every post at once with no edits from you.

For a few dozen posts, recovering block by block is tedious enough to be worth automating in the browser. Open the post in the editor, open the console, and run this against the loaded editor state. It uses the same documented store actions the recovery button uses, and recovers nested blocks too:

( function () {
  const store = 'core/block-editor';
  const { select, dispatch } = wp.data;
  const { createBlock } = wp.blocks;
  let fixed = 0;

  function walk( rootClientId ) {
    select( store ).getBlocks( rootClientId ).forEach( function ( block ) {
      if ( ! select( store ).isBlockValid( block.clientId ) ) {
        dispatch( store ).replaceBlock(
          block.clientId,
          createBlock( block.name, block.attributes, block.innerBlocks )
        );
        fixed += 1;
        return;
      }
      if ( block.innerBlocks.length ) {
        walk( block.clientId );
      }
    } );
  }

  walk();
  console.log( 'Recovered ' + fixed + ' block(s). Nothing is saved until you press Update.' );
} )();

Rehearse this on one post first

This is a blanket recovery: it accepts the regenerated output for every invalid block without showing you a diff, so it carries the same risk as clicking Attempt Block Recovery without reading the console — multiplied by however many blocks are on the page. Run it on one representative post, compare the result against the published page in another tab, and only then use it on the rest. It does not save, so a reload undoes everything.

Synced patterns and template parts

A synced pattern stores its blocks once, in a wp_block post, and every page that uses it renders from that single copy. So one invalid block inside a synced pattern shows up as invalid everywhere the pattern appears — and recovering it on a page does nothing lasting, because the page is not where the markup lives. Edit the pattern itself from the Patterns screen, recover it there, save, and all its uses come good at once.

Template parts in a block theme behave the same way: the header or footer is a separate entity, edited in the Site Editor, not in the post you were looking at. If the warning is in a region you cannot select in the post editor, that is why. Which container a given chunk of markup belongs in is its own decision, and it changes how far a single broken block can spread — synced patterns vs template parts works through that choice, including why an unsynced pattern confines the damage to one page while a synced one does not. Customising a block theme with theme.json or a child theme covers which parts of a block theme live where, which is also the quickest way to work out whether a theme.json change caused the mismatch in the first place.

If you build the block: write the deprecation

Every time you change what a block’s save() emits, you break the stored copies in your users’ databases unless you tell the editor how to read the old shape. That is what the deprecated array is for: a list of previous versions, each with its own attributes, supports and save, tried in turn when the current save function fails to match.

// v1 wrapped the text in a p; the current save uses a div.
const v1 = {
  attributes: {
    content: { type: 'string', source: 'html', selector: 'p' },
  },
  save( { attributes } ) {
    return createElement( 'p', null, attributes.content );
  },
};

registerBlockType( 'acme/notice', {
  // ...current attributes and edit...
  save( { attributes } ) {
    return createElement( 'div', null, attributes.content );
  },
  deprecated: [ v1 ],
} );

Three details from the API docs that catch people out. Deprecations do not chain like database migrations — each entry is matched independently against the original stored content, so a new migrate may need adding to several entries rather than just the newest. A deprecation whose save does not match is skipped entirely, including its migrate. And attributes, supports and save are not inherited from the current block, so each entry has to spell them out in full. List them newest-first so the likeliest match is tried first.

The alternative is to not store markup at all. A block registered with a PHP render_callback and no save output cannot fail validation, because there is nothing cached to compare. You give up the ability to edit the output as HTML and you render on every request instead of serving stored markup, so it is a real trade-off rather than a free win — but for anything whose output depends on site state, it is the right shape.

There is a lighter version of that same idea worth knowing before you build a block at all. If the only dynamic part is one value — a price, a date, a per-post link — the Block Bindings API lets a core Paragraph, Heading, Image or Button pull that value from a custom field at render time. The surrounding block is still a plain core block with ordinary stored markup, so you get server-resolved content without taking on a block’s deprecation burden.

Disclosure

I build and maintain Bloqra, a block plugin, so this is self-interested advice from someone who has had to ship deprecations and get them wrong. The practical point for site owners is not which block plugin to pick but how to judge one: before you build a hundred pages on any third-party block library — Bloqra, Kadence Blocks, GenerateBlocks or another — check whether its changelog mentions deprecations when markup changes, and whether its support forum fills up with invalid-content reports after each release. That is a maintenance signal you can verify yourself in ten minutes, and it tells you more than any feature list.

Reducing how often this happens

Update block plugins on staging and open three real posts. Not the homepage — the posts that use the plugin’s blocks most heavily. Validation failures only surface in the editor, so a smoke test that only checks the front end will miss them entirely. Updating plugins safely and testing a site after a major update cover building that habit.
Stop hand-editing block HTML. If a block will not do what you need, use a Custom HTML block deliberately rather than bending a paragraph into something it cannot reproduce. The second one looks fine until the next page load.
Check what your optimisation plugins write back. Anything offering to rewrite images, strip attributes or minify your content needs to be doing it on output, not on save. If a plugin’s settings mention modifying post content, test it on a draft and reload the editor before letting it near your archive.
Keep staging and production on the same versions. Content copied down from a site running newer core or newer block plugins carries markup the older site’s save functions cannot reproduce. Sync code first, content second.
Turn on debugging while you investigate. The validation notice itself is a console message, but a plugin breaking the editor’s scripts often shows up in the PHP log at the same time. Enabling WordPress debugging and reading error logs safely covers doing that without printing notices to visitors.

One piece of context worth having: core contributors are actively unhappy with how often users see this warning. A proposal in the Gutenberg repository would replace today’s binary valid/invalid check with five graded validation levels, so blocks with recoverable differences would be regenerated quietly instead of flagged. It has not been merged, and the objections raised against it are about exactly the risk described above — silently rewriting content can lose things nobody meant to lose. Treat it as a direction of travel, not a fix that is coming next release.

Frequently asked questions

Does this error affect what visitors see on the front end?

Usually not. The stored HTML is still in post_content and still renders, so the page looks normal to visitors while the editor complains. The exception is a block whose markup was mangled by whatever caused the mismatch — open the published page in another tab and check before you assume it is editor-only.

Is it safe to click Attempt Block Recovery?

Safe in the sense that nothing is written until you press Update, so you can always reload to undo it. Not always correct, though: recovery rebuilds the block from its attributes, so any content that lived only in the stored HTML goes missing. Read the Expected and Actual output in the console first. If Actual has content that Expected lacks, pick Convert to HTML instead.

Why did dozens of old posts break at the same time?

Because something shared changed. A plugin or theme update altered a block’s save output without a deprecation, or a bulk operation rewrote post_content across the site. Note the date the posts started failing, compare it against your update history, and check which block type the console names. A vendor fix is far cheaper than editing fifty posts by hand.

Can I find invalid blocks with WP-CLI or a database query?

Not directly. Validation runs the block’s JavaScript save function and diffs the result, so only a loaded editor can decide whether a block is valid. You can use wp db query with a LIKE on the block name to list every post that uses the suspect block, which tells you the scale of the problem even though it cannot tell you which ones fail.

How is this different from the missing block support notice?

Invalid content means the block type exists and works, but its stored markup no longer matches its save output. The notice that says your site does not include support for a named block means the block type is not registered at all, normally because its plugin is deactivated. Fix the second by restoring registration — converting it to HTML discards the attributes that hold the block’s settings.

Will converting to HTML hurt my SEO?

No. The markup is preserved verbatim, so headings, links and alt text stay exactly as they were and crawlers see no change. The cost is editorial rather than technical: the content stops responding to theme and palette changes, and it is harder for the next person to edit.

The warning is on my header or footer and I cannot select the block. Why?

Because it lives in a template part or a synced pattern, which is stored separately from the post. Open it in the Site Editor, or on the Patterns screen for a synced pattern, and recover it there. One fix in the right place resolves every page that uses it.

Sources

Leave a Reply to This Post