Troubleshooting

Gutenberg Block Invalid Content: Validate and Repair Block Markup

The editor says a block contains unexpected or invalid content and stops there: no line, no attribute, no cause. Here is what it compared, why block markup drifts after a migration, and how to find the exact character that broke it.

Gutenberg Block Invalid Content: Validate and Repair Block Markup

You open a post you published two years ago and one block is grey. Above it sits a sentence: Block contains unexpected or invalid content. Below it, a button labelled Attempt recovery and a menu with three conversions. Nothing in that panel tells you which line, which attribute, or which character is wrong.

The editor does know. It printed the answer to the browser console a moment before it drew the warning: Block validation failed for `core/image`, then the content its save function generated, then the content it retrieved from the post body, one after the other. Most people never open the console, so the warning stays a shrug.

A block can be invalid at all because of a decision made when the block editor shipped: block structure lives in HTML comments inside post_content, and nowhere else. There is no block table, no JSON column, no schema in the database. The markup is the record.

That is what makes this fixable by reading. It is also what makes it your problem, because a find and replace, a migration script or a careless paste edits that record as plain text, with no idea that some of the text is structure.

What the warning is comparing

When a post loads in the editor, WordPress parses post_content into blocks, reads each block’s attributes from the comment JSON and from the HTML itself, then calls that block’s save function with those attributes. It compares the string it just generated against the string stored in the post. Both are tokenised and walked token by token. If a tag name, an attribute or a run of text differs, the block is marked invalid and you get the grey panel.

The comparison is not byte for byte. A class attribute is compared as a set, so reordering class names is fine. A style attribute is parsed into properties first. Boolean attributes only need to be present in both. But the number of attributes on a tag has to match exactly, and any attribute the save function did not write produces Encountered unexpected attribute and fails the block. Adding a title by hand to an image is enough.

Which means the fix is nearly always mechanical, and the fastest route to it is to read the markup the way the parser reads it rather than staring at a grey rectangle.

Block markup validator and repair

Paste the content of a post, the markup the block editor shows under Code editor, and this reads the block comments the way WordPress reads them: where a block opens, where it closes, and where the two do not line up. Every finding carries a line, a column and a sentence, because the editor only ever says that a block contains unexpected or invalid content. Then it repairs what can safely be repaired and shows the corrected markup, the block tree and a plain rendering of the content. Everything happens in this browser tab: nothing is uploaded, nothing is stored, nothing leaves the page.

empty

