Create WordPress Table of Contents automatically – without plugin!

Create WordPress Table of Contents automatically – without plugin! Thumbnail
Published on 19. May 2020Last updated on 25. April 2021

With many WordPress blog entries or pages, a table of contents can work wonders and give the visitor a quick overview of the structure of the content.

Advertisement

It is simply too cumbersome to create a new table of contents in every post or to update it when changes occur. Installing a plugin (e.g. Easy Table Of Contents) for such a “little thing” is also not a good solution, as it only slows down the site and – as you will see in a moment – you can easily code it yourself.

If you have little or no programming experience, you can still use this table of contents. This is no problem! If there are any questions left, I can also try to help you with the comments.

This is what our end result will look like:

The styling is a little bit different as I adapted it to my blog design for me.

The complete code is programmed by myself and may be used free of charge for any purpose (also commercial) and may be modified as desired!

1. Generate table of contents (PHP script)

The following code generates the table of contents from the headings <h2> and <h3>.

Advertisement

For experienced programmers: This code must be included in the functions.php of the used theme (if used in the child theme).

For inexperienced programmers: In the backend menu (left margin) there is an item “Appearance”. Navigate there to the submenu item “Theme Editor” and then select the “Theme Functions” (functions.php) on the right side under “Theme Files”. There you can scroll to the very end and after the last line of code insert 2-3 empty lines and then insert and save the PHP code below 1:1.

// filter function to generate the table of content (from webdeasy.de)
function get_table_of_content($content) {
    ob_start();
    preg_match_all("/<h[2,3](?:\sid=\"(.*)\")?(?:.*)?>(.*)<\/h[2,3]>/", $content, $matches);
    $tags = $matches[0];
    $ids = $matches[1];
    $names = $matches[2];
    ?>
    <div class="table-of-contents">
    	<p class="toc-headline"><strong>Table of Contents</strong></p>
    	<span class="toggle-toc">+</span>
	    <ul style="display: none;">
	        <!-- Table of contents by webdeasy.de -->
	        <?php for($i = 0; $i < count($names); $i++) { ?>
	            <?php if(strpos($tags[$i], "h2") === false || strpos($tags[$i], "class=\"nitoc\"") !== false) continue; ?>
	            
	                <li>
	                    <?php if(!empty($ids[$i])) { ?>
	                        <a href="#<?php echo $ids[$i]; ?>"><?php echo $names[$i]; ?></a>
	                    <?php } else { ?>
	                        <?php echo $names[$i]; ?>  
	                    <?php } ?>
	        
	                    <?php if($i !== count($names) && strpos($tags[$i+1], "h3") !== false) { ?>
	                        <ul>
	                            <?php for($j = 0; $j < count($names) - 1; $j++) { ?>
	                                <?php $sub_index = $i + $j; ?>
	                                <?php if($j != 0 && strpos($tags[$sub_index], "h2") !== false) break; ?>
	                                <?php if(strpos($tags[$sub_index], "h3") === false || strpos($tags[$sub_index], "class=\"nitoc\"") !== false) continue; ?>
	                                <li>
	                                    <?php if(!empty($ids[$sub_index])) { ?>
	                                        <a href="#<?php echo $ids[$sub_index]; ?>"><?php echo $names[$sub_index]; ?></a>
	                                    <?php } else { ?>
	                                        <?php echo $names[$sub_index]; ?>  
	                                    <?php } ?>
	                                </li>
	                            <?php } ?>
	                        </ul>
	                    <?php } ?>
	                </li>
	        <?php } ?>
	    </ul>
	</div>
    <?php
    return ob_get_clean();
}

The table of contents is collapsed by default with this code. If you want to have it open by default just remove style="display: none;" in line 12. We program the functionality to expand and collapse in section 3.

You can include the table of contents in your posts in two different ways.

Option 1: If you want to set the position in each post yourself using the placeholder {{TABLE_OF_CONTENTS}}, then add this code as well:

function add_table_of_content($content) {
    return str_replace("<p>{{TABLE_OF_CONTENTS}}</p>", get_table_of_content($content), $content);
}
// add our table of contents filter (from webdeasy.de)
add_filter('the_content', 'add_table_of_content');

