Dynamic Table of Contents

Geoff Graham on
function csstricks_toc($post=false) {
  global $post;
  $blocks = parse_blocks( $post->post_content );
  $headings = array();

  foreach( $blocks as $block ) {
    if( 'core/heading' === $block['blockName'] ) {
      if( false !== strpos( $block['innerHTML'], 'id=' ) ) {
        $element = 'h' . $block['attrs']['level'];
        $title = wp_strip_all_tags( $block['innerHTML'] );

      } else {
        $title = wp_strip_all_tags( $block['innerHTML'] );
      }
      
      $headings[] = $title;
    }
  }
  
  if( !empty( $headings ) ) {
    echo '<ol class="toc">';
    foreach( $headings as $heading )
    echo '<li class="toc-link"><a href="#aa-' . sanitize_title_with_dashes($heading) . '">' . $heading . '</a></li>';
    echo '</ol>';
  }
}

The crux of this comes from The SEO Guidebox. The original snippet parses all of the blocks in the post content, looks for a heading level, strips its HTML, and then spits out the heading text in an ordered list.

I really wanted the outputted text to be anchored to the post headings. We already use a plugin here on CSS-Tricks that dynamically inserts ID on headings, so I didn’t need that part and ripped it out. Then I added an anchor element to the list items that uses the sanitize_title_with_dashes() function convert the $heading into a lowercase string separated with dashes.

The only thing is that the plugin we use to insert IDs prepends aa- to each ID it generates. So, I slipped that into the loop and now we have a table of contents component on this site. We took it a little bit further by using an ACF field that lets us insert the table of contents conditionally.

Advanced Custom Fields plugin screen for creating a radio button with Yes and No options.