The error
You open a post in Gutenberg and hit a gray banner on one or more blocks:
This block contains unexpected or invalid content
Two buttons appear: Attempt Block Recovery and Convert to HTML. You click "Attempt Block Recovery." Nothing happens โ or the content disappears. Here's how to get it back.
Why this happens
Gutenberg stores blocks as HTML comments: <!-- wp:paragraph --> delimiters with JSON attributes embedded. On every load, it validates the saved markup against each block's registered schema. Any mismatch throws this error.
Six common triggers:
- A plugin or theme update changed a block's output format. Old saved markup no longer validates against the new schema.
- Content was written directly into the
post_contentcolumn via raw SQL orwpdb->update(), mangling the block comment delimiters. - A PHP notice or warning printed output before
<!-- wp:blockname -->, corrupting the delimiter string. - Copy-pasting from Word or Google Docs injected non-standard markup โ smart quotes,
, or extra wrapper divs โ breaking block parsing. - The block's JavaScript bundle wasn't loaded (missing
enqueue_block_editor_assetshook registration), so Gutenberg couldn't recognize the block type. - A plugin's
wp_update_post()call double-encoded block comment markers, turning<!--into<!--.
Step 1 โ Find the broken block
Before clicking any recovery button, switch to Code Editor mode: Ctrl+Shift+Alt+M (or โฎ menu โ Code editor). You'll see the raw block markup. Scan for these signs of corruption:
<!-- wp:paragraph -->
<p>Normal content</p>
<!-- /wp:paragraph -->
<!-- wp:columns {"someProp":"value"} --> <!-- โ broken if plugin changed the schema -->
<div class="wp-block-columns">...</div>
<!-- /wp:columns -->
Red flags: mismatched open/close tags, HTML entities like <!-- instead of <!--, stray PHP error text before a block marker, or a block type name that no longer exists after a plugin was removed.
Step 2 โ Try "Attempt Block Recovery" first
Click the three dots (โฎ) on the broken block โ Attempt Block Recovery. Gutenberg rewrites the delimiter and re-validates the attributes. For minor schema drift after a plugin update, this usually works. Content comes back, you save, done.
If recovery wipes the content instead โ hit Ctrl+Z immediately, before saving. The undo stack survives block recovery attempts.
Step 3 โ Convert to HTML (safe fallback)
Recovery didn't work? Select Convert to HTML. This wraps the raw markup in a wp:html block, which Gutenberg stores verbatim without schema validation. The post renders correctly on the frontend. You lose block interactivity โ column drag handles, reusable block syncing โ but nothing gets deleted.
<!-- wp:html -->
<div class="wp-block-columns">
<div class="wp-block-column">...</div>
</div>
<!-- /wp:html -->
Treat this as a temporary fix for a live site. Rebuild the block properly once you've tracked down the root cause.
Step 4 โ Fix corrupted delimiters in the database
When a plugin update breaks block attribute structure across many posts, use WP-CLI rather than editing each post manually:
# View raw post content
wp post get 123 --field=post_content
# Pull to a file, edit, then push back
wp post get 123 --field=post_content > /tmp/post123.html
# Edit /tmp/post123.html โ fix the block markup
wp post update 123 --post_content="$(cat /tmp/post123.html)"
If a plugin renamed its block type โ e.g. wp:old-plugin/card โ wp:new-plugin/card โ do a targeted search-replace. Always dry-run first:
# Dry run โ shows what would change, touches nothing
wp search-replace '<!-- wp:old-plugin/card' '<!-- wp:new-plugin/card' --all-tables --dry-run
# Looks right? Drop --dry-run
wp search-replace '<!-- wp:old-plugin/card' '<!-- wp:new-plugin/card' --all-tables
wp search-replace '<!-- /wp:old-plugin/card' '<!-- /wp:new-plugin/card' --all-tables
Step 5 โ Hunt for PHP errors polluting block output
A sneaky culprit: a plugin printing a PHP notice or warning before the REST API response. Gutenberg receives JSON with stray text prepended, can't parse it, and marks the block invalid on reload.
# Watch the error log while saving a post
tail -f /var/log/nginx/error.log | grep -i 'php\|notice\|warning'
# Or enable debug logging in wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Spot Notice: Undefined variable entries timestamped exactly when you save a post? That's your culprit. Fix the offending plugin. Short-term workaround for production:
error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
Step 6 โ Re-register missing block types
Deactivating a plugin removes its block registrations. Gutenberg flags every one of those blocks as invalid. Re-enabling the plugin fixes it instantly.
Want to remove the plugin permanently? Convert all its blocks to HTML (Step 3) before deactivating. Find the affected posts first:
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%wp:removed-plugin%' AND post_status != 'auto-draft';"
Verify the fix
- Open the post in Gutenberg โ no gray banners should appear.
- Switch to Code Editor mode. Confirm every
<!-- wp:blockname -->has a matching<!-- /wp:blockname -->. - Save, then hard-refresh with Ctrl+Shift+R. Gutenberg re-validates all blocks on load.
- View the post on the frontend โ content should render correctly.
- Run
wp post get <ID> --field=post_contentand scan for stray characters before block markers.
Prevent it from happening again
- Before any block plugin update: snapshot the database โ
wp db export backup_$(date +%Y%m%d).sql. Takes about 10 seconds and can save hours of recovery work. - Block plugins should ship a
deprecatedarray in their block registration so Gutenberg migrates old markup automatically. If an update breaks blocks without providing deprecations, that's a bug โ file one. - Never edit
post_contentdirectly via raw SQL. Usewp_update_post()and verify the returned content still has intact block delimiters. - Test plugin updates on staging first. A staging environment with recent production content catches validation errors before real users see them.