Option 2: If you want the table of contents to be inserted automatically after the first paragraph in each post, then add this code as well:

function add_table_of_content($content) {
	if (!is_single()) return $content;

    $paragraphs = explode("</p>", $content);
    $paragraphs_count = count($paragraphs);
    $middle_index= absint(floor($paragraphs_count / 2));
    $new_content = '';

    for ($i = 0; $i < $paragraphs_count; $i++) {
        if ($i === 1) {
        	$new_content .= get_table_of_content($content);
        }

        $new_content .= $paragraphs[$i] . "</p>";
    }
    return $new_content;
}
// add our table of contents filter (from webdeasy.de)
add_filter('the_content', 'add_table_of_content');

If you want to output the table of contents only after the second paragraph, you can simply adjust the number in line 10.

Advertisement

And one more thing about the technical background:

The regular expression for reading the headings is structured as follows:

<h[2,3](?:\sid=\"(.*)\")?(?:.*)?>(.*)<\/h[2,3]>

With this we search for all HTML tags that are an h2 or h3. The part (?:\sid=\"(.)\")?(?:.)? groups us the ID (if available) and also groups all other attributes (if available) that the HTML tag still has.

A little tip: If you want to earn some money on the side with your website or blog, this article will tell you everything you need to know about Google Adsense, one of the largest advertising services on the Internet.

2. Pretty up the table of contents (CSS)

Your table of contents is already generated with the previous code. But it would be output as a normal list <ul>. To make the whole thing look a bit nicer, we also add a few lines of CSS.

For experienced programmers: This code must be inserted into a CSS file of the used theme (if used in the child theme). Often this is the top level style.css in the theme folder by default.

For inexperienced programmers: In the same menu item as with the PHP code, you can simply select “Stylesheet” (style.css) on the right. There you also go back to the end and add the following code after the last line and then save the file.

Advertisement
.table-of-contents {
  padding: 1rem;
  border-left: 3px solid #cecece;
  background-color: #e6e6e6;
  position: relative;
}
.table-of-contents .toc-headline {
    margin-bottom: 0;
    font-size: 110%;
    cursor: pointer;
}
.table-of-contents .toggle-toc {
    position: absolute;
    top: .4rem;
    right: 1rem;
    font-size: 30px;
    cursor: pointer;
    font-weight: 800;
    cursor: pointer;
}
.table-of-contents ul {
    padding-left: 1rem;
}
.table-of-contents li {
  list-style: none;
}

Now the table of contents looks better and is also recognizable as such. Of course, you can adapt everything to your theme and your wishes.

3. Expand and collapse table of contents (JavaScript)

With this code visitors can expand and collapse the table of contents.

document.querySelectorAll('.table-of-contents .toggle-toc, .table-of-contents .toc-headline').forEach(toggler => {
	toggler.addEventListener('click', function() {
		let tocList = document.querySelectorAll('.table-of-contents ul')[0];
		let toggler = document.querySelectorAll('.table-of-contents .toggle-toc')[0];
		if(tocList.style.display == 'none') {
			tocList.style.display = 'block';
			toggler.innerHTML = '-';
		} else {
			tocList.style.display = 'none';
			toggler.innerHTML = '+';
		}
	});
});

For experienced programmers: You can simply insert this into an existing JS file.

For inexperienced programmers: In the theme editor there is usually a js folder with .js files or directly a file with the extension .js. Just select a file and paste the code at the very end.

4. Include directory in individual contributions

If you have chosen option 1, to output the table of contents now on a page, you simply have to insert {{TABLE_OF_CONTENTS}} as a normal paragraph in the backend editor of the respective post.

Insert placeholder in WordPress post
Insert placeholder in WordPress post

To ensure that the individual headings are also linked, they require a so-called HTML anchor. Again, there are two possibilities. Either a script inserts them for you automatically (option 1) or you insert them manually (option 2).

Possibility 1:

Advertisement

The script was recommended to me by a reader (thank you!) and he seems to have got it from this site, thank you.

Note that you put this code before the PHP code from point 1.

/**
 * Automatically add IDs to headings such as <h2></h2>
 */
function auto_id_headings( $content ) {
	$content = preg_replace_callback('/(\<h[1-6](.*?))\>(.*)(<\/h[1-6]>)/i', function( $matches ) {
		if(!stripos($matches[0], 'id=')) {
			$matches[0] = $matches[1] . $matches[2] . ' id="' . sanitize_title( $matches[3] ) . '">' . $matches[3] . $matches[4];
		}
		return $matches[0];
	}, $content);
    return $content;

}
add_filter('the_content', 'auto_id_headings');

Possibility 2:

To insert the anchors manually, click on the headline and insert an anchor under “Advanced”. The name is up to you, but it should fit the headline.

Add WordPress HTML Anchor
Add WordPress HTML Anchor

That’s all! If you look at the page now, the table of contents should be output according to the <h2> and <h3> tags.

Exclude menu items

If you want to exclude one or more menu items (like this headline :)), so that they are not displayed in the table of contents, you can additionally give the respective headline the CSS class nitoc. The script will then ignore them.

Conclusion

You did it! With just a few lines of code you can now always generate your own table of contents and don’t have to resort to ready-made plugins anymore. Or even worse: always write and update the tables of contents yourself. Have fun with it! 🙂

Related Posts
💬 Join the Conversation

51 Comments

  1. Robert Baker says:

    I seem to be getting an error…

    Undefined array key 13

  2. <a href=”#id”> not show. Please help me!

  3. Sathish says:

    how to display this function inside the single.php using PHP code?

  4. Kürşad says:

    Hi,How can i –> “If h2 or h3 empty do not show”     ?Thanks for everythings 🙂

    1. LH says:

      You can add || empty($names[$i]) into the first continue line and || empty($names[$sub_index]) into the second continue line in the get_table_of_content() function 🙂 You know what I mean? 😀

  5. Luca says:

    Hey, is there any way to show the Heading under the fixed navbar? When I click on the link in the TOC the headline is hidden behind the nav bar. Would be great if you have any JS code for that. Thanks a lot!

    1. LH says:

      Of course. I use this JS snippet:

      document.querySelectorAll(‘a[href^=”#”‘).forEach(anchorLink => {

      anchorLink.addEventListener(‘click’, anchorClick);

      });

      function anchorClick(event) {

      event.preventDefault();

      let hrefSplit = event.target.href.split(‘#’);

      let elementID = hrefSplit[1];

      if(elementID == undefined || elementID == ”) return false;

      let element = document.getElementById(elementID);

      location.replace(‘#’ + element.id);

      if (element == null) return false;

      var elementOffset = element.getBoundingClientRect().top + window.pageYOffset – 120

      window.scrollTo({

      top: elementOffset,

      behavior: ‘smooth’

      });

      }


      You can adjust the – 120 value until it fits. Hope this helps 🙂

  6. Yoann says:

    Thanks, it works great!There is a little problem with the prefix increment, I get 4.3 and 4.4 instead of 4.1 and 4.2 because x.1 and x.2 were incremented in title 2.https://i.stack.imgur.com/RjsC1.jpgCan you help me correct this error please?

    1. LH says:

      Oh… thanks for the hint! I will fix it next time I update this post 🙂

  7. Again back here with an issue mate…. table of content is working fine… but in some cases/posts like this one – https://techbehindit. com/how-to/how-to-select-the-right-eyelashes-and-the-right-lash-vendors/,  it dont  include link in the headings. I used the above table of content and headings code same/exact…. kindly check and revert…. 

    1. LH says:

      What exactly is the problem..? 😀

      1. mate, I used another site code for a table of content…. it works fine…but now I am stuck in “adding dynamic schema(JSON LD) in articles”, you must create a tutorial for this, as there is no other tutorial present on the internet.I don’t want to use any plugin to make my site heavy…I know you are the best in it and will surely guide us.

  8. Robert Baker says:

    How do I put the {{ TABLE OF CONTENTS }} in a shortcode?  I want it somewhere else away from the content on the page.

    1. LH says:

      You can use this code:

      function toc_shortcode() {
      return get_table_of_contents(auto_id_headings(get_the_content()));
      }
      add_shortcode('TOC', 'toc_shortcode');

      With this code you can use the [TOC] shortcode

      1. Robert Baker says:

        You rock!  Thank you so much!  Sorry for the double posting.  I didn’t see my post and thought it didn’t go through so I posted again.

        1. LH says:

          No problem dude, I will delete the second comment 🙂

  9. Eduard says:

    I put function get_table_of_content($content) into functions.php file. Also used the first approach to see this in action. ( First approach: have to manually insert shortcode into post ). The second didn’t worked for me, from inspector I saw div and empty <ul> only. Well, at least it worked with shortcode approach. But this is not enough for me. It would be great if you can tell me how I can use it for custom page/post templates?Let’s say I have custom template “My Custom Template” what I can choose from Page attributes when editing my page. How I can use it only there? If I call it out in functions then all the templates get it. But I don’t want it. I did tried to keep function get_table_of_content($content) in functions.php file and then just to use add_filter(‘the_content’, ‘add_table_of_content’); but no luck, it wont appear.Also I do not want to automatically appear after first paragraph. I want to choose location inside template by myself. So I removed that part of code where you filter out first paragraph and basically now I have this, but this for some reason removes my page content and shows only ToC. Please help 🙂 function add_table_of_content($content) {  return get_table_of_content($content);}

    1. LH says:

      Hey, try to add get_table_of_contents(get_the_content()); into your custom template. That should work. Your solution with function add_table_of_content($content) {  return get_table_of_content($content);} won’t work because you replace your content ($content) with the TOC.


      Hope this will help 🙂

  10. Rahat Ali says:

    Doesnt work for me in Genesis

    1. LH says:

      What exaclty doesn’t work?

      1. Rahat Ali says:

        Sorry, its Working fine. I have some question about this Toc. Please suggest me.Is json structure require for seo ?Why its not include above h4 heading ?Is this support hindi language ?I found anchor link for hindi look like this. http://localhost:8000/hello-world/#%e0%a4%b9%e0%a4%95-%e0%a4%a8%e0%a4%b9%e0%a5%80%e0%a4%82-%e0%a4%ae%e0%a4%bf%e0%a4%b2%e0%a5%87%e0%a4%97%e0%a4%be-%e0%a4%9c%e0%a4%a1%e0%a4%bc-%e0%a4%b8%e0%a5%87-%e0%a4%9c%e0%a5%8d%e0%a4%af%e0%a4%be

        1. LH says:

          Hi! I didn’t include h4, because I never needed it before. When I update this post I will add it. And I didn’t test it with hindi language, but it should work…

  11. Dejan says:

    Hi LH,

    Excellent code and thank you very much.

    Really basic question.

    How do you make your table of content collapse and expand on the click event?
    Do you use some javascript or I have missed something reading your post?

    Cheers

    1. LH says:

      Hey, good point! I added this functionality some days ago on my blog. I will add the JS code to this post within the next week. Feel free to check back from time to time 🙂

      Regards,
      Lorenz

      1. Dejan says:

        Hi Lorenz,

        Thanks for the quick reply and I will do so. Looking forward to seeing the JS.

        I have used the following function just running before your functions to add anchors to the headings in the article so I do not need to do that manually and it works like a charm.

        Maybe you should consider adding this into your post as an option and no need for manual work at all

        Here is the code

        // add anchor to headings for table of contents
        function auto_id_headings( $content ) {

        $content = preg_replace_callback( ‘/(\(.*)()/i’, function( $matches ) {
        if ( ! stripos( $matches[0], ‘id=’ ) ) :
        $matches[0] = $matches[1] . $matches[2] . ‘ id=”‘ . sanitize_title( $matches[3] ) . ‘”>’ . $matches[3] . $matches[4];
        endif;
        return $matches[0];
        }, $content );

        return $content;
        }
        add_filter( ‘the_content’, ‘auto_id_headings’ );

        1. LH says:

          Very nice! I will also add it, thank you 🙂

        2. LH says:

          Hey, I added the JS part! 🙂 Note, that the HTML structure in PHP changed a little bit.

          Regards,
          Lorenz

          1. Dejan says:

            Thx a lot. I will check and try to implement it on my website. I will let you know how worked. 

          2. Dejan says:

            Thx a lot. I will try it and see how it works for me…

  12. yf says:

    Great function. But how have exactly autolinking? The render html don´t have a href with link. Can you update little function child for that please.

    p.s; i tried function “* Automatically add IDs to headings such as” but not success, when i see inspector, i see the li but nothing about ahref=”link to the section”

    1. LH says:

      Hey, next time when I update this post I will check if I can add this feature.

      Greetings
      LH

      1. yf says:

        Thanks. Can you paste some tips quick tips code to do that please and replease, a child function for this option.

        1. yf says:

          if posible before update the post of course

          1. yf says:

            I forgot: seem you have a conflict when we use special tags blog spintax from plugin page generator

            https://www.wpzinc.com/documentation/page-generator-pro/generate-using-spintax/
            i believe is for that use ‘ # ‘ (example #p#text text #/p)

  13. Hello friend, I tried lots of coding to display h4 headings in the “table of content”, but failed, kindly provide the code display h4 headings as well. I need help friend.

    1. LH says:

      Hey!
      In the comments of the german version of this post (click here) has someone posted a recursive version of the code (including h4). You can try this code. Someday I will create my own (improved) version – until you can you his code. 🙂

  14. Thanks for this code buddy, but i want to know how to display output means “table of content” in single.php file, what will be the code for single.php file to include to display table of content. I don’t want to write {{TABLE_OF_CONTENTS}} again and again in every post.

    1. LH says:

      You’re welcome! You can use a code snippet from another tutorial: https://webdeasy.de/en/wordpress-code-snippets-en/#show-similar-posts. You can combine this code with the code from the table of contents. Just rewrite the line if($i === $middle_index) { to if($i === 1) {. And inside this if-statement you can output the table of contents.

      The table of contents will be displayed after the first paragraph in every post.

      I hope I was able to help. If you have any other questions, let me know. 🙂

      1. Deepak says:

        Thats great friend, bcz i was looking for “interlinking code” as well, but can you plz provide a detail tutorial with code either in this comment or a new article for both the tasks “interlinking” and “table of content” bcz i am one of them who knows where to put the code if someone tells but don’t understand the code fully,

        Plz create a tutorial for both tasks or provide a full code for both tasks and tell where to include in single.php and which code to include in functions.php file.

        Friend it will really help

        1. LH says:

          Hey! I have edited this post. Now you have two options to choose.
          Thanks for the idea, hope this is that what you need! 🙂

          1. Thanks for this friend, I am facing another error in linking the “table of content headings” to the headings of the article. Please create or provide a code that will automatically link the “table of content headings” with the “Article headings”. Please check any article from my site and I think you will easily understand my issue, I have implemented the code in my site.
            please solve this friend, you are the last hope in the internet world. Bcz there is no tutorial like this, which explains in deep and really solves the issue.
            thanks in advance

          2. Again Back here friend, I searched for the code to automatically insert id to the heading tags. code is here
            <?php
            /**
            * Automatically add IDs to headings such as
            */
            function auto_id_headings( $content ) {

            $content = preg_replace_callback( ‘/(\(.*)()/i’, function( $matches ) {
            if ( ! stripos( $matches[0], ‘id=’ ) ) :
            $matches[0] = $matches[1] . $matches[2] . ‘ id=”‘ . sanitize_title( $matches[3] ) . ‘”>’ . $matches[3] . $matches[4];
            endif;
            return $matches[0];
            }, $content );

            return $content;

            }
            add_filter( ‘the_content’, ‘auto_id_headings’ );
            ?>
            this code works perfectly and included the ids in each heading tag of my articles, you can my site “tech behind it”, it’s still there, but linking still don’t appear. However, if I add the id from the admin panel(backend) to the headings of any article, it works, please provide a solution(code) that gives automatic linking to “table of content headings” with article headings. Thanks

          3. LH says:

            Try to put the “insert id” code before the table of contents code

          4. superb! now everything works better, i am still curious how to display h4 and h5 headings in “the table of content”.

            It’s a request of a fan of you and your site to provide code to display h4 and h5 headings in the “table of contents”.
            then it will be the most perfect open source code in the internet world

  15. LEAC says:

    Thank it works !
    I would bery much like an explanation of the regular expression used in the preg_match_all at the beginning.
    If you had time to do this, I would very much appreciate it.
    Best regards

    1. LH says:

      Good point, I will add it. 🙂

    2. LH says:

      Done 😉

  16. manish kumar says:

    Thanks Working for me

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