Display specific Gutenberg blocks of a post outside of the post content in the theme

Sometimes it could be useful to display parts of a post or page on another place on the site. For example, a slider above the title of the post/page. Here I show you how to loop through the blocks of a post block per block in a theme to get this done.

Update from January 9, 2020: As Jeslen pointed out in the comments, the wpautop filter should be removed from running on our content markup. Otherwise, we might get unexpected paragraph elements in our output. I updated the code accordingly.

Usually, a theme uses the_content() to display the post content. More helpful for us is get_the_content(), to get the content of a post or page including the block comments of Gutenberg as it is stored in the database.

Since WordPress 5.0, core runs the do_blocks function on this content, that looks like that in 5.0.3:

/** * Parses dynamic blocks out of `post_content` and re-renders them. * * @since 5.0.0 * @global WP_Post $post The post to edit. * * @param string $content Post content. * @return string Updated post content. */ function do_blocks( $content ) { // If there are blocks in this content, we shouldn't run wpautop() on it later. $priority = has_filter( 'the_content', 'wpautop' ); if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) { remove_filter( 'the_content', 'wpautop', $priority ); add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); } $blocks = parse_blocks( $content ); $output = ''; foreach ( $blocks as $block ) { $output .= render_block( $block ); } return $output; }
Code language: PHP (php)

You can see that a parse_blocks() function is called, that returns the blocks of the content so that they can be accessed one by one in a loop. And this is the part we need.

Get the content blocks as an array

We can use the following line of code to get the blocks of a post as an array:

$blocks = parse_blocks( get_the_content() );
Code language: PHP (php)

This is what an array entry of a paragraph block looks like:

2 => array (size=5) 'blockName' => string 'core/paragraph' (length=14) 'attrs' => array (size=0) empty 'innerBlocks' => array (size=0) empty 'innerHTML' => string '<p>Ein Absatz</p>' (length=19) 'innerContent' => array (size=1) 0 => string '<p>Ein Absatz</p>' (length=19)
Code language: PHP (php)

The entry contains various information about the block, like the name and its content.

Display the slider block separate from the content

To display a slider from the content above the post title, we can do something like this:

// Get post content to extract slider shortcode. $blocks = parse_blocks( get_the_content() ); foreach ( $blocks as $block ) { if ( 'soliloquy/soliloquywp' === $block['blockName'] ) { echo do_shortcode( $block['innerHTML'] ); break; } } // Display the title.
Code language: PHP (php)

The project where I had to create that behavior for uses the Soliloquy slider, which comes with a Gutenberg block (in the end it stores a shortcode in the database). The block has the block name soliloquy/soliloquywp, and that is the value we check for in the foreach loop.

After finding it, we display the Shortcode from $block['innerHTML'] and end the loop. Then we can display the title.

Displaying the rest of the content

Now we need to display the remaining content:

$content_markup = ''; foreach ( $blocks as $block ) { if ( 'soliloquy/soliloquywp' === $block['blockName'] ) { continue; } else { $content_markup .= render_block( $block ); } } // Remove wpautop filter so we do not get paragraphs for two line breaks in the content. $priority = has_filter( 'the_content', 'wpautop' ); if ( false !== $priority ) { remove_filter( 'the_content', 'wpautop', $priority ); } echo apply_filters( 'the_content', $content_markup ); if ( false !== $priority ) { add_filter( 'the_content', 'wpautop', $priority ); }
Code language: PHP (php)

We loop through the $blocks array again checking for the slider block. But this time to ignore that block in the output. For all other blocks, we add the return value of the render_block( $block ) call to a variable. The function returns the HTML string for a given block.

To correctly display things like embeds and shortcodes, we run all the the_content filters above $content_markup before displaying it.

This solution has the potential for optimization, so we could remove the core filters for block rendering to not run them twice. But if you — like me — want to use this special behavior of the content only in a few places and in all other cases simply call the_content(), you would need to modify those places, too.

If you do not have an enormous amount of blocks in your posts, the performance loss due to the double execution of the block parser logic probably will not be of much importance.

36 reactions on »Display specific Gutenberg blocks of a post outside of the post content in the theme«

  1. it is a very interesting article. However, a problem remains: it does not work for PHP views (dynamic blocks)...

      1. $blocks = parse_blocks( get_the_content() );
        foreach ( $blocks as $block ) {
        if ( 'test/test' === $block['blockName'] ) {
        echo do_shortcode( $block['innerHTML'] );
        break;
        }
        }

        the test / test block is not displayed.

        print_r($blocks) :
        [...]
        Array ( [blockName] => test/test [attrs] => Array ( ) [innerBlocks] => Array ( ) [innerHTML] => [innerContent] => Array ( ) ) )

  2. In fact it works with:

    $content_markup = '';
    $blocks = parse_blocks( get_the_content() );
    foreach ( $blocks as $block ) {
    if ( 'test/test' !== $block['blockName'] ) {
    continue;
    } else {
    $content_markup .= render_block( $block );
    }
    }

    echo apply_filters( 'the_content', $content_markup );

    1. Hey Ceylan,

      great to hear that you figured it out! Yes, the code in the article is meant for a block that stores a shortcode in the database, so it cannot be used exactly like that for other blocks.

      Best,
      Florian

  3. Just a note, you may want to add the remove_filter/add_filter stuff for wpautop.

    In the example above, this is located in the first few lines of the do_blocks function. In the blocks.php core file that houses the do_blocks function, this is located at the bottom before the return statement.

    You might get some random tags thrown into your output without it.

  4. Great! I could split text from "core/gallery" block, that are now separated.
    The only issue is that lighbox plugins that could integrate with the Gutenberg Gallery don't work when I apply this.
    I have tried with Simple Lightbox and Advanced Gutenberg (they both enable lighboxes for Gutenberg gallery block).
    Any ideas how to get them to work, together with this fix?

      1. Hi Florian,
        I'm afraid I can't post links in here, maybe due to Akismet or some other spam filters, it's the 4th time I try to reply! 🙂
        Can I write you by mail or Fb?

  5. Hi Florian,

    since I am not very familiar with programming, I used the code exactly as you showed here.
    The only differences are:

    1) block name "core/gallery" instead of "soliloquy/soliloquywp"
    2) "Displaying the rest of the content" code is being printed BEFORE the "Display the slider block separate from the content" code.
    3) $blocks = parse_blocks( get_the_content() ); is declared before "Displaying the rest of content"

    You can see here the result in my WIP website:
    https://www.creativenergy.it/portfolio/sports-mania/

    When I print with , Simple Lightbox plugin could apply the lightbox to the Gutenberg generated gallery. After that I print the filtered content, and then the splitted core/gallery. In this case the Simple Lightbox plugin can't apply the lightbox to it.

    Here is the php for this template:

    https://gist.github.com/Ksasa78/005e2aa5852bf7d29acb72f0cf02b33d

    Hope you can figure what's going on!

      1. Grazie Florian!!! Now it works like a charm 🙂

        So you added:

        if ( function_exists('slb_activate') ) {
        $content = slb_activate($content);
        }

        right before printing the content, like in a manual plugin activation, I see.
        I will send soon a »tangible« thank you, since the whole explanation was really helpful with what I wanted to achieve with this template.

        1. Hi Salvatore,

          happy to hear that it works! 🙂

          Yes, that chunk of code is the important part to make the lightbox plugin work with markup that was generated by own functions.

          » I will send soon a »tangible« thank you, since the whole explanation was really helpful with what I wanted to achieve with this template.«

          Thanks a lot, it arrived ❤

          Best,
          Florian

  6. hello, I'm wondering how you use the parse_blocks function if the block you are looking for is an inner block ? Indeed with gutenberg it is now very easy to use columns for the layout and then your blocks are inner blocks. Problem is that if the above mentioned function does't see them if you do for example
    ´´´
    $blocks = parse_blocks( get_the_content() );
    foreach ( $blocks as $block ) {
    if ( 'test/test' !== $block['blockName'] ) {
    continue;
    }
    ´´´
    Or am I totally wrong ?

    1. Hi Fab,

      no, you are right. You would need to check for each block if it has inner blocks and also loop over them (and check if the inner blocks contain inner blocks, again).

      Best,
      Florian

  7. Thank you very much for such an elegant solution. My specific case is: I need my gallery block to appear as separate element adjacent to .entry-content so to have a more elegant way to position it outside of the flow of elements that make the article. I either show a featured image there, or if there is a gallery I place the gallery instead.

    I solved it like this:

    if ( ! has_block( 'gallery' ) ) :
    get_template_part entry_thumbnail ...
    else:
    get_template_part entry_gallery ...

    and there simply this:

    $blocks = parse_blocks( get_the_content() );
    foreach ( $blocks as $block ) {
    	if ( 'core/gallery' === $block['blockName'] ) {
    		echo do_shortcode( $block['innerHTML'] );
    		break;
    	}
    }

    And finally in entry_content template part:

    $blocks = parse_blocks( get_the_content() );
    $content_markup = '';
    foreach ( $blocks as $block ) {
    	if ( 'core/gallery' === $block['blockName'] ) {
    		continue;
    	} else {
    		$content_markup .= render_block( $block );
    	}
    }
    
    $priority = has_filter( 'the_content', 'wpautop' );
    if ( false !== $priority ) {
    	remove_filter( 'the_content', 'wpautop', $priority );
    }
    
    echo wp_kses_post( apply_filters( 'the_content', $content_markup ) );
    
    if ( false !== $priority ) {
    	add_filter( 'the_content', 'wpautop', $priority );
    }

    Result is exactly what I need, if there is a gallery, show it outside post content, and DO NOT show it also in the post content. Awsome, thank you a LOT for this.

  8. Hello,
    I now realised my example is not without problems. If you could please help ... that would be great.

    Blocks that have images (image block, social icons ... or are in figure element, video embeds etc.) Do NOT display correctly. Image block get repeated twice. Video embed (I tried youtube) and social icons do not display images. So I get empty containers.

    Normal text blocks, headings, paragraphs, blockquotes and dinamic ones, recent posts for exmp .. work great.

    I taught $blocks ... contains all gutenberg blocks other then gallery?

    What am I missing?
    Thanks so much.

  9. Oh, its the wp_kses_post() that does this. 🙁

    Hmmm ...
    for now I rewrote it like this:
    echo apply_filters( 'the_content', wp_kses_post( $content_markup ) );
    Which I think is correct escaping for this situation, but phpcs still reports it as unescaped :/

    Anyhow ... thanks again.

  10. Hey Florian, thank you so much for this article, however here's the problem:

    if ( function_exists('slb_activate'); ) {
    $content = slb_activate($content);
    }

    It shows me somehow there is an error but I don't know where?
    Thanks
    Igor

    1. Hi Igor,

      you need to remove the semicolon in the if statement, it has to be if ( function_exists('slb_activate') ) {.

      The rest looks correct.

      Best,
      Florian

  11. Hi Florian, thank you for your article! I am trying to have the fist block, which is an core/image block, to be excluded from the_content. With your code it works, yet it also excludes all the other image blocks. Do you have an idea how to target only the first one? Thank you in advance!

    1. Hi Donald,

      adjusting the code like this should work:

      $img_block_counter = 0;
      foreach ( $blocks as $block ) {
      	if ( 'core/image' === $block['blockName'] && $img_block_counter === 0 ) {
      		$img_block_counter++;
      		continue;
      	} else {
      		$content_markup .= render_block( $block );
      	}
      }

      Hope that helps!
      Florian

      1. Hi Florian, thank you so much for your quick reply! It works!

        I adjusted the code a bit more so that the image also needs to be the first block of the post content:

        $img_block_counter = 0;

        foreach ( $blocks as $block ) {
        if ( $blocks[0]['blockName'] === 'core/image' ) {
        if ( $block['blockName'] === 'core/image' && $img_block_counter === 0) {
        $img_block_counter++;
        continue;
        } else {
        $content_markup .= render_block( $block );
        }
        } else {
        $content_markup .= render_block( $block );
        }
        }

  12. Hi Florian,

    Thanks for this, I can't seem to get it to work though.

    On my page.php template I have the following code:

    ---------

    ID ); ?>

    ---------

    The code within the the seems to be doing what it should (not displaying the page header image) but the 'foreach' code after the '$blocks = parse_blocks( get_the_content() );' - in the section - doesn't seem to work? Just need the 'acf/page-header-image' to appear outside the post content area.

    I don't know PHP that well so not sure how to make this work, any help would be much appreciated, thank you.

    1. Hi Simon,

      I guess you posted more code, but it got stripped out by the comment text escaping. It should work if you modify the code like this:

      $blocks = parse_blocks( get_the_content() );
      foreach ( $blocks as $block ) {
      	if ( 'acf/page-header-image' === $block['blockName'] ) {
      		echo render_block( $block );
      		break;
      	}
      }

      Hope that helps!

      Best,
      Florian

Leave a Reply

Your email address will not be published. Required fields are marked *