I am looking to do the following:-

  1. Parse the blocks on a page and check to see if the block is a core/group block
  2. If it is, check the first innerBlocks is a core/heading (would be useful to achieve this and check a classname if possible)
  3. If it is, take the content of that first inner block, strip all of its tags and add the result to the $headings array for later use
  4. Then, modify the markup of that first block, adding an id attribute made up of the innerHTML, stripped of it's tags. Repurposed in the following way str_replace(' ', '-', $innerHTML)
  5. Once that has been done, create the anchor links from the previously saved $headings array, then render the blocks as normal

1,2,3, and 5 I appear to be able to achieve. Number four seems to be where I am coming unstuck. I guess this is because parse_blocks is happening after the markup has been generated, so at this point I cannot make any further alterations. Does anyone have ideas for how I can achieve the fourth item on the list, adding the anchor to the first block in a group?

Ideally, I want as little input required in the backend. So the user would add a core/group block, then its content. As long as the first item in that group block is a heading, it will automatically use that as it's anchor name and generate a anchor link for it.

The code for the current code is as follows:



      <?php while ( have_posts() ) : the_post(); ?>

        if(!$post) {
          global $post;
        if(!$post) { return ''; }

        $headings = [];      
        $blocks = parse_blocks( $post->post_content );

        if(count($blocks) == 1 && $blocks[0]['blockName'] == null) { // Non-gutenberg
        } else {
          foreach($blocks as $block) {
            if($block["blockName"] == 'core/group') {
              // check the first block in the group block is a heading
              if($block['innerBlocks'][0]['blockName'] == 'core/heading') {
                 * Get the first heading's inner html and remove all tags from it,
                 * then add it to the $headings arr for later use
                 * */ 
                $content_without_tags = wp_strip_all_tags( $block['innerBlocks'][0]["innerHTML"] );
                $headings[] = ['title' => $content_without_tags];

        if(empty($headings)) { // No headings found in post
          return '';

        $toc = '<ol>';
        foreach($headings as $heading) {
          $toc .= '<li><a href="#' . str_replace(" ", "-", $heading['title']) . '">' . $heading['title'] . '</a></li>';
        $toc .= '</ol>';

        echo $toc;


      <?php endwhile; ?>


1 Answer 1


I think you should use filters instead, and here are examples based on your code: (these should be added to your theme functions.php file)

  • Filter 1, to be hooked on the render_block_data hook:

    function my_render_block_data( $parsed_block ) {
        // Check whether it's a group block.
        if ( 'core/group' === $parsed_block['blockName'] ) {
            // Check whether the very first block is a heading block.
            if ( isset( $parsed_block['innerBlocks'][0] ) &&
                'core/heading' === $parsed_block['innerBlocks'][0]['blockName']
            ) {
                $block   = $parsed_block['innerBlocks'][0];
                $content = $block['innerContent'][0];
                $id      = sanitize_title( $content );
                global $toc_headings;
                $toc_headings = is_array( $toc_headings ) ? $toc_headings : array();
                // Add the heading to our global headings array, in the form of
                // `$toc_headings['<HTML id>'] = '<heading text>'`. But it's up to
                // you if you want to use another format.
                $toc_headings[ $id ] = wp_strip_all_tags( $content );
                // Add an `id` attribute to the heading tag.
                $new_content = preg_replace(
                    '/<h(\d)( |>)/',
                    '<h$1 id="toc-' . esc_attr( $id ) . '"$2',
                // Alternatively, you can add an anchor, e.g.
                //$anchor      = sprintf( '<a id="toc-%s"></a>', esc_attr( $id ) );
                //$new_content = $anchor . $content;
                // Modify the block's content.
                $parsed_block['innerBlocks'][0]['innerContent'][0] = $new_content;
        return $parsed_block;
  • Filter 2, to be hooked on the the_content hook:

    function my_add_toc_headings( $content ) {
        global $toc_headings;
        if ( is_array( $toc_headings ) ) {
            $toc = '<ol>';
            foreach ( $toc_headings as $id => $text ) {
                $toc .= sprintf(
                    '<li><a href="#toc-%s">%s</a></li>',
                    esc_attr( $id ),
                    esc_html( $text )
            $toc .= '</ol>';
            // Add the TOC at the top.
            $content = "$toc $content";
            // Reset the list.
            $toc_headings = array();
        return $content;

So with that, your while loop would be as simple as:

while ( have_posts() ) : the_post();
    add_filter( 'render_block_data', 'my_render_block_data' );
    add_filter( 'the_content', 'my_add_toc_headings' );


    remove_filter( 'render_block_data', 'my_render_block_data' );
    remove_filter( 'the_content', 'my_add_toc_headings' );

Note: I conditionally added the filters (or added them in the above way) so that the TOC stuff is only added for the above the_content() call. So for example, the REST API output would be left untouched.

Also, the above snippets were tried & tested working with WordPress v6.1.1 and the Twenty Twenty-Three theme.

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.