How to create a Table of Contents in WordPress (without plugin, my free code)

A table of contents for WordPress posts and articles is a clever thing. Visitors have a quick overview, SEO benefits and chic design really enhance your post!

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 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 complete code is programmed by myself and may be used free of charge for any purpose (also commercial) and may be modified as desired!

Step 1: Generate table of contents (PHP script)

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.

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

function get_toc($content) {
	// get headlines
	$headings = get_headings($content, 1, 6);

	// parse toc
	ob_start();
	echo "<div class='table-of-contents'>";
	echo "<span class='toc-headline'>Table Of Contents</span>";
	echo "<!-- Table of contents by webdeasy.de -->";
	echo "<span class='toggle-toc custom-setting' title='collapse'>−</span>";
	parse_toc($headings, 0, 0);
	echo "</div>";
	return ob_get_clean();
}

function parse_toc($headings, $index, $recursive_counter) {
  // prevent errors

  if($recursive_counter > 60 || !count($headings)) return;

  // get all needed elements
  $last_element = $index > 0 ? $headings[$index - 1] : NULL;
  $current_element = $headings[$index];
  $next_element = NULL;
  if($index < count($headings) && isset($headings[$index + 1])) {
    $next_element = $headings[$index + 1];
  }

  // end recursive calls
  if($current_element == NULL) return;

  // get all needed variables
  $tag = intval($headings[$index]["tag"]);
  $id = $headings[$index]["id"];
  $classes = isset($headings[$index]["classes"]) ? $headings[$index]["classes"] : array();
  $name = $headings[$index]["name"];

  // element not in toc
  if(isset($current_element["classes"]) && $current_element["classes"] && in_array("nitoc", $current_element["classes"])) {
    parse_toc($headings, $index + 1, $recursive_counter + 1);
    return;
  }

  // parse toc begin or toc subpart begin
  if($last_element == NULL) echo "<ul>";
  if($last_element != NULL && $last_element["tag"] < $tag) {
    for($i = 0; $i < $tag - $last_element["tag"]; $i++) {
      echo "<ul>";
    }
  }

  // build li class
  $li_classes = "";
  if(isset($current_element["classes"]) && $current_element["classes"] && in_array("toc-bold", $current_element["classes"])) $li_classes = " class='bold'";

  // parse line begin
  echo "<li" . $li_classes .">";

  // only parse name, when li is not bold
  if(isset($current_element["classes"]) && $current_element["classes"] && in_array("toc-bold", $current_element["classes"])) {
    echo $name;
  } else {
    echo "<a href='#" . $id . "'>" . $name . "</a>";
  }
  if($next_element && intval($next_element["tag"]) > $tag) {
    parse_toc($headings, $index + 1, $recursive_counter + 1);
  }

  // parse line end
  echo "</li>";

  // parse next line
  if($next_element && intval($next_element["tag"]) == $tag) {
    parse_toc($headings, $index + 1, $recursive_counter + 1);
  }

  // parse toc end or toc subpart end
  if ($next_element == NULL || ($next_element && $next_element["tag"] < $tag)) {
    echo "</ul>";
    if ($next_element && $tag - intval($next_element["tag"]) >= 2) {
      echo "</li>";
      for($i = 1; $i < $tag - intval($next_element["tag"]); $i++) {
        echo "</ul>";
      }
    }
  }

  // parse top subpart
  if($next_element != NULL && $next_element["tag"] < $tag) {
    parse_toc($headings, $index + 1, $recursive_counter + 1);
  }
}

function get_headings($content, $from_tag = 1, $to_tag = 6) {
  $headings = array();
  preg_match_all("/<h([" . $from_tag . "-" . $to_tag . "])([^<]*)>(.*)<\/h[" . $from_tag . "-" . $to_tag . "]>/", $content, $matches);
  
  for($i = 0; $i < count($matches[1]); $i++) {
    $headings[$i]["tag"] = $matches[1][$i];
    // get id
    $att_string = $matches[2][$i];
    preg_match("/id=\"([^\"]*)\"/", $att_string , $id_matches);
    $headings[$i]["id"] = $id_matches[1];
    // get classes
    $att_string = $matches[2][$i];
    preg_match_all("/class=\"([^\"]*)\"/", $att_string , $class_matches);
    for($j = 0; $j < count($class_matches[1]); $j++) {
      $headings[$i]["classes"] = explode(" ", $class_matches[1][$j]);
    }
    $headings[$i]["name"] = strip_tags($matches[3][$i]);
  }
  return $headings;
}