What is wrong

    Block checks

      Four rules the editor itself enforces when it compares the markup it finds with the markup it would write: an image whose id attribute disagrees with the wp-image-id class on the img, a heading whose level attribute disagrees with its tag, a list holding anything other than list items, and a code block whose content was never escaped. Each of these produces the same unhelpful notice in the editor.

      Repairs

      Switch one off and that kind of problem is still reported, it is simply left in the output. Attribute JSON is rewritten the way the editor writes it, with the two hyphens, the angle brackets and the ampersand escaped, which is also what puts a stray arrow inside an attribute value beyond harm. Nothing repairs attribute JSON that cannot be read in the first place, a closer whose name matches nothing that is open, or content that sits outside every block: those need a decision only you can make.

        WunderPaint
        The Dynamic Design and Automation Studio
        WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

        Block markup is HTML with the structure in comments

        Everything the block editor knows about a post is recovered by one regular expression in WP_Block_Parser::next_token(). It is worth looking at, because every surprising case in this article is hiding in it:

        /<!--s+(?P<closer>/)?wp:(?P<namespace>[a-z][a-z0-9_-]*/)?
         (?P<name>[a-z][a-z0-9_-]*)s+(?P<attrs>{ ... }s+)?(?P<void>/)?-->/s

        Read it left to right and you get the whole grammar. After <!-- there has to be at least one whitespace character. Then an optional / that makes it a closer. Then the literal wp:. Then an optional namespace, which defaults to core/ when it is absent. Then the block name, which must start with a lowercase letter and may contain only lowercase letters, digits, hyphens and underscores. Then an optional attribute object, which must be followed by whitespace of its own. Then an optional / that makes the block self closing.

        Miss any of that and the comment is not an invalid block. It is not a block. It is an HTML comment sitting in your content, invisible on the front end, and the editor will never warn you about it because there is nothing there to validate.

        Flow diagram of the block editor validation pass, from post content through the parser, the attributes and the save function to a token comparison and the invalid warning, with a table of which differences a block still survives.

        What the parser does with the tokens matters just as much. Openers are pushed onto a stack. A closer never checks the name against anything: it pops whichever frame is on top of the stack, so a stray <!-- /wp:group --> in the middle of a column will close the column and nobody will complain. A closer that arrives when the stack is empty is worse. The parser calls add_freeform() and returns false, which ends the parse loop, and the entire rest of the document becomes one freeform blob. One orphan closer near the top of a long post can silently demote every block after it.

        Attribute JSON has a similar quiet failure. The parser runs json_decode() on the object and never checks for an error. If the JSON is unreadable, the block loads with no attributes at all, which guarantees that the save function produces something different from what is stored, which produces the warning. The cause is a missing brace three lines up and the symptom is a block that looks empty.

        The three ways a valid block turns invalid

        The save function changed under it. A plugin update ships a new save implementation, the old markup in your posts no longer matches what the new code produces, and every existing instance goes grey at once. Block authors are supposed to register a deprecation for the previous shape so the editor can migrate old markup on load. When they forget, or when they change the output in a patch release, your archive pays for it. Nothing in your content is wrong in that case, and no markup repair will help.

        An attribute no longer exists on the block. Attributes that the block definition does not declare are dropped at parse time. If the stored HTML carries a class or an inline style that used to come from that attribute, the regenerated markup will not have it, and the token comparison fails on the very first tag.

        The stored HTML was edited. That covers the block’s own Edit as HTML, the whole post code editor behind Ctrl+Shift+Alt+M, and every tool that has ever written to post_content without parsing it: a search and replace during a domain move, an import, a sloppy sed over a database dump. This is the interesting one, because it is the case where the two copies of the same fact drift apart.

        How a class and an id come apart

        Here is an image block exactly as the editor writes it:

        <!-- wp:image {"id":42,"sizeSlug":"large"} -->
        <figure class="wp-block-image size-large">
          <img src="https://cdn.wp-image-editor.com/wp-content/uploads/2024/03/hero.jpg" alt="" class="wp-image-42"/>
        </figure>
        <!-- /wp:image -->

        The attachment ID is written twice. Once as "id":42 in the comment, once as wp-image-42 in the image class, because the save function builds that class from the attribute with a template literal. The parser reads the 42 from the comment. The save function writes wp-image-42 from what it read. As long as nothing touches the HTML, the two agree forever.

        Now delete that attachment, upload the replacement, and fix the posts with a find and replace on the file name. The src points at the new file, both copies of the ID still say 42, and the front end looks fine. Swap the ID in the comment but not the class, or the class but not the comment, and the mismatch is immediate: the class set contains wp-image-58 where the save function insists on wp-image-42.

        This is why replacing the file behind an existing attachment is a different operation from deleting and re-uploading, and why the difference shows up in your post markup rather than in the media library. It is also why a big media library cleanup deserves a check afterwards: every deleted attachment leaves an ID in some post’s block comment that now points at nothing.

        The recovery options, and what each one throws away

        Attempt recovery is the primary button. It builds a fresh block with createBlock(name, attributes, innerBlocks), using the attributes as they were parsed, and lets the current save function write the markup again. The block type survives. Everything in the stored HTML that was not captured by an attribute does not: a class you added by hand, an inline style, an extra wrapper. If the attribute JSON was the thing that failed, recovery gives you an empty default block, because there were no attributes to recover.

        Convert to HTML takes the original content byte for byte and drops it into a core/html block. Nothing is lost and nothing is gained: the markup is now opaque content, it will never be validated again, and it will never receive a future deprecation migration either. That is the right choice when the markup is correct and the block definition is the thing that vanished, for example after you removed the plugin that provided it. Convert to Classic Block does the same thing into a core/freeform block instead.

        Resolve opens a comparison dialog showing the stored content next to what the block would have produced, with a Convert to Blocks button. That path runs the same raw handler the editor uses when you paste HTML into a post. It reads the markup as HTML, matches whatever it recognises, and rebuilds the region as new blocks. Anything the raw transforms do not recognise ends up in paragraphs or an HTML block, so it is a rewrite, not a rescue.

        Table of the four recovery choices in the invalid content panel, showing which one keeps the block type, which ones keep the stored HTML byte for byte, and what each choice actually runs.

        Read as a set, the choice is about which copy of the truth you trust. Recovery trusts the attributes and rewrites the HTML. The conversions trust the HTML and abandon the block. When the markup is only slightly wrong, neither is what you want: you want the markup corrected and the block kept, which is a text edit, not a button.

        What the validator is doing underneath

        The tool above parses the block comment grammar itself, following the same rules the PHP parser follows: whitespace on both sides of the name, a closer that pops whatever is open regardless of its name, unreadable attribute JSON that silently loses every attribute. Every finding carries a line, a column and a sentence. An opener with no closer is reported together with where its enclosing block ends. A closer with no opener, a mismatched pair, a self closing block that also has a closer, content sitting outside every block, a stray --> inside an attribute value: each one gets a position you can jump to in your editor.

        Malformed attribute JSON is pinned to the exact character by a hand written scanner rather than a yes or no from a JSON parser, because knowing that column 61 is where the string was never closed is the difference between a two second fix and a hunt. The three near miss cases get their own findings too: the missing space after the arrow, the invalid block name, the attribute object that never closes. Those never produce an editor warning, since WordPress does not see them as blocks at all.

        The block tree is a collapsible outline with each block’s name, its attributes as written and the size of its inner HTML, with expand all and collapse all for long posts. The tree is deliberately built from what you evidently meant, matching a closer against the nearest opener of that name, while the sentences describe what WordPress actually does with the same input. The gap between those two readings is the entire bug class.

        A separate section runs the four rules the editor itself enforces on core blocks: a wp:image id against its wp-image-N class, a wp:heading level against the heading tag it wraps, wp:list children that are not wp:list-item, and wp:code content that was never escaped. Those are the checks that catch the drift described above before you paste the content back.

        Five repairs can be switched on independently: close unclosed blocks, drop orphan closers, reformat attribute JSON the way the editor writes it, correct the image class to match the id, correct the heading level to match the tag. Attribute JSON is re-serialised with the same escaping Gutenberg uses for --, <, >, & and the double quote, which is what disarms a stray arrow inside a value. The corrected markup is then re-parsed and re-checked, so the note underneath it says exactly what is left rather than claiming success. Copy it, push it back into the input for another pass, or reset.

        Everything happens in your browser. The paste is never uploaded, which matters when the content belongs to a client and is not published yet. The rendering tab follows the same rule: the markup is parsed in a detached document and rebuilt element by element, images and frames become labels instead of loading, links keep their text but lose their target, scripts and styles are dropped. Nothing in the paste can reach the network or run.

        Where it stops on purpose

        Three findings are reported and never repaired, because a human has to decide. Unreadable attribute JSON is never rewritten: there is nothing to re-serialise from, and that also blocks the heading level fix on the same block. A closer whose name matches nothing that is open is left alone, because only the author knows whether the name is wrong or the nesting is. Content that sits outside every block is flagged and never moved, since moving it changes the post. Those cases stay in the output deliberately, and the note under the corrected markup counts them.

        There are hard limits as well. The input is capped at 250,000 characters, the issue list at 80 rows, the tree at 1,500 rows and the rendering at 4,000 nodes. For scale, a 240 KB post with 2,554 blocks parses without a stall. Duplicate keys inside attribute JSON, deprecated block versions and block-specific attribute schemas beyond the four named checks are out of scope, and a block that went invalid because a plugin changed its save function is not a markup problem at all: no correction to your content will fix it, only the plugin’s own deprecation or a rollback.

        If the attribute JSON is genuinely mangled and has to be rebuilt by hand rather than reformatted, take the object into a JSON validator that shows what the parser actually saw and bring the repaired object back. And if the same breakage arrives with every import rather than once, the fix belongs in code that runs on save or on load, which usually means a small amount of PHP in a plugin or in the theme’s functions.php, with the usual care about what earns a place in that file.

        The markup is the database

        Storing block structure in comments was a good trade. Post content stays readable and portable, a site with the editor disabled still renders every post, and nothing is lost when a plugin disappears. The cost is that the structure has no protection. Every tool that has ever run a string replace across your posts table has been editing a data format while treating it as prose, and the block editor is the only thing that ever checks the result.

        That check happens one block at a time, in a panel with no line numbers, at the moment you least want to debug anything. Reading the markup directly inverts that: you see the whole post at once, you see the comments that never parsed as blocks alongside the ones that did, and you see the exact character where an attribute object went wrong instead of a sentence about unexpected content.

        The habit worth building is smaller than the tooling. After any operation that writes to post_content as text, a migration, a bulk replace, an import, open one or two of the affected posts and check that the structure still parses. The breakage is almost always uniform, so the first post tells you what happened to all of them, and a fix applied to the markup keeps the blocks that recovery would have quietly rewritten.

        Gutenberg Block Invalid Content: Validate and Repair Block Markup

        Table of Contents

        Learn it by building something

        Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

        One mail a week, and then it ends.
        Unsubscribe in one click.

        WordPress Images

        Image Quality Curve: Why Every Recommended Setting Is Wrong

        Quality 80 means one thing in JPEG, another in WebP, and something different again on a screenshot than on a photograph. Here is what the number actually sets, what SSIM can and cannot tell you, and how to find the point where your own file stops getting meaningfully smaller and starts getting visibly worse.

        Security & Privacy

        Screenshots Worth Publishing: Capture, Frame, Annotate

        An enormous amount of what people need to see is a picture of a screen, and most of those pictures are worse than they need to be for reasons that take seconds to fix.

        WordPress Development

        Custom Post Type WordPress: Register It in Code, Not a Plugin

        A complete register_post_type() snippet you can paste into a one-file plugin, plus an honest walk through the arguments that change real behaviour: why the block editor refuses to load without show_in_rest, and why every new single URL returns a 404 until the rewrite rules are flushed.

        Photo Editing

        Make a tidy application photo out of an ordinary phone snapshot

        A face detector of 190 KB returns a grid of anchors, a box and five landmark points, and everything after that is arithmetic: head height, eye line, the angle to level by, millimetres to pixels. An application photo, explicitly not an official passport photo, made without uploading your face anywhere.

        Photo Editing

        Brighten dark photos in your browser with a 590 KB neural network

        A brightness slider adds the same amount everywhere, which is why a dark photo goes pale instead of bright. A small trained network predicts a different correction for every pixel, plus one gamma and one colour matrix for the whole frame. It runs in your browser tab and your picture never leaves it.

        WordPress Development

        functions.php Explained: What It Is and How Not to Break Your Site With It

        functions.php is a normal PHP file that ships with your WordPress theme and runs on every page load, which makes it powerful, easy to misuse, and the wrong place for anything you want to keep. Here's what it actually does, and how to edit it without taking your site down.

        Download the free WunderPaint Plugin for WordPress

        The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

        The Image Editor & Design Studio

        Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

        Free

        Chaos Art

        Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

        Pro

        Particle Strokes

        Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

        Pro

        City Diorama

        Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

        Free

        Papercut Art

        Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

        Pro

        3D Earth Studio

        A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

        Free

        Mystic Studio

        Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

        Free

        Marble Bath

        Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

        Free

        Day Ring

        Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

        Free

        Code Shot

        Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

        Pro

        3D Solar System Studio

        Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.