Skip to main content
1 of 2

How to extract 'intro' block from page content, but later strip from page main content

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 with substr_replace instead of preg_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 applying the_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 apply the_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?