With this code all headings are read out, summarized and output again as table of contents.

By default, all headings (H1-H6) are read and output. If you want to customize this, you can adjust the parameters in line 3.

Insert table of contents in pages

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 Shortcode TOC, then add this code as well:

// TOC (from webdeasy.de)
function toc_shortcode() {
    return get_toc(get_the_content());
}
add_shortcode('TOC', 'toc_shortcode');

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_toc($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.

Do you want to earn money with your website or blog? Then check out my review about Ezoic, the best Google AdSense alternative!

Step 2: Style 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.

I have created two different styles. Just use the code from the table of contents that you like best.

Variant 1 – Minimalistic

CSS code for it:

/* Adjust these variables for your project */
:root {
	--dark-grey: #333333;
	--main-color: #036773;
	--font-size: 16px;
	--line-height: 1.2;
}

.table-of-contents {
	margin: 4rem 0;
	position: relative;
}

.table-of-contents .toc-headline {
	font-size: 22px;
	color: var(--dark-grey);
	font-weight: 600;
	display: block;
	cursor: pointer;
}

.table-of-contents .toggle-toc {
	position: absolute;
	top: 0;
	right: 1rem;
	font-size: 30px;
	cursor: pointer;
	font-weight: 800;
	color: var(--main-color);
}

.table-of-contents ul {
	padding: 0;
	padding-left: 1rem;
}

.table-of-contents li {
	position: relative;
	padding-left: 1rem;
	list-style: none;
	line-height: var(--line-height);
	font-weight: 400;
	margin: .3rem 0;
	transition: .2s ease all;
}

.table-of-contents li:hover {
	padding-left: 1.1rem;
}

.table-of-contents li a {
	font-size: var(--font-size);
	line-height: var(--line-height);
	text-decoration: none;
	color: var(--main-color);
}

.table-of-contents li:before {
	content: '';
	-webkit-transform: translateX(-0.75em) rotate(45deg);
	transform: translateX(-0.75em) rotate(45deg);
	height: 0.5em;
	width: 0.5em;
	border-top: solid 2px var(--dark-grey);
	border-right: solid 2px var(--dark-grey);
	border-radius: 0;
	background: 0 0;
	position: absolute;
	display: block;
	top: 7px;
	left: 0;
}

.table-of-contents li>ul {
	padding-left: 0.7rem;
	padding-bottom: 1rem;
}

Variant 2 – Basic

CSS code for it:

/* Adjust these variables for your project */
:root {
	--dark-grey: #333333;
  --background-color: #eee;
	--main-color: #036773;
	--font-size: 16px;
	--line-height: 1.2;
}

.table-of-contents {
	margin: 4rem 0;
	position: relative;
  background-color: var(--background-color);
  padding: .5rem 1rem;
  border-left: 5px solid var(--main-color);
  border-radius: 5px;
}

.table-of-contents .toc-headline {
	font-size: 22px;
	color: var(--dark-grey);
	font-weight: 600;
	display: block;
	cursor: pointer;
  margin-top: .3rem;
}

.table-of-contents .toggle-toc {
	position: absolute;
	top: .8rem;
	right: .8rem;
	font-size: 20px;
	cursor: pointer;
	font-weight: 800;
	color: #FFF;
  width: 1.5rem;
  height: 1.5rem;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 50%;
  line-height: 10px;
  background-color: var(--main-color);
}

.table-of-contents ul {
	padding: 0;
}

.table-of-contents li {
	position: relative;
	list-style: none;
	line-height: var(--line-height);
	font-weight: 400;
	margin: .3rem 0;
	transition: .2s ease all;
}

.table-of-contents li a {
	font-size: var(--font-size);
	line-height: var(--line-height);
	color: var(--main-color);
}

.table-of-contents li>ul {
	padding-left: 1rem;
	padding-bottom: .5rem;
}

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.

Step 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 = '+';
		}
	});
});

Close table of contents by default

If you want the table of contents to be closed by default, you can add the following line after the previous JavaScript code.

document.querySelectorAll('.table-of-contents .toggle-toc, .table-of-contents .toc-headline')[0].click();

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

Earn money with your website or blog

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.

Step 4: Include directory in individual contributions

If you have chosen option 1, in order to output the table of contents on a page now, you simply have to insert the shortcode TOC in the backend editor of the respective post.

Insert TOC shortcode into WordPress post
Insert TOC shortcode into 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:

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

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');

If you have chosen to insert via shortcode, you have to adjust the function toc_shortcode() from step 1 again:

// TOC (from webdeasy.de)
function toc_shortcode() {
    return get_toc(auto_id_headings(get_the_content()));
}
add_shortcode('TOC', 'toc_shortcode');

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 displayed.

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

127 Comments

  1. Gustav says:

    How do I make the “table of content collapse” on default so the visitor can expand it only if they need it?

    1. Lorenz says:

      Hey, I added a chapter for this case. Check it out, it’s just one line: Close table of contents by default

  2. Anouar says:

    Hello Lorenz, First of all I would like to thank you for your big help to the community.I have a problem with the TOC, only the bullets appear without the headings content as seen here: https://imgur.com/a/fctmN7XFor info: I’m using Astra child theme Thank you beforehand.

    1. Lorenz says:

      Hey Anouar, thanks for your words! I have just tested the script with Astra Theme – it’s working. Do you use normal WP headline blocks or something different? To debug your issue, you can print the $headings variable in the get_toc() function

      1. Lorenz says:

        Try to replace the get_headings() function with the code from this post. I’ve fixed another issue, maybe this has also fixed your issue 🙂

  3. Robert Baker says:

    This works great!  However when a user decides to bold or add some kind of style it breaks the TOC.  How do I get it where it ignores H2’s or H3’s with <span style=”font-weight: 600;”> for example?

    1. Lorenz says:

      I fixed the issue! Replace your get_headings() function with the updated code from this post

  4. Ali says:

    Hi Lorenz, I tried nitoc function of your script, and it does work in posts, when I add nitoc h2 class to the heading,however it doesn`t work if another plugin adds a section with h2-h6, maybe because plugins use different CSS classes for the headings already. Is there any way to exclude multiple CSS classes, that`s created by other plugins for h2, from being displayed in TOC?

    1. Lorenz says:

      Hi Ali, thanks for your note! Actually the script can’t do this. If you’re experienced in PHP you can adjust the code of the parse_toc() function to add your custom classes. I will make a note of it and include it in the next update.

      1. Ali says:

        Hi Lorenz, thank you for your response! I`m not very experienced in PHP (at all 🙂 ), but I managed to add custom classes, and they do work if headings are in post, but somehow it doesnt work if h2-h6 are inserted via shortcode of the plugin directly to the post. Is there any quick fix to, maybe ignore\hide TOC script to fetch anything from shortcodes in the posts at all? Thanks in advance!

  5. Cosmin says:

    Hi, any idea how we could change your code to match only the elements with a specific class, no matter what tag? I have something similar but unfortunately, I have no idea how to implement it in a shortcode (it’s a filter function), therefore your solution might work better. Thanks!

    1. Lorenz says:

      I don’t know if I get it right: You want to find elements instead of headlines? Thatfore you can change the first RegEx in the get_headings() function. But I think you need to do more adjustments in the code.

  6. Dan says:

    Hi! nitoc is not working for the headings inside Gutenberg blocks. For example, it does not work for the Cover block with an H1 heading. I’ve tried putting nitoc in the Additional CSS class(es) field of both the Cover block and the Heading block inside the Cover block. Can you please guide me on how to exclude those? Thanks.

    1. Lorenz says:

      Hi Dan, without your real world example I can’t reconstruct the error. You can try to output the $headings in the get_toc() function to see, if the script extracts the headlines exactly or if the error is while parsing the TOC. Maybe this will help you.

      1. Dan says:

        Hi Lorenz,Please refer to the 2 images below:1. WordPress Backend: https://freeimage.host/i/WPL7yB2. Frontend of the site: https://freeimage.host/i/WPQeRVAs you can see I do not want the heading of the Cover block in WordPress in the Table of Contents on the frontend of the site.I am not a coder, so I don’t understand what you mean by outputting the $headings.In fact, I do not want to output the headings.The nitoc CSS class works fine for stand-alone Headings block.But it does not work for other blocks like the Cover block, where the headings are inside the block.How to exclude the heading inside the Cover block?Thanks.

  7. Ali says:

    Hi Lorenz! Awesome TOC script, works really well! I have couple questions: Is it possible to add numbering and style in TOC headings, for H2 and H3, H4, H5, H6 like this: 1. h2 2. h2   2.1 h3   2.2 h3   2.3 h3      2.3.1 h4      2.3.2 h4 3. h2   3.1 h3   3.2 h3     3.2.1 h4 4. h2 5. h2 and so on… And the other question is, how to add TOC script before first H2 in the post? Thank you very much!

    1. Lorenz says:

      This is possible. It’s more than one line, so I will add this feature when I update this post – thanks for your interest! You can take the add_table_of_content() function and replace the string to split with an </h2> tag. That should work 🙂

      1. Ali says:

        Hi Lorenz, thank you very much for your response.I changed </p> with </h2> and it worked, but TOC is showing right below H2 title. Is it possible to insert TOC above H2 and not after that tag? Thank you very much, excited for the future post updates! 🙂

        1. Lorenz says:

          Try to split on the starting tag (<h2>). I didn’t test it, but it should work

          1. Jason says:

            Hi, this does not actually work. Since the code adds an ID into each heading, there no longer exist any blank heading tags… so you won’t be able to find just ‘<h2>’ within the html. You need to grab the ID of the first heading.I was wondering if you could please modify the code to include this? Also, are you doing an update soon to remove the PHP errors? Thanks!

          2. Lorenz says:

            Actually I don’t have the time to test/fix your issue. I can’t tell you exactly when I update the code to remove all PHP errors….

  8. Puran says:

    nitoc class not working for me, When adding it to any heading which is not removing from the table of content.

    1. Lorenz says:

      Did you check that the heading really has the class in your frontend? I will check the code again

  9. ASA04 says:

    Hello,Your article interested me enormously, and I set to work (little compared to what you have achieved!).My tests were done with: Local by Flywheel, PHP 8.0, WP: 5.9.3, Theme: NEVE 3.2.3. – Child-ThemeI encounter PHP errors, which I allow myself to list for you.It’s probably not much, but my level is too fair!I thank you in advance. Error Warnings:Warning: Undefined array key 1 in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 203 line 203: $headings[$i][“id”] = $id_matches[1]; Message x 6 fois Warning: Undefined array key “classes” in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 141 line 141: $classes = $headings[$index][“classes”]; Warning: Undefined array key “classes” in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 146 line 146: if($current_element[“classes”] && in_array(“nitoc”, $current_element[“classes”])) { Warning: Undefined array key “classes” in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 158 if($current_element[“classes”] && in_array(“toc-bold”, $current_element[“classes”])) $li_classes = ” class=’bold'”; Warning: Undefined array key “classes” in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 164 Warning: Undefined array key “classes” in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 141 Warning: Undefined array key “classes” in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 146 ….. Warning: Undefined array key 6 in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 132 $next_element = $index < count($headings) ? $headings[$index + 1] : NULL; Warning: Undefined array key “classes” in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 141 …. Warning: Trying to access array offset on value of type null in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 170 if(intval($next_element[“tag”]) > $tag) { Warning: Trying to access array offset on value of type null in C:\Users\jnnal\Local Sites\enligne\app\public\wp-content\themes\NEVE_CHILD\functions.php on line 178 if(intval($next_element[“tag”]) == $tag) {

    1. Lorenz says:

      Hi! These entries are only PHP Warnings, you can ignore them – the script should still run.

      1. ASA04 says:

        Thank you for your super fast response!Do you still have a track to correct these “small problems”, potentially embarrassing for the future. The syntax is correct even with php 8.0.Thank you once again for your work and your availability.

        1. Lorenz says:

          You’re welcome, glad I could help! I’ll notice and “fix” these warnings with the next update.

          1. ASA04 says:

            Super! +1 🙂Thanks

        2. Lorenz says:

          Hey! I “fixed” the PHP warnings. You can check out the new code 🙂

  10. Jack says:

    Hi Lorenz, thanks for this awesome script!Can you please share how to have the default menu state as closed?I managed to get it working but then only had H2 in menu (lost all h3+) 

    1. Lorenz says:

      Thanks! You can simply add this into your JS file:

      document.querySelectorAll(‘.table-of-contents ul’)[0].style.display = ‘none’;

      Hope this helps!

  11. Rahat Ali says:

    Hi Lorenz,I want use it as Menu behaviour. When I’ll click on toggle, TOC should hide. Is it possible.One thing more. I want to hide TOC on page reload.Please reply soon as possible.Thanx!

    1. Lorenz says:

      Of course. You need to add some basic JavaScript for that. I’ve implemented this here on my blog. You can check out the code with your browsers Developer tools

  12. Musta says:

    Hi,Very nice solution to display TOC without using any plugin.I have tested it but it doesn’t pick up h4.¿Any work-around to solve this problem?I saw the recursive solution on the German version but I have no idea how or where to place that code.I appreciate if you could help

    1. Lorenz says:

      Hi, thank you! This english and german version has the same code and it should also display your h4’s. I just tested it again and it’s working. Are all others displayed? You can print the $headings variable in the get_toc() function and see if all headlines are found by the regEx string.

      1. Musta says:

        Hi Lorenz.thanks for you quick reply.You are absolutely right. It works and it picks all h4 that do not have any html tags inside. I found out that h tags that have span, em or i, etc. tags are skipped.Is there a way to make the script or the regex pick up headings with html tags inside?Thanks again!

        1. Lorenz says:

          I’ve created a new RegEx for you:

          /<h([1-6])([^>]*)>(.*)<\/h[1-6]>/

          I didn’t test it exactly but you can give it a try. You also need to replace this line:

          $headings[$i]["name"] = $matches[3][$i];

          with this:

          $headings[$i]["name"] = strip_tags($matches[3][$i]);

          1. Musta says:

            Superb!!Now it works correctly. I really appreciate your help.I have a little problem though with multilevel nested lists.When the loop reaches the third level it doesn’t start all the way from first level for the next h2. It is not a css problem because I noticed this behaviour even before adding any styles.Please have a look at it here to better understand what I mean.As you can see there are two tables of content. The first one is based on your script and the second one is based generated by javascript using a widget that is packed with Elementor. This latter works correctly but as it is based on javascript I prefer your solution.If you need more details please let me know

          2. Lorenz says:

            I don’t understand the problem correclty… Maybe you can describe it better via mail: lh[at]webdeasy.de

  13. lola says:

    okay one last question, i decided to go the file manager way lolbut isn’t it possible to make this a plugin? i mean, i can do it, but i can’t link the css code to it. whereas i am doing this by pasting it in the theme it does. so i suppose i should add   specific code to link the two files to each other. but i can’t find anywhere how to do that – or i just dont know what to look for ;-;

    1. Lorenz says:

      You can also create an own plugin out of it, that’s also possible. But you don’t have a FTP Login, right?

      You can add the TOC CSS to your style.css from the child theme. Then you add the PHP code to your functions.php and create a JS file (how I wrote in the other comment) and include this file via wp_enqueue_script().

      1. lola says:

        i thought i didn’t, but i managed to get it anyways (my host helped) – yes, i made a plugin already. and the basic TOC works, but i added a .css file in the plugin which doesn’t work. so i can’t style the TOC. i expect i should link it or include the css in some way? (do you happen to have any tutorial on this? i feel like a pain in the ass lol)–other than that, i still have to get to the .js part, but i suspect i should link the .js also in some sort of way so that they work all together right?

        1. Lorenz says:

          Ok great! You can link CSS with wp_enqueue_style() and your JS with wp_enqueue_style().

          You can do both in your plugin PHP file. That should work! 🙂

  14. lola says:

    awesome, tysm!! one question though: i’m an inexperienced programmer. in my theme editor i can’t find a .js file (i want to be able to hide and show the toc).there is only a stylesheet.css and functions.php. what would you recommend doing in this case? (i very much need it because i have a lot of headings so my TOC is very very long)

    1. Lorenz says:

      Maybe there is a js or javascript folder with .js files? If not you can do the following:

      1. Create a script.js file in your theme folder and insert the JS Code from this tutorial
      1. Open your functions.php and add the following line: wp_enqueue_script(‘custom-js’, get_template_directory_uri() . “/script.js”, array(), ‘1.0.0’, true);


      Attention: If you’re using a theme that receives updates from time to time you should better create a child theme for this. Check this site

      If you’re using a childtheme, you need to replace get_template_directory_uri() with get_stylesheet_directory_uri() (from point 2)

      Hope this helps! 🙂

      1. lola says:

        no, but i am only using the theme editor in the wordpress dashboard. there i cannot find a js or javascript folder, merely the stylesheet.css and functions.php folder. i am indeed using a child theme. in the parent theme’s folder i can see .js folders, but i expect i can better avoid to change anything there because it will be gone with updates indeed. thank you! i was trying that out, but how do i make a script.js file in my theme folder? can i do that by using the wordpress dashboard?

        1. Lorenz says:

          I found this article about how to create a new file: https://chap.website/creating-new-files-in-the-wordpress-theme-editor/

          It would be easier if you connect with FTP and create the file there…

          1. lola says:

            okay i am very confused now, that article tells how to make a .php file (which i did now). but there is no use of that since i need a .js file to make the toc visible and hide, right?anyways, tried it anyways and added the code into the new .php file but that didn’t do anything.. ):

          2. Lorenz says:

            You have to create a .js file and not a .php file 😉

            After that you can use wp_enqueue_script() how I described above.

  15. David Smith says:

    Great code!  I’ve been trying the automatic addition of the heading ID’s using the additional code ‘before’ the TOC code. However, I cannot get them to work together.The TOC code works perfectly, and the automatic ID code works fine, the ID’s are being added, however the TOC links are not picking up the # anchor names, each link only has the hashtag and nothing after.Ive added the ID code before the TOC code, but nothing changes. Any ideas?Thanks in advance!

    1. Lorenz says:

      Thanks! It’s important that this line:
      add_filter('the_content', 'auto_id_headings');
      is before this line:
      add_filter('the_content', 'add_table_of_content');

      If you’re using the TOC Shortcode try to replace:
      return get_toc(get_the_content());
      with:
      return get_toc(auto_id_headings(get_the_content()));

      Hope this helps! 🙂

      1. David Smith says:

        Replacing the TOC shortcode was the missing link!Thanks again! This is such a great code to use. 

  16. Twe says:

    how can i add a active class in toc?Help me pls

    1. Lorenz says:

      What do you mean? Do you have the TOC sticky or fixed in your sidebar? Otherwise an active class doesn’t make sense, right?

  17. great tutorial. Thanks. I was wondering if I wanted to only add the TOC to posts that have more than 5 headings or 1000 words, how would I do that?Thanks

    1. Lorenz says:

      Thank you! I created a little code snippet for you. I think you chose option 2 to add the TOC automatically with add_table_of_content(), correct?

      Then just add these three lines in line 3 of the add_table_of_content() function:

      $text = trim(strip_tags($content));
      $word_number = substr_count("$text ", ' ');
      if ($word_number < 5000) return $content;

      This code only adds to TOC when your post has more than 5000 words.

      If you want that the TOC only appears when your post has more than 5 headings, add this code in line 4 of the get_toc($headings) function:

      if (count($headings) < 5) return $content;

      Hope this helps! If you have any questions, feel free to write again :)

  18. Ben says:

    Hallo Lorenz, dieses TOC Tutorial ist großartig!!!! Genau das was ich gesucht habe :)Leider funktioniert es bei mir nur teilweise.Ich versuche dieses automatische Inhaltsverzeichnis auf einer Oxygen Builder Seite zu nutzen.Hierfür habe ich den Code zur Erstellung der Tabelle in Code Snippets hinterlegt und gebe die TOC über einen Shortcode aus. Ebenso ist das Script, welches automatisch ID’s zu den Headings hinzufügt, in Code Snipptes hinterlegt. Das klappt so weit ganz gut.In meinem Beitrag werden alle Headings mit ID’s versehen und auch die Table of Content kann ich mit Hilfe eines Shortcodes ausgeben. Dort sind auch alle Überschriften aufgelistet. Automatisch mit einem Anker verlinkt sind jedoch nur die Überschriften, die von mir im Gutenberg Editor händisch einen Anker zugewiesen bekommen haben. Der Rest aller TOC-Elemente hat im ahref Attribut nur ein “#” hinterlegt. Auch der Scroll-Indikator scheint nicht zu funktionieren.Hier kannst du das Beispiel sehen:https://growboxen.eu/ratgeber/aktuelles/cannabis-legalisierung-in-deutschland-wann-ist-es-so-weit-und-was-ist-erlaubt/ Kannst du mir verraten wo der Fehler liegt? Ich bin leider kein Entwickler und kann daher nicht sehen, woran es hier scheitert. Das wäre super. Vielen Dank dir schon einmal :)Beste Grüße aus Regensburg! 🙂 

    1. Lorenz says:

      Hallo Ben, hast du das Auto-ID-Heading (add_filter(‘the_content’, ‘auto_id_headings’);) vor dem Code für die Generierung des TOC? Das scheint mir hier das Problem zu sein. Falls das nicht hilft, meld’ dich gern nochmal!

      Viele Grüße
      Lorenz

  19. Niam Amm says:

    Thats work very well for me, but i need remove text “table of content at top” and toogle, i just want the list, what the code that must i remove?

    1. Lorenz says:

      You need to remove these two lines:

      • echo “<span class=’toggle-toc custom-setting’ title=’collapse’>−</span>”;
      • echo “<span class=’toc-headline’>Table Of Contents</span>”;

  20. Niam Amm says:

    Very Nice, But i want use my css ul default. and i automatic got list with all of my heading with anchor link. Im newbie in coding. what must i doing?

    1. Lorenz says:

      If you want to use your own CSS, just don’t copy my CSS into 🙂

  21. Apoorva Singh says:

    Hi Thanks a lot for this code but one issue i am facing is scrolling of page I Kind of got the table of content but when I click those headings it not taking me to particular heading. May be something with the anchor tag. P.S. I am Novice in WordPress

    1. Lorenz says:

      Does is not scroll at all or does it scroll to the wrong headline? Maybe it’s an issue with another plugin or javascript code you’ve implemented on your page. Give some more information, than I can maybe write a little JavaScript Code for you that fixes that issue 🙂

  22. QA says:

    Hello, I copied the code of step 1. However I got error “undefined array key 1” of get id, I also var_dump $id_matches and att_string to find out a solution and found that after array(0) is a string. I am just a newbie so dont know how to fix the error.  

    1. Lorenz says:

      Hi! Please check if the regular expression (“/id=\”([^\”]*)\”/“) is exactly the same in your code. Sometimes a slash or backslash is deleted when coping this string. If this is correct check if your headlines on the page have the anchor attributes from step 4. If you need further help, feel free to comment again.

      1. Apoorva Singh says:

        Hi I really need your help in this code. 

        1. Lorenz says:

          Did you check the regular expression?

  23. Pete says:

    I followed every step but the TOC is not collapsing and it’s not linking all headings it’s linking a few headings.

    1. Lorenz says:

      It’s hard to say what the problem is. Maybe there is another JS problem which is causing the collapse issue… Press F12 and check the browser console for errors.

      Are all heading made of “h”-tags? You can try to print the $matches array in the auto_id_headings() function.

  24. Robert Baker says:

    I seem to be getting an error…

    Undefined array key 13

    1. Lorenz says:

      You can retry it now! I reviewed the whole post 🙂

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

    1. Lorenz says:

      You can retry it now! I reviewed the whole post 🙂  

  26. Sathish says:

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

    1. Lorenz says:

      You can also call get_toc(get_the_content()) in your single.php

  27. Kürşad says:

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

    1. Lorenz 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? 😀

  28. 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. Lorenz 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 🙂

  29. 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. Lorenz says:

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

  30. 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. Lorenz 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.

        1. Lorenz says:

          I will think about it! It’s very complex to handle all different Schema types… but I will see what I can do 🙂

  31. 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. Lorenz 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. Lorenz says:

          No problem dude, I will delete the second comment 🙂

  32. 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. Lorenz 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 🙂

  33. Rahat Ali says:

    Doesnt work for me in Genesis

    1. Lorenz 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. Lorenz 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…

  34. 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. Lorenz 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. Lorenz says:

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

        2. Lorenz 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…

  35. 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. Lorenz 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)

  36. 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. Lorenz 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. 🙂

  37. 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. Lorenz 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. Lorenz 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. Lorenz 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

  38. 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. Lorenz says:

      Good point, I will add it. 🙂

    2. Lorenz says:

      Done 😉

  39. manish kumar says:

    Thanks Working for me

Your email address will not be published.

bold italic underline strikeThrough
insertOrderedList insertUnorderedList outdent indent
removeFormat
createLink unlink
code

This can also interest you