Your archive card shows a Latest Posts list where the excerpt should be. Or a Query Loop. Or a related-posts grid. The post has no manual excerpt, so WordPress generated one, and somewhere in that generation a dynamic block’s render_callback ran and its output landed in the card.
This is not a bug in excerpt_remove_blocks(). It is the expected result of render_block() executing a dynamic block during excerpt generation. The allowlist is the control surface, and the fix belongs there.
Symptom
An archive template calls the_excerpt(). The post has an empty post_excerpt. The card displays markup that belongs to a dynamic block: a list of recent posts, a query loop, a server-rendered widget. The post’s own prose is missing or truncated around it.
Two conditions produce this:
- The dynamic block’s name is in the allowed blocks list that
excerpt_remove_blocks()uses. - The post has no manual excerpt, so
wp_trim_excerpt()generates one from the content.
If the post has a manual excerpt, get_the_excerpt() returns it and no block parsing happens. That is the cheapest diagnostic: set a manual excerpt on the post and reload. If the leak disappears, you are on the auto-generation path.
Trace
get_the_excerpt() returns the post’s post_excerpt value after applying the get_the_excerpt filter. The function source is short:
return apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
When post_excerpt is empty, wp_trim_excerpt() generates one from the content. That function applies the_content filters, and do_blocks() is attached to the_content. do_blocks() parses dynamic blocks out of post_content and re-renders them.
But the excerpt path does not simply call do_blocks() on the raw content. It calls excerpt_remove_blocks(), which parses blocks out of the content string and renders those appropriate for the excerpt. The function’s own description: “As the excerpt should be a small string of text relevant to the full post content, this function renders the blocks that are most likely to contain such text.”
Inside excerpt_remove_blocks(), the loop checks each parsed block’s blockName against an allowed list. If the name is in the list, the function calls render_block( $block ) and appends the result to the output.
render_block() constructs a WP_Block from the parsed block and returns the result of its render() method:
$block = new WP_Block( $parsed_block, $context );
return $block->render();
For a dynamic block, render() invokes the block’s render_callback. That callback runs during excerpt generation. Its output is now part of the excerpt string.
Root cause
The allowed blocks list is filterable through excerpt_allowed_blocks:
$allowed_blocks = apply_filters( 'excerpt_allowed_blocks', $allowed_blocks );
The default list includes inner blocks such as core/paragraph, core/heading, core/list, core/quote, and wrapper blocks such as core/columns, core/column, and core/group. The retrieved source lists these defaults but does not enumerate every dynamic block that a plugin or theme may add.
A dynamic block enters the excerpt in one of two ways:
- Its name is in the default allowed list.
- A site added it through
excerpt_allowed_blocks.
The second is the common case for custom blocks. A developer wants a block’s text to appear in excerpts, adds the block name to the filter, and does not account for the fact that render_block() will execute the block’s render_callback during excerpt generation.
There is a documented hazard. The excerpt_remove_blocks() source warns that if a dynamic block is added to the allowed list, it must not generate another excerpt, because that causes an infinite loop. The Gutenberg commit that introduced the stripping approach includes a test comment explaining the mechanism: when dynamic blocks generate an excerpt, they can cause an infinite loop if that block is part of the post’s content. wp_trim_excerpt() applies the_content, which has do_blocks attached, trying to render the block, which again attempts to return an excerpt of that post.
The commit added strip_dynamic_blocks() and filters to strip dynamic blocks from excerpts before the_content is applied. That is a proposed implementation, not necessarily what shipped in core. The current core function is excerpt_remove_blocks(), and its allowlist is the control surface.
Fix
Remove the dynamic block from excerpt_allowed_blocks. If the block must contribute text to an excerpt, render that text through a path that does not call get_the_excerpt() again.
First, find the registrations. Grep the codebase for the filter name:
wp grep excerpt_allowed_blocks --all
Or with standard tools:
grep -rn "excerpt_allowed_blocks" wp-content/
For each callback, check which block names it adds. Compare those names against the dynamic block registry. A block is dynamic if it has a render_callback or a render file in its block.json. You can list registered block types with WP-CLI:
wp block-type list --fields=name,title
That command lists registered block types. To check whether a specific block is dynamic, inspect its registration in code or query the block type registry in a short script. The registry stores the block type object; a dynamic block has a non-null render_callback.
If the block should not contribute to excerpts, remove it from the filter. If you cannot edit the plugin that added it, remove it at a later priority:
add_filter( 'excerpt_allowed_blocks', function( $allowed ) {
return array_diff( $allowed, array( 'my-plugin/latest-posts' ) );
}, 20 );
If the block must contribute text, do not let its render_callback call get_the_excerpt() or wp_trim_excerpt(). Read from a field or attribute that does not re-enter excerpt generation. For example, a block that stores a summary in post meta should read that meta directly:
function my_plugin_summary_block_render( $attributes, $content, $block ) {
$post_id = $block->context['postId'] ?? get_the_ID();
$summary = get_post_meta( $post_id, 'my_plugin_summary', true );
if ( '' === $summary ) {
return '';
}
return '' . esc_html( $summary ) . '
';
}
This callback does not call get_the_excerpt(), so it cannot recurse. It also does not render post-derived content that belongs to another query.
For posts that use dynamic blocks near the top of the content, a manual excerpt is the cheapest reliable fix. get_the_excerpt() returns post_excerpt before any block parsing happens. Set a manual excerpt on those posts and the auto-generation path is bypassed entirely.
Verification
Confirm the leak is gone and the fix did not break the block’s front-end output.
First, check the excerpt directly. In a WP-CLI shell or a temporary script:
wp eval 'echo get_the_excerpt( 123 );'
Replace 123 with the post ID. The output should contain the post’s prose and not the dynamic block’s markup.
Second, check the front-end post. The dynamic block should still render where it belongs. The excerpt filter only affects excerpt generation, not the_content.
Third, if you removed the block from excerpt_allowed_blocks, verify the filter is no longer adding it. You can log the allowed list during a request:
add_filter( 'excerpt_allowed_blocks', function( $allowed ) {
error_log( print_r( $allowed, true ) );
return $allowed;
}, 999 );
Check the debug log for the block name. If it is absent, the removal worked.
Fourth, test the infinite-loop hazard. If you have a dynamic block that generates an excerpt, create a post containing that block, leave the excerpt empty, and request the post. If the request hangs or exhausts memory, the block is re-entering excerpt generation. Remove it from the allowed list.
Audit checklist
- Grep for
excerpt_allowed_blocksin plugins, themes, and mu-plugins. - For each added block name, check whether the block is dynamic (has a
render_callbackorrenderfile). - For each dynamic block in the list, check whether its
render_callbackcallsget_the_excerpt(),wp_trim_excerpt(), orthe_excerpt(). - If it does, remove the block from the list or change the callback to read from a non-recursive source.
- For posts with dynamic blocks near the top, set manual excerpts.
- Verify with
wp eval 'echo get_the_excerpt( $id );'and check the front-end block output.
The parsed block tree your archive cards render is the same tree do_blocks() uses. The difference is which blocks excerpt_remove_blocks() chooses to render. Control that list, and the leak stops.