How to fix "This block contains unexpected or invalid content" in WordPress
The editor compared the HTML saved for a block with what the block's code writes today, and the two differ. The saved content is intact, and nothing changes until you press a button. Copy the post first, then choose by what changed the markup.
- By
- WP Ministry
- Published
- Tested on
- WordPress 7.1.3, PHP 8.3.35
In short
- Nothing is lost by opening the post. A block you leave alone is saved back with the HTML it had.
- Copy the post's content before you press anything, from the Code editor or with wp post get into your home folder.
- Attempt recovery rebuilds the block from what the editor could read. Whatever it could not read is dropped.
- Convert to HTML keeps the markup exactly. On a block that holds other blocks, it leaves those out.
- Many blocks from one plugin failing after an update is for the plugin's author to fix. Do not recover them one by one first.
- A warning that returns after every save means something rewrites the content on save, such as a plugin or a role that may not post unfiltered HTML.
"Block contains unexpected or invalid content." means the editor compared the HTML saved for that block with the HTML the block's code would write today, and the two do not match. The saved content is still in the database, and the public page usually still shows it. Nothing changes until you press one of the buttons, so take a copy of the post before you do.
WordPress 6.7 and later word it that way, with a button that says "Attempt recovery". WordPress 6.6 and earlier say "This block contains unexpected or invalid content." and "Attempt Block Recovery". The check is the same. "The editor has encountered an unexpected error." is a different message: there the editor itself has stopped, not one block's check.
What the editor is checking
A post made of blocks is stored as HTML. Each block sits between two HTML comments that name it, and the opening comment can hold the block's settings:
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Shipping rates</h3>
<!-- /wp:heading -->When the editor opens a post, it reads each block's settings out of the comment and the HTML, runs the block's own code on them, and compares the result with what is stored. If the two differ, it cannot be sure it would write your content back the way it found it. It shows the warning in place of the block and waits.
- The check runs in your browser. Nothing on the server marks a block as invalid, and visitors never see the warning. The public page is made from the stored HTML.
- A block you leave alone stays as it is. Save the post without touching the block, and the editor writes back the HTML it found.
Where a fix below is marked as tested, the test covers the server's side: what is stored, what a save or a replacement did to it, and that each command does what it says. Whether the editor then shows the warning is decided in the browser, and no command can show that.
What each choice does
The warning has one button, "Attempt recovery", and a "More options" menu (three dots) that holds "Resolve" and "Convert to HTML".
| Choice | What it does | What can be lost |
|---|---|---|
| Attempt recovery | Makes the block again, as the same kind of block, from the settings and content the editor could read out of what is stored. Blocks inside it stay. | Whatever the editor could not read into a setting: markup or an attribute added by hand, or an image address a plugin moved to another attribute. If the settings in the comment are unreadable, the block comes back with its defaults. |
| Resolve | Opens a dialog, "Resolve Block". "Current" shows the stored markup. "After Conversion" shows what converting it to blocks would give, with the differences marked. | Nothing, until you press one of its two buttons. |
| Convert to Blocks (in that dialog) | Reads the stored HTML as if you had pasted it, and makes whatever blocks fit. | The result can be other kinds of block. Settings kept only in the comment are dropped, and so are blocks nested inside. |
| Convert to HTML | Puts the stored markup, unchanged, into a Custom HTML block. | The block's own controls: from then on you edit HTML. On a block that holds other blocks, such as Cover, Group or Columns, the blocks inside are left out. |
| Leave it | Nothing. Saving the post writes the block's HTML back as it was stored. | Nothing, but you cannot edit that block. Settings in a comment that was already unreadable are not written back. |
WordPress 7.0 and earlier also offered "Convert to Classic Block", which put the stored markup into a Classic block. WordPress 7.1 removed it.
Which to choose:
- You want the block the way its settings describe it: Attempt recovery.
- You want to keep markup someone wrote by hand: Convert to HTML, on a block with no blocks inside it.
- You are not sure: Resolve, read the two columns, and close the dialog.
- Many blocks of one plugin show the warning: none of them yet. Go to the plugin's fix below.
Every choice can be undone with Undo while the editor is open, and nothing reaches the database until you save.
How to tell which cause you have
The editor records what differed, in the browser's console. With the post open, press F12 or Ctrl+
Each invalid block writes an error that reads "Block validation: Block validation failed for" and then names the block, as in core/. Under it, "Content generated by save function" is what the block's code writes now, and "Content retrieved from post body" is what is stored. The difference between the two is the change you are looking for.
| What you find | Go to |
|---|---|
| One block, in a post someone edited in the Code editor or with "Edit as HTML" | Recover or convert the block in the editor |
| Every block of one plugin, in many posts, since that plugin or theme was updated | Go back to the plugin version the content was made with |
| Blocks of several kinds, in many posts, since a search-replace, an import or a move | Read what is stored, then repair it |
| Stored markup that has lost a tag or gained attributes, and a warning that returns after a save | Stop the content being changed on save |
| Only a block being built for this site | Add a deprecation to the block |
| A notice that says "Your site doesn’t include support for" | Switch the block's plugin back on |
Fixing many posts at once
The check runs in the browser, one post at a time, when that post is opened. No WordPress screen runs it across a site, and no WP-CLI command does: the block commands in WP-CLI's reference list, count, parse and edit blocks, and none compares a block with what its code writes. What the server can do is find every post that holds the block, so you know the size of the job.
Where it goes wrong
A page request passes through each of these in turn. This one comes from WordPress itself.
- Browser
- DNS
- HTTPS
- CDN or firewall
- Web server
- PHP
- WordPress (this error comes from here)
- Database and files
What causes it
The block's markup was edited by hand
CommonSomeone changed the block's HTML in the Code editor or with "Edit as HTML". The block's code would not have written that markup, so the two no longer match.
Fix: Keep a copy of the post before you choose, or Recover or convert the block in the editor, or Restore the revision from before the change
An update changed the markup a plugin's or theme's block writes
CommonThe new version of the block writes different HTML and does not recognize what the old version wrote. Every block of that kind, in every post, fails at once.
Fix: Keep a copy of the post before you choose, or Go back to the plugin version the content was made with, or Add a deprecation to the block, or Find every post that holds the block
A search-replace, an import or a migration altered the stored HTML
SometimesA replacement run across the database changed part of a block and not the settings that describe it, broke the settings in its comment with a quote, or an import dropped the backslashes the settings are written with.
Fix: Keep a copy of the post before you choose, or Recover or convert the block in the editor, or Read what is stored, then repair it, or Find every post that holds the block
Something rewrites the content when it is saved
SometimesA plugin changes the HTML on its way into the database, or WordPress strips markup because the person saving has a role without the unfiltered_
html capability. The stored block then differs from what its code writes. Fix: Keep a copy of the post before you choose, or Recover or convert the block in the editor, or Restore the revision from before the change, or Stop the content being changed on save
A custom block's save code changed
SometimesA block being built for the site had the output of its save function changed, with no deprecation for the markup it wrote before.
Fix: Keep a copy of the post before you choose, or Add a deprecation to the block, or Find every post that holds the block
The plugin that provides the block is switched off or gone
SometimesThis shows a different notice, which begins "Your site doesn’t include support for". The editor has no code for the block at all, so it compares nothing.
How to fix it
Keep a copy of the post before you choose
- Easy
- No risk
- About 5 minutes
- Steps tested on WordPress 7.1.3
Step 1: Copy it from the Code editor
Open the Options menu, the three dots at the top right of the editor, and choose "Code editor". Select everything in the box, copy it, and paste it into a plain text file on your computer. Choose "Exit code editor" to go back.
Step 2: Or copy it over SSH
The post's ID is the number after
post=in the editor's address bar. From the folder that holdswp-config.php, the first line writes the stored content to a file in your home folder, outside the folder the site is served from. The second counts the lines in the copy that open a block.A number above zero means the copy holds the post's blocks. How to use WP-CLI to manage a WordPress site covers connecting and running a first command.
bashwp post get 123 --field=post_content > ~/post-123.html grep -c '<!-- wp:' ~/post-123.html
To put a copy back, paste it into the Code editor in place of what is there, and save.
To undo it: Nothing to undo. Delete the copy once the post is right.
Recover or convert the block in the editor
- Easy
- Low risk
- About 10 minutes
Step 1: Read the comparison first
Open "More options" on the warning and choose "Resolve". Under "Current" is the stored markup. Anything there that was written by hand will not survive Attempt recovery.
Step 2: Recover the block, or keep the markup
Close the dialog and press "Attempt recovery" to get the block back with its controls. An extra class on the block is kept: the editor moves it to the block's "Additional CSS class(es)" setting. Other added attributes, and tags the block does not write, are dropped.
To keep hand-written markup as it is, choose "Convert to HTML". A Custom HTML block is the right home for it: the editor does not check that block this way.
Step 3: Check it, then save
Compare the block with your copy. If something is missing, press Undo and choose again. Then save and reload the editor.
If saving fails with "Updating failed", that is a separate fault: see how to fix "Updating failed".
To undo it: Use Undo before you leave the editor. After saving, restore the earlier revision, or paste your copy back in the Code editor.
Restore the revision from before the change
- Takes care
- Low risk
- About 15 minutes
- Steps tested on WordPress 7.1.3
WordPress keeps a revision each time a post is saved. If a hand edit or a save broke the block, the revision before it holds the markup that worked.
Step 1: In the editor
Open the Settings sidebar, select the Post or Page tab, and select the number next to "Revisions". Find the revision from before the change and select "Restore". Do this as an Administrator: if the content was stripped because of the role of the person who saved, the same role strips it again on restore.
Step 2: Or list the revisions over SSH
Use the post's ID. The
post_column is the ID of the user who made each save.author bashwp post list --post_type=revision --post_parent=123 --fields=ID,post_date,post_authorStep 3: See what a save changed
Give the IDs of two revisions, the earlier one first. Lines that begin with
-are only in the earlier one, and lines that begin with+only in the later.bashwp post get 456 --field=post_content > ~/revision-456.html wp post get 457 --field=post_content > ~/revision-457.html diff -u ~/revision-456.html ~/revision-457.html | head -n 20Step 4: Put the earlier one back
This replaces the post's content with that revision's.
--usermakes WP-CLI act as that user, so name an administrator.It answers "Success: Updated post 123." Reload the editor to see whether the warning has gone.
bashwp post get 456 --field=post_content > ~/revision-456.html wp post update 123 ~/revision-456.html --user=admin-login
A search-replace changes revisions too, because they are rows in the same table. This fix does not undo one.
To undo it: Restore the later revision the same way.
Stop the content being changed on save
- Takes care
- Low risk
- About 20 minutes
- Steps tested on WordPress 7.1.3
A recovered block fails again if the next save rewrites it. Two things do that.
The role of the person saving. WordPress filters the HTML of anyone who lacks the unfiltered_ capability. On a single site, Administrators and Editors have it, and Authors and Contributors do not. On a multisite network only a Super Admin has it. The filter removes tags such as <svg>, <iframe>, <script>, <style> and <form>, and attributes such as onclick. A plugin's block that draws an icon inline no longer matches after such a save. A Custom HTML block is not checked, so an embed inside it disappears with no warning at all.
A plugin. One that writes lazy-loading attributes into the stored <img> on save, or moves an image's address to another attribute, leaves an Image block that fails the check.
Step 1: Find who made the save
List the post's revisions, as in the fix above, and note the
post_of the revision where the markup changed. Then ask for that user's role by ID:author bashwp user get 45 --fields=user_login,rolesStep 2: If the role lacks the capability
Have an Administrator or an Editor edit the posts that hold such blocks. Or, if you would trust this person with everything an Editor may do, change their role: Users, then the user, then Role. Over SSH:
WordPress's documentation warns against giving this capability to users you do not trust, because it lets them post any HTML and JavaScript. On multisite a role change does not help: a Super Admin has to make the edit.
bashwp user set-role user-login editorStep 3: If an Administrator's save changes it too
Then a plugin is doing it. On a staging copy, switch plugins off and save the post again until the stored markup stops changing. How to find and fix a WordPress plugin conflict has the method.
Step 4: Restore the content
Put the earlier revision back as an Administrator, with the fix above.
To undo it: Set the user's role back with the same command, naming the old role.
Read what is stored, then repair it
- Takes care
- Back up first
- About 30 minutes
- Steps tested on WordPress 7.1.3
A replacement that changes an address everywhere it appears does not cause this: the comment and the HTML change together, and core blocks still match. The damage comes from a change to one and not the other.
Step 1: Look at one affected block as it is stored
Use the post's ID, and the block's name as its comment writes it:
heading, orplugin-name/for a plugin's block.block-name bashwp post get 123 --field=post_content | grep -n -A 2 '<!-- wp:heading 'Step 2: Compare the comment with the markup under it
- The tag or a class differs from the settings. A replacement of
<h2with<h3leaves a Heading block whose comment still describes a level 2 heading. - A straight double quote sits inside the comment's settings. The settings can no longer be read, and the block is treated as having none.
- Text such as
u0026oru003cappears in the comment. The editor writes&and<there as\u0026and\u003c. An import that handed the content to WordPress without escaping it first has dropped the backslashes.
- The tag or a class differs from the settings. A replacement of
Step 3: Repair it
For a few blocks, press "Attempt recovery" on each. A heading that the replacement turned into
<h3>goes back to the level its comment gives. Use the block's own controls for the change you wanted.For many posts, restore the database backup from before the replacement or the import, and run it again correctly. How to move WordPress to a new domain shows a replacement done with a dry run first. With no backup to return to, set one up before changing anything else: see how to schedule automatic WordPress backups.
The command shows what is stored. Whether the repaired block then passes is seen only in the editor.
To undo it: Restore the database backup taken before the repair.
Go back to the plugin version the content was made with
- Takes care
- Back up first
- About 30 minutes
- Steps tested on WordPress 7.1.3
When every block from one plugin fails after an update, the author changed what the block writes and left out the code that recognizes the older markup. Only the author can add it. Do not press "Attempt recovery" on each block first: where the new code cannot find the content in the old markup, the recovered block can come back empty.
WordPress's own blocks carry that code: a List block saved in the older format, for example, is updated without a warning.
Step 1: Note the version you have now
bashwp plugin get plugin-slug --field=versionStep 2: Install the version from before the update
The plugin's page on WordPress.org lists its earlier versions under "Advanced View", with a warning that they may not be secure or stable. Treat it as a stop until the author's fix.
--forcereplaces the installed files and keeps the plugin active.It ends "Success: Installed 1 of 1 plugins." Without SSH, download that version from the same page, upload the zip on the Plugins screen, and choose "Replace current with uploaded". A paid plugin's earlier versions come from its author.
bashwp plugin install plugin-slug --version=1.2.3 --forceStep 3: Hold it there
If the plugin updates itself, select "Disable auto-updates" beside it on the Plugins screen, or the next automatic update brings the fault back.
Step 4: Reload a post, then tell the author
The blocks made before the update should load without the warning. Report the fault on the plugin's support forum, with the block's name and the two pieces of markup from the console. Update again only when a release says it is fixed. How to safely update WordPress, plugins and themes shows how to try that release first.
That the blocks then pass is seen only in the editor.
To undo it: Update the plugin again from the Plugins screen.
Add a deprecation to the block
- Advanced
- Low risk
- About 30 minutes
This one is for whoever writes the block's code. A block has no version number. When its current save does not reproduce the stored markup, the editor tries each entry in the block's deprecated list. The first whose save does reproduce it is used to read the block, and the current save then writes it afresh.
Copy the save function and the attributes exactly as they were before the change. They are not inherited from the current version. Here the wrapper changed from <div> to <aside>:
const attributes = {
text: { type: 'string', source: 'text', selector: 'p' },
};
registerBlockType( 'acme/notice', {
// The block's other settings stay as they are.
attributes,
save: ( { attributes: { text } } ) => (
<aside { ...useBlockProps.save() }>
<p>{ text }</p>
</aside>
),
deprecated: [
{
attributes,
save: ( { attributes: { text } } ) => (
<div { ...useBlockProps.save() }>
<p>{ text }</p>
</div>
),
},
],
} );Reload a post that holds the old markup. The warning is gone, and the console shows "Updated Block" with the block's name in place of the validation error. The post is rewritten in the new markup the next time it is saved. List the newest deprecation first, and remove one only when no post still holds its markup.
To undo it: Remove the deprecation and put the earlier save function back.
Switch the block's plugin back on
- Easy
- Low risk
- About 10 minutes
- Steps tested on WordPress 7.1.3
This is a different notice:
Your site doesn’t include support for the "plugin-name/
block-name" block. You can leave it as-is, convert it to custom HTML, or remove it.
WordPress 6.6 and earlier end it "You can leave this block intact, convert its content to a Custom HTML block, or remove it entirely." It has one button, "Keep as HTML". Here the editor has no code for the block at all, because the plugin or theme that provides it is not active. The block's content is still stored, and the public page still shows whatever HTML was saved for it.
Step 1: Find the plugin
The first part of the name in the notice is the namespace of the plugin or theme that registered the block, and it usually resembles the plugin's name. List the plugins that are installed and switched off:
bashwp plugin list --status=inactive --fields=name,status,versionStep 2: Activate it
On the Plugins screen, select "Activate" under the plugin. Over SSH:
If the plugin was deleted, install it again: its blocks are still in your posts. Then reload the post. That the block loads with its controls is seen only in the editor.
bashwp plugin activate plugin-slug
If you mean to stop using the plugin, leave the block as it is, or press "Keep as HTML" to turn it into a Custom HTML block that needs no plugin.
To undo it: Deactivate the plugin again on the Plugins screen.
Find every post that holds the block
- Takes care
- No risk
- About 10 minutes
- Steps tested on WordPress 7.1.3
Step 1: Search the stored content for the block's comment
Put the block's name between
wp:and the space, as the comment writes it:headingfor a core block,plugin-name/for a plugin's. The space keeps a longer name that begins the same way out of the list.block-name bashwp db query "SELECT ID, post_type, post_status, post_title FROM $(wp db prefix)posts WHERE post_type <> 'revision' AND post_content LIKE '%<!-- wp:plugin-name/block-name %' ORDER BY ID"Step 2: Read the list
Each row is a post, a page, or another kind of content that holds the block.
wp_is a synced pattern, andblock wp_andtemplate wp_are the theme's templates. The list says where the block is. It cannot say which of them the editor will refuse.template_ part Step 3: Decide who does the rest
A handful of posts can be opened and fixed one at a time. Rewriting stored markup in dozens of posts is a developer's job: a script that changes the database, run on a staging copy and checked in the editor before it goes near the live site. WP Ministry's one-time WordPress fix starts with a diagnosis: a written cause and a fixed quote.
To undo it: Nothing to undo. The command only reads.
When to get help
Stop when blocks from one plugin are invalid across many posts and its author has no fix yet, when recovery leaves blocks empty, or when the warning comes back after every save and you cannot find what changes the content. Repairing stored markup in many posts at once means changing the database, and that is tried on a staging copy first.
Common questions
Will pressing "Attempt recovery" wreck my page?
It changes one block, in the editor only, and Undo reverses it. Nothing is stored until you save. What it can drop is whatever the block's settings do not describe, such as markup added by hand. Copy the post first, and compare the block with the copy before saving.
Is my content lost, and do visitors see the warning?
No to both. The message is about a mismatch, not about missing content, and it exists only in the editor. The public page is built from the stored HTML, so it usually looks as it did.
Why did many blocks break at once after an update?
A plugin or theme changed the HTML its block writes, and did not include a deprecation for the old markup. Every block of that kind made before the update then fails. Go back to the earlier version and report it to the author.
Why does the warning come back after I fix it?
Something changes the content each time the post is saved. It is either a plugin that rewrites the HTML, or the role of the person saving. Compare two revisions to see what the save changed, and who made it.

