Skip to content

Editor: Do not apply Block Hooks when generating post excerpts. - #13524

Open
MarcinDudekDev wants to merge 3 commits into
WordPress:trunkfrom
MarcinDudekDev:trac/66110
Open

MarcinDudekDev wants to merge 3 commits into
WordPress:trunkfrom
MarcinDudekDev:trac/66110

Conversation

@MarcinDudekDev

@MarcinDudekDev MarcinDudekDev commented Sep 14, 2026 •

Copy link
Copy Markdown

What / why

Since r59523 (Core-61074, WP 6.8), the_content carries a Block Hooks callback at priority 8 —
apply_block_hooks_to_content, swapped for apply_block_hooks_to_content_from_post_object
in r59838 (Core-61074, Core-62716). wp_trim_excerpt() already unhooks do_blocks (r56560 / Core-58682
and wp_filter_content_tags (r55850 / Core-56588) around its inner apply_filters( 'the_content' )
call, but the priority-8 callback was never unhooked. Three consequences:

  • Hooked-block markup leaks into excerpts as plain text. The callback wraps content in a
    temporary core/post-content wrapper block so first_child/last_child hooked blocks have
    an anchor; the algorithm serializes them into the content, do_blocks is unhooked so they can
    never render, and wp_strip_all_tags() strips the `` delimiters — leaving the markup a
    hooked block carries via the hooked_block filter as literal excerpt text.
    Reproduced: excerpt goes from `'Visible text.'` to `'Visible text. LEAKED HOOKED TEXT'`.
  • Guaranteed wasted work — a parse_blocks() traversal, get_hooked_blocks(), the
    hooked_block_types/hooked_block filters, and a _wp_ignored_hooked_blocks meta read on
    every generated excerpt, for output that can never be visible.
  • Wrong post context — the callback resolves context via get_post() (current loop post),
    not the post passed to wp_trim_excerpt(), so off-loop excerpts read the wrong post's
    _wp_ignored_hooked_blocks suppression.

The change

Unhook and conditionally restore apply_block_hooks_to_content_from_post_object (priority 8)
inside wp_trim_excerpt(), mirroring the existing do_blocks/wp_filter_content_tags dance and
the identical unhook core already performs in insert_hooked_blocks_into_rest_response()
(blocks.php:1559-1573). A site that deliberately unhooked the callback is left unchanged.

Testing

  • Six new PHPUnit tests in tests/phpunit/tests/formatting/wpTrimExcerpt.php: callback unhooked
    during the inner the_content run; restored afterwards; not resurrected when previously
    unhooked; Block Hooks algorithm does not run (spied via hooked_block_types, which only fires
    inside the algorithm); hooked-block markup does not leak into the excerpt; Block Hooks still
    apply to a subsequent real the_content run.
  • Negative control: the same reproduction on the code path without the fix yields the leaked
    excerpt 'Visible text. LEAKED HOOKED TEXT', confirming the bug on trunk.
  • 5283 tests pass across the formatting / post / query / REST-posts / feed groups (sequential
    sweep); PHPCS clean.

Trac ticket: https://core.trac.wordpress.org/ticket/66110

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code (Anthropic), Devin (Cognition)
Model(s): Claude Opus 4.8 (orchestration/review), Devin (implementation)
Used for: root-cause analysis, patch + test authorship, git fact-checking, and full test sweep; reviewed and taken responsibility for by the contributor.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

🤖 Generated with Claude Code

wp_trim_excerpt() builds the excerpt from already-rendered content: it runs
excerpt_remove_blocks() and temporarily unhooks do_blocks() and
wp_filter_content_tags() so the excerpt is not processed a second time. The
Block Hooks callback apply_block_hooks_to_content_from_post_object(), added to
'the_content' at priority 8 in [59523], was never unhooked there and still runs
during excerpt generation.

The callback wraps the content in a temporary core/post-content wrapper block,
which anchors first/last-child hooked blocks even when no block delimiters
remain. Those blocks can never render, because do_blocks() is unhooked, and any
markup they carry via the 'hooked_block' filter leaks into the excerpt text as
plain text. The callback also resolves its context from the current loop post
rather than the post being excerpted.

Unhook and restore it alongside the existing do_blocks()/wp_filter_content_tags()
dance, matching the same unhook already performed in
insert_hooked_blocks_into_rest_response().

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props myththrazz, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me. However, I will request a review from @ockham since he introduced this function to begin with.

@westonruter
westonruter requested a review from ockham September 15, 2026 19:09
* would also resolve its context from the current loop post via
* get_post() rather than the post being excerpted.
*/
$filter_block_hooks_removed = remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', 8 );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a theme or plugin has changed the priority of this hook, it won't be removed. In the Trac ticket under the Precedent heading, it calls out how it's done in insert_hooked_blocks_into_rest_response(), where it gets the priority from has_filter and then uses it when calling remove_filter(). Like this:

	$priority = has_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object' );
	if ( false !== $priority ) {
		remove_filter( 'the_content', 'apply_block_hooks_to_content_from_post_object', $priority );
	}

That seems more robust than hardcoding it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other such checks being done for other filters in this function. If this is done, it should be done for all of them.

I agree that it would be ideal to not have to hare-code the priority twice.

)
);

register_block_type(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these three new test methods do the same block registration and removal, thoughts on moving these calls to set_up() and tear_down()?

Comment on lines 4079 to 4083
/*
* Only restore the filter callback if it was removed above. The logic
* to unhook and restore only applies on the default priority of 10,
* which is generally used for the filter callback in WordPress core.
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit, but the priority 10 in the comment became inaccurate at some point, and I wonder if it could be generalized and moved above the new block on line 4075 and cover both. Or we could remove it since the code makes it evident?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Definitely should change that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants