How to Add a Table of Contents to WordPress Blog Posts Without a Plugin

If you run a WordPress blog, you already know the drill: every long-form article needs a table of contents to help readers navigate, improve dwell time, and pick up those juicy sitelinks in Google search results. The lazy solution is to install a plugin. The smart solution is to build it yourself. In this tutorial, we will show you how to add a table of contents in WordPress without a plugin by parsing your H2 and H3 headings automatically with a few lines of PHP and a sprinkle of JavaScript. No bloat, no third-party scripts, no performance hit. Why Build a Table of Contents Without a Plugin? Most tutorials on the first page of Google will tell you to manually create anchor tags and lists for every post. That works, but it is tedious and error-prone. Others will push you toward heavy plugins like Easy Table of Contents. Here is why the DIY approach wins in 2026: Performance: No extra HTTP requests, no jQuery dependency, no admin bloat. Full control: You decide the markup, the styling, and the behavior. SEO friendly: Clean semantic HTML with proper anchor links helps Google generate jump-to links in the SERP. No plugin conflicts: One less thing to update or break during a core WordPress upgrade. Reusable: Once written, it works on every post automatically. How It Works: The Logic Behind Automatic TOC Generation The idea is simple. Before WordPress prints your post content on the page, we intercept it with a filter, scan it for H2 and H3 tags, inject unique IDs into those headings, and build a nested list of links. Then we prepend that list to the content. Here is a quick overview of the pieces involved: Component Role PHP function in functions.php Parses headings, injects IDs, generates the TOC list WordPress filter the_content Applies the function to every single post CSS in your theme stylesheet Styles the TOC box Vanilla JavaScript (optional) Adds smooth scroll and collapse behavior Step 1: Add the PHP Function to Your Theme Open the functions.php file of your child theme (never edit the parent theme directly). Paste the following code at the bottom: function expressjs_generate_toc($content) { if (!is_single() || !in_the_loop() || !is_main_query()) { return $content; } // Match all H2 and H3 tags preg_match_all(‘/<h([2-3])(.*?)>(.*?)<\/h[2-3]>/i’, $content, $matches, PREG_SET_ORDER); if (count($matches) < 3) { return $content; // Skip TOC if fewer than 3 headings } $toc = ‘<div class=”expressjs-toc” id=”toc”>’; $toc .= ‘<p class=”toc-title”>Table of Contents</p>’; $toc .= ‘<ul>’; $current_level = 2; foreach ($matches as $match) { $level = (int) $match[1]; $title = strip_tags($match[3]); $anchor = sanitize_title($title); // Inject the ID into the heading inside the content $new_heading = ‘<h’ . $level . ‘ id=”‘ . $anchor . ‘”‘ . $match[2] . ‘>’ . $match[3] . ‘</h’ . $level . ‘>’; $content = str_replace($match[0], $new_heading, $content); if ($level == 3 && $current_level == 2) { $toc .= ‘<ul>’; $current_level = 3; } elseif ($level == 2 && $current_level == 3) { $toc .= ‘</ul>’; $current_level = 2; } $toc .= ‘<li><a href=”#’ . $anchor . ‘”>’ . $title . ‘</a></li>’; } if ($current_level == 3) { $toc .= ‘</ul>’; } $toc .= ‘</ul></div>’; return $toc . $content; } add_filter(‘the_content’, ‘expressjs_generate_toc’); What this code does, step by step: Runs only on single posts inside the main loop. Uses a regex to grab every H2 and H3 in the post content. Skips generation if the post has fewer than 3 headings (short articles do not need a TOC). Creates a slug for each heading using sanitize_title(). Injects an id attribute into each heading so anchor links work. Builds a nested unordered list and prepends it to the content. Step 2: Style the Table of Contents with CSS Add the following CSS to your theme’s style.css or the WordPress Customizer under Additional CSS: .expressjs-toc { background: #f7f9fc; border-left: 4px solid #0d6efd; padding: 20px 25px; margin: 30px 0; border-radius: 6px; font-size: 15px; } .expressjs-toc .toc-title { font-weight: 700; font-size: 18px; margin: 0 0 12px 0; } .expressjs-toc ul { list-style: none; padding-left: 15px; margin: 0; } .expressjs-toc ul ul { padding-left: 20px; margin-top: 6px; } .expressjs-toc li { margin: 6px 0; } .expressjs-toc a { text-decoration: none; color: #0d6efd; } .expressjs-toc a:hover { text-decoration: underline; } Feel free to adjust the colors to match your brand. This minimal style keeps the TOC readable without shouting for attention. Step 3: Add Smooth Scroll with Vanilla JavaScript Modern browsers support smooth scrolling natively via CSS, but a tiny script gives you more control (like offsetting for a sticky header). Add this to your theme’s JS file or drop it inline before the closing body tag: document.addEventListener(‘DOMContentLoaded’, function() { const links = document.querySelectorAll(‘.expressjs-toc a’); links.forEach(link => { link.addEventListener(‘click’, function(e) { e.preventDefault(); const target = document.querySelector(this.getAttribute(‘href’)); if (target) { const offset = 80; // adjust for sticky header height const top = target.getBoundingClientRect().top + window.pageYOffset – offset; window.scrollTo({ top: top, behavior: ‘smooth’ }); history.pushState(null, null, this.getAttribute(‘href’)); } }); }); }); That is it. Publish a post with a few H2 and H3 tags and reload it. You should see a clean, automatic table of contents at the top. Optional Improvements Make the TOC Collapsible If your posts are long, users may want to hide the TOC. Wrap the list in a details element for a native accordion with zero JavaScript: <details open> <summary>Table of Contents</summary> <ul>…</ul> </details> Add a Floating Sidebar TOC For desktop readers, a sticky TOC on the left side is a huge UX win. Use CSS position: sticky with a top offset and hide it on mobile with a media query. Support H4 Headings Simply update the regex pattern from [2-3] to [2-4] and add another nesting level in the loop. Common Pitfalls to Avoid Do not use it on pages with existing anchor plugins. You will get duplicate IDs. Escape HTML entities inside heading text. If your titles include quotes or ampersands, use esc_html() when building the anchor. Test with caching plugins. Clear your cache after editing functions.php or the TOC

How to Add a Table of Contents to WordPress Blog Posts Without a Plugin Read More »