1

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 page content.
 */
?>
<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?

3
  • note that there are filters that WP calls when rendering a block that allow you to override it, you don't need to mess around with regex parsing or the_content filter when you can run a test to see if the block should show and do return "" if it shouldn't
    – Tom J Nowell
    Commented Apr 3, 2023 at 16:07
  • @TomJNowell , appreciate the note. I have actually tried using the render_block filter. But how can I differentiate between rendering it for the intro section, and skipping it for the page content?
    – Visnetje
    Commented Apr 3, 2023 at 17:34
  • @TomJNowell , I managed to fix the issue by applying the render_block filter after having rendered the intro block, and right before calling the_content(). I will post the solution below, perhaps it can help others, as well. Thanks again for suggesting to drop the_content in favor of the render_block filter.
    – Visnetje
    Commented Apr 3, 2023 at 19:34

1 Answer 1

1

For anyone interested, with the suggestion from @TomJNowell I managed to retry using the render_block filter and simplify the solution to the problem:

Callback function (replacing example_filter_the_page_content() from my initial post):

/**
 * Callback function used to skip intro section block from rendering.
 *
 * To be used as callback for the `render_block` filter.
 */
function example_filter_render_block_intro( $block_content, $block ) {
    if ( is_page() && is_singular() && in_the_loop() && is_main_query() ) {
        if ( 'core/group' === $block['blockName'] && 
            ! empty( $block['attrs']['className'] ) && 
              preg_match( '/\bis-style-intro-section\b/', $block['attrs']['className'] )  ) {
            $block_content = '';
        }
    }

    return $block_content;
}

And then calling add_filter after rendering the intro, right before calling the the_content() function to render the page content:

<?php
/**
 * Template part for displaying page content.
 */
?>
<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

            add_filter( 'render_block', 'example_filter_render_block_intro', 10, 2 );
            the_content();

            ?>
        </div>
    </div><!-- .entry-content -->
</article><!-- #post-<?php the_ID(); ?> -->

This way, the render_block filter has already done its work for the intro section block, and consequently strips the intro section block right before rendering the page content.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.