Skip to content ๐Ÿ“ž Book Free Call Now
Blog

WordPress Table of Contents Without Plugin (Step-by-Step Guide)

By Rajan Gupta

โ€ข โฑ 4 min read

Why Use WordPress Table of Contents Without Plugin

Using a custom post type (CPT) instead of default posts gives you:

  • Better content isolation
  • Custom URL structure
    (/case-study/, /insights/, etc.)
  • Dedicated schema control
  • Performance benefits (no unnecessary filters or plugins)

Example CPT


register_post_type('case-study', [
  'label'    => 'Case Studies',
  'public'   => true,
  'rewrite'  => ['slug' => 'case-study'],
  'supports' => ['title', 'editor', 'thumbnail'],
]);

Content Structure (Very Important for SEO)

Your blog must follow this hierarchy:

H1 โ†’ Blog Title (Only ONE)
H2 โ†’ Main sections (used for TOC)
H3 โ†’ Sub-sections
Paragraphs
Lists
Images

Example


<h1>How We Increased Conversion Rate by 120%</h1>

<h2>Project Overview</h2>
<p>Brief description of the project scope.</p>

<h2>Challenges We Faced</h2>
<h3>Performance Issues</h3>
<h3>UX Problems</h3>

<h2>Our Solution</h2>

Rendered Output Example:

How We Increased Conversion Rate by 120%

Project Overview

We analyzed the existing funnel, user behavior, and technical bottlenecks affecting conversions.

Challenges We Faced

Performance Issues

Slow page load times and render-blocking assets were impacting user retention.

UX Problems

Poor CTA visibility and confusing navigation reduced engagement.

Our Solution

We optimized Core Web Vitals, restructured the layout, and improved CTA
placement.

Google uses H2s to understand page structure.
๐Ÿ’ก Your custom TOC reads H2 โ†’ perfect alignment.


Writing Content Using ACF (Best Practice)

Use ACF Flexible Content blocks like:

  • Content Block
  • Image + Content
  • Quote Block
  • Summary Block
  • Author Block

Each block outputs real HTML headings, not shortcodes.

โœ… Clean
โœ… Crawlable
โœ… TOC-friendly


Pure Custom Table of Contents (No Plugin)

You already implemented this correctly ๐Ÿ‘Œ

What makes it SEO-safe:

  • Reads real <h2> headings
  • Generates real anchor links
  • Uses ItemList schema
  • No JavaScript dependency for content rendering

Why Google Likes This

  • Anchors appear in SERP sitelinks
  • Improves dwell time
  • Improves page experience signals

TOC Schema (Rich Result Friendly)

Youโ€™re using the correct schema:


{
  "@type": "ItemList",
  "name": "Table of Contents"
}

โœ… Supported
โœ… Safe
โŒ No fake schema types
โŒ No misuse of FAQ

This helps Google understand page navigation structure.


document.addEventListener('DOMContentLoaded', function () {

  const contentWrap = document.querySelector(
    '.blog-details-content > .col-right > .blog-details-content-part'
  );

  const toc = document.querySelector('#case-toc');
  const tocList = toc?.querySelector('ul');

  if (!contentWrap || !toc || !tocList) return;

  const headings = contentWrap.querySelectorAll('h2');
  if (!headings.length) {
    toc.style.display = 'none';
    return;
  }

  const HEADER_OFFSET = 100; // adjust for sticky header
  const schemaItems = [];

  /* -------------------------
   * Build TOC + Schema
   * ------------------------- */
  headings.forEach((heading, index) => {

    if (!heading.id) {
      heading.id = `section-${index + 1}`;
    }

    /* ---------- TOC UI ---------- */
    const li = document.createElement('li');
    const a  = document.createElement('a');

    a.href = `#${heading.id}`;
    a.textContent = heading.textContent;

    a.addEventListener('click', function (e) {
      e.preventDefault();

      const targetPos =
        heading.getBoundingClientRect().top +
        window.pageYOffset -
        HEADER_OFFSET;

      window.scrollTo({
        top: targetPos,
        behavior: 'smooth'
      });
    });

    li.appendChild(a);
    tocList.appendChild(li);

    /* ---------- Schema ---------- */
    schemaItems.push({
      "@type": "ListItem",
      "position": index + 1,
      "name": heading.textContent,
      "url": window.location.href.split('#')[0] + '#' + heading.id
    });
  });

  const tocLinks = tocList.querySelectorAll('a');

  /* -------------------------
   * Active link on scroll
   * ------------------------- */
  function setActiveHeading() {
    let currentIndex = 0; // always keep one active

    headings.forEach((heading, index) => {
      const rect = heading.getBoundingClientRect();

      if (rect.top <= HEADER_OFFSET + 10) { currentIndex = index; } }); tocLinks.forEach(link => link.classList.remove('active'));

    if (tocLinks[currentIndex]) {
      tocLinks[currentIndex].classList.add('active');
    }
  }

  // Initial state
  tocLinks[0]?.classList.add('active');
  setActiveHeading();
  window.addEventListener('scroll', setActiveHeading);

  /* -------------------------
   * Inject JSON-LD Schema
   * ------------------------- */
  const schema = {
    "@context": "https://schema.org",
    "@type": "ItemList",
    "name": "Table of Contents",
    "itemListElement": schemaItems
  };

  const script = document.createElement('script');
  script.type = 'application/ld+json';
  script.textContent = JSON.stringify(schema);
  document.head.appendChild(script);

});

Build Faster, Rank Better โ€” Without Plugins

Want a lightweight WordPress setup with custom CPTs, plugin-free Table of Contents, and clean schema that Google actually understands?

๐Ÿ‘‰ Letโ€™s build a high-performance WordPress site together

Rajan Gupta

Rajan Gupta

FullStack Web Developer

Rajan Gupta is a passionate web developer and digital creator who loves sharing insights on WordPress, modern web design, and performance optimization. When not coding, they enjoy exploring the latest tech trends and helping others build stunning, high-performing websites.