I am trying to find a way to strip a block from the page content, but first extract it to be able to render it earlier on the page.
Instead of having the intro section block inside the <div>
with .entry-content
, the goal is to end up with an HTML structure like below (cleaned up for illustrative purposes):
<article class="page">
<header>
<h2 class="entry-title">Page title</h2>
<div class="entry-intro">
<div class="wp-block-group alignwide is-style-intro-section is-layout-constrained">
... intro section block content ...
</div>
</div>
</header>
<div class="entry-content">
... page content (all other blocks, without intro section block) ...
</div>
</article>
Several points of attention:
- Page content must be rendered as normal, only stripping out the 'intro section' block. So if possible, I would prefer not to write a replacement function for
the_content()
to prevent issues future WordPress versions in case block parsing changes. - The group block with the 'intro section' style can be nested inside another group block (or further down).
- The stripping of the intro section block is done on every page request, so for performan at the moment I am using
strpos
withsubstr_replace
instead ofpreg_replace()
(see below).
Attempts so far:
To be able to let users mark a group block as 'intro section', I have defined a custom block style:
register_block_style(
'core/group',
array(
'name' => 'intro-section',
'label' => __( 'Intro section', 'example' ),
)
);
Inspired by Get blocks from other pages, from within current page and Filter Gutenberg Blocks Content I have used parse_blocks()
to find the 'intro section' block recursively:
/**
* Walker function to find intro section block recursively.
*/
function _example_find_intro_block( $blocks, &$match = null ) {
foreach ( $blocks as $block ) {
if ( 'core/group' === $block['blockName'] &&
! empty( $block['attrs']['className'] ) &&
preg_match( '/\bis-style-intro-section\b/', $block['attrs']['className'] ) ) {
if ( isset( $match ) ) {
$match = $block;
} else {
continue;
}
} elseif ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
_example_find_intro_block( $block['innerBlocks'], $match );
}
}
}
Function to retrieve the intro block separately:
/**
* Retrieve first block marked as intro section from page content.
*/
function example_get_the_page_intro_block( $render = true ) {
if ( ! is_page() || ! has_block( 'core/group' ) ) {
return false;
}
$blocks = parse_blocks( get_the_content() );
$intro_block = '';
_example_find_intro_block( $blocks, $intro_block );
if ( $render && ! empty( $intro_block ) ) {
return render_block( $intro_block );
} else {
return $intro_block;
}
}
Function to output intro block:
<?php
/**
* Display page intro section.
*/
function example_the_page_intro() {
if ( ! is_page() || ! has_block( 'core/group' ) ) {
return;
}
$intro = example_get_the_page_intro_block();
if ( $intro ) :
?>
<div class="entry-intro">
<?php echo apply_filters( 'the_content', $intro ); ?>
</div>
<?php
endif;
}
?>
Function to filter page content:
/**
* Display page content, but strip intro section block from it.
*
* Ensures `the_content` filter is performed AFTER having replaced page intro section block.
*/
function example_filter_the_page_content( $content ) {
$the_content = apply_filters( 'the_content', $content );
if ( is_page() && is_singular() && in_the_loop() && is_main_query() ) :
$intro_block = apply_filters( 'the_content', example_get_the_page_intro_block() );
if ( $intro_block ) {
$pos = strpos( $the_content, $intro_block );
if ( $pos !== false ) {
// Only replace first occurrence
$the_content = substr_replace( $the_content, '', $pos, strlen( $intro_block ) );
}
}
endif;
return $the_content;
}
add_filter( 'example_the_content', 'example_filter_the_page_content' );
And lastly, the page content template file:
<?php
/**
* Template part for displaying posts
*/
?>
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
<header class="entry-header container">
<h2 class="entry-title"><?php the_title(); ?></h2>
<?php example_the_page_intro(); ?>
</header>
<div class="entry-content">
<div class="container">
<?php
echo apply_filters( 'example_the_content', get_the_content() );
?>
</div>
</div><!-- .entry-content -->
</article><!-- #post-<?php the_ID(); ?> -->
Considerations:
- I have managed to get things working by filtering
the_content
directly with a priority of 9. However, this prevents me from applyingthe_content
to the intro section block separately. - So I have created a separate
example_the_content
filter that is only applied when outputting the page content. This way I can applythe_content
both to the intro block and page content separately.
The issue is that with my current solution strpos()
does not correctly match the block output: apparently apply_filters( 'the_content', render_block( $intro_block ) )
removes excess whitespace compared to apply_filters( 'the_content', get_the_content() )
.
I would prefer to be able to strip the matched intro section block by ID when outputting the page content. Is there a way to do this?
How can I make sure the intro section block is always found first AND stripped later from the page content?