Web Development

Debugging the Invisible: When Your Table of Contents Goes Missing

How custom React components can break auto-generated features, and why understanding DOM-based tooling is essential for modern web development.

Dec 8, 2024
8 min

Building With AI?

Learn how to build features like this yourself. I offer 1-on-1 AI web development coaching to help you ship faster with tools like Claude, Cursor, and ChatGPT.

The Symptom

I was reviewing blog post #1—"The 10-Minute Blog Upgrade"—when I noticed something odd. The Table of Contents showed only a handful of items. The blog had 11 sections. The TOC showed maybe 3.

This is the kind of bug that makes you question reality. The code hadn't changed. The TOC component worked fine on other posts. What was different about this one?

The Investigation

First instinct: check the TOC component. Maybe there's a filter I forgot about. Maybe there's a limit.

// table-of-contents.tsx
const headings = contentRef.current?.querySelectorAll('h2, h3') || [];

Nope. It's scanning for all H2 and H3 elements. No limits. No filters (except excluding headings inside the TOC itself to prevent recursion).

Second instinct: check the markdown. Maybe the headings are malformed.

## The WordPress Problem
## The Custom Blog Advantage
## How the Retrofit Works
...

All correct. Proper H2 syntax. IDs would be auto-generated.

Third instinct: actually look at the rendered DOM.

And there it was. Or rather, there it wasn't.

The Root Cause

This blog post uses custom visual components. Instead of rendering the markdown content directly, we render a React component:

// blog-post-new.tsx
) : blogPost.slug === "10-minute-blog-upgrade-ai-retrofit" ? (
  <>
    <div className="text-lg text-slate-700 leading-relaxed mb-8">
      <p className="mb-6">Opening paragraph...</p>
    </div>
    <TenMinuteBlogUpgradeVisuals />
  </>

The markdown content is never rendered to the DOM.

The visual component had its own structure—cards, diagrams, timelines—but it only used H3 headings for sub-sections. No H2 headings for main sections.

The TOC was doing exactly what it was supposed to: scanning the DOM for H2/H3 elements. It found the H3s from the visual components. It found nothing else because there was nothing else.

The Fix

The solution is embarrassingly simple: add explicit H2 headings to the visual component.

export function TenMinuteBlogUpgradeVisuals() {
  return (
    <>
      <h2 id="the-wordpress-problem" className="text-3xl font-bold text-slate-900 mt-10 mb-4">
        The WordPress Problem
      </h2>
      
      <div className="prose prose-lg max-w-none my-6">
        <p>Content here...</p>
      </div>

      <Card1_MainStat />

      <h2 id="the-custom-blog-advantage" className="text-3xl font-bold text-slate-900 mt-10 mb-4">
        The Custom Blog Advantage
      </h2>
      
      {/* More content... */}
    </>
  );
}

Key requirements:

  1. Use proper H2 elements (not styled divs)
  2. Include id attributes for anchor linking
  3. Use kebab-case IDs: id="the-wordpress-problem"
  4. Place headings before the relevant content blocks

The Deeper Lesson

This bug reveals a fundamental tension in modern web development: abstraction vs. semantics.

React encourages component composition. We build UIs from reusable pieces. But HTML has semantic meaning. Screen readers, search engines, and yes—Table of Contents components—rely on that semantic structure.

When we replace markdown rendering with custom components, we're taking responsibility for that semantic structure. The markdown processor would have created proper H2 elements with IDs. Our custom component needs to do the same.

The Pattern

For any blog post with custom visual components:

  1. Identify the main sections from your markdown structure
  2. Add explicit H2 headings at the start of each section
  3. Include IDs for anchor linking
  4. Test the TOC after adding visuals
// Template for visual components
export function YourBlogVisuals() {
  return (
    <>
      <h2 id="section-one" className="text-3xl font-bold text-slate-900 mt-10 mb-4">
        Section One
      </h2>
      <YourVisualComponent />
      
      <h2 id="section-two" className="text-3xl font-bold text-slate-900 mt-10 mb-4">
        Section Two
      </h2>
      <AnotherVisualComponent />
    </>
  );
}

Documentation as Prevention

I updated our blog documentation with a new section: "⚠️ CRITICAL: Table of Contents Requirements"

The best bugs are the ones you never have to fix twice. Documentation turns a debugging session into institutional knowledge.

The Meta Reality

This bug happened on a blog post about building blog systems. I was adding features to demonstrate the power of custom components, and in doing so, broke one of the most basic features.

There's a lesson there about complexity. Every abstraction has a cost. Every custom solution requires custom maintenance. The 10-minute upgrade becomes a 30-minute debugging session when you forget the fundamentals.

But that's also the point. When you own your system, you can fix it. When you understand how it works, you can extend it. The bug was mine to create, and mine to solve.

Key Takeaway

DOM-based features require DOM-based elements. If your TOC scans for headings, your content needs headings. If your component replaces the content that would have had headings, your component needs to provide them.

Abstraction doesn't exempt you from semantics. It just makes you responsible for them.

Share This Article

Spread the knowledge

Free Strategy Session

Stop Guessing.
Start Growing.

Get a custom strategy built around your goals, not generic advice. Real insights. Measurable results.

No obligation
30-min call
Custom strategy

Continue Your Learning Journey

Explore these related articles to deepen your understanding of web development

AI Dev Session: Building Social Carousel Cards & Admin Dashboard in 3 Hours

Building social carousel cards, admin dashboard, and fixing a sneaky TOC bug—in 3 hours.

8 min read
Read →

Volume 7: Branding Our Own Blog Series

The meta moment when we built custom branding for the Pink Slips NSW blog series - pink gradients, dual logos, and conditional styling in 2 hours.

6 min read
Read →

Building 500+ Location Pages with AI-Generated Content

Building 790 location pages with AI content, database schema, and SEO implementation.

20 min read
Read →