Skip to content

Built-in validation fixes can silently drop an anchor from blocks that render it on a nested element #83603

Closed

Description

@hbhalodia

Description

Followup from the PR comment here -- #82836 (review)

applyBuiltInValidationFixes() removes an anchor or ariaLabel the block's markup no longer carries. That is right for a hand-edited block, but the helper also runs from the parser and the deprecation path, where nobody edited any markup, so a block that went invalid for an unrelated reason can lose its anchor.

The loss is silent: removing the anchor also removes the id from the save output, so the block usually validates afterwards and renders with no warning.

Introduced in #82836, which added the removal branch to fixGlobalAttribute.

Scope

Core blocks are not affected. Each one with anchor support renders the id on the first top-level element of its save output, so getHTMLRootElement() finds it, and blocks whose save output is inner blocks only are skipped by the existing innerHTML.trim() guard.

Affected are third-party apiVersion 2 blocks that spread useBlockProps.save() onto a nested element — the id is in the markup but invisible to a root-only check - and old deprecations of the same shape.

The exposure is narrower than it looks: serializeBlock() returns serializeRawBlock( block.__unstableBlockSource ) for an invalid block, so one that stays invalid keeps its original source on save. The loss only reaches stored content when removing the anchor is what makes the block validate.

Step-by-step reproduction instructions

No core block has this shape, so the repro registers one from the browser console. Nothing needs building or installing.

  1. Open a new post in the editor, then open the browser console and paste:
const el = wp.element.createElement;
const { useBlockProps } = wp.blockEditor;

wp.blocks.registerBlockType( 'test/nested-anchor', {
	apiVersion: 2,
	title: 'Nested anchor test',
	category: 'text',
	supports: { anchor: true },
	edit: () => el( 'div', useBlockProps(), 'Content' ),
	save: () => el( 'div', null, el( 'p', useBlockProps.save(), 'Content' ) ),
} );
  1. Open the code editor (Ctrl/Cmd + Shift + Alt + M) and paste this block. The id is present but sits on the inner <p>, and the text differs from the save output so the block is invalid for a reason unrelated to the anchor:
<!-- wp:test/nested-anchor {"anchor":"keep-me"} -->
<div><p class="wp-block-test-nested-anchor" id="keep-me">Content changed</p></div>
<!-- /wp:test/nested-anchor -->
  1. Switch back to the visual editor. The block shows the usual invalid-content warning.
  2. In the console, read the parsed block:
wp.data.select( 'core/block-editor' ).getBlocks()
	.map( ( b ) => ( { isValid: b.isValid, anchor: b.attributes.anchor } ) )

Expected

anchor: 'keep-me'. The markup still carries id="keep-me", just not on the root element.

Actual

anchor: undefined. The anchor has been removed although nobody edited any markup. Clicking Attempt Block Recovery then writes the block out without the id, making the loss permanent.

Possible fix

Before deleting, check whether the value the block holds still appears as that attribute on any element in the markup, not only on the root:

if (
	innerHTML.trim() &&
	! isValueInMarkup(
		innerHTML,
		attributeSchema.attribute,
		modifiedBlockAttributes[ supportKey ]
	)
) {
	delete modifiedBlockAttributes[ supportKey ];
}

Scoping the removal to hand-edited markup instead is not viable: the parser cannot tell a code editor edit from stored post content, since both arrive through the same path, so that would break deleting an id in the code editor — one of the flows #82836 fixes.

Related

Screenshots, screen recording, code snippet

No response

Environment info

Please confirm that you have searched existing issues in the repo.

  • Yes

Please confirm that you have tested with all plugins deactivated except Gutenberg.

  • Yes

Please confirm which theme type you used for testing.

  • Block
  • Classic
  • Hybrid (e.g. classic with theme.json)
  • Not sure

Activity

  1. added
    [Type] BugAn existing feature does not function as intended
    [Feature] Block APIAPI that allows to express the block paradigm.
    on Sep 28, 2026
  2. drzraf commented on Oct 3, 2026

    @drzraf

    Not specific to anchor but clearest framing of this problem among the bugtracker:

    The loss is silent … the block usually validates afterwards and renders with no warning.

    serializeBlock() returns serializeRawBlock( block.__unstableBlockSource )
    for an invalid block, so one that stays invalid keeps its original source on
    save. The loss only reaches stored content when removing the anchor is what
    makes the block validate.

    The same holds for a third-party attribute stored in the block comment delimiter that no schema declares: it is dropped at parse time, the block is valid before and after, and the loss reaches stored content on the next save.

    validity is what removes the protection.

    Reported with reproduction under #38922, since that issue already states the "preserve what you can't recognise" principle and names re-generation from working attributes as the mechanism.

    Flagging it here only so the two are not fixed in isolation: if #83605 ends up narrowing when applyBuiltInValidationFixes() may discard data, it may be worth checking whether the resulting rule also wants to cover "the parser found a delimiter key nothing declares", which is the same question asked about a different field.

    /Opus 5

  3. hbhalodia commented on Oct 22, 2026

    @hbhalodia
    ContributorAuthor
    • Closing it as convered in PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

[Feature] Block APIAPI that allows to express the block paradigm.[Status] In ProgressTracking issues with work in progress[Type] BugAn existing feature does not function as intended

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions