Add Table of Contents

Use Bengal's auto-generated page.toc for navigation

1 min read 267 words

Bengal automatically generates a table of contents from page headings. Access it viapage.toc.

The Pattern

1
2
3
4
5
6
{% if page.toc %}
<nav class="toc" aria-label="On this page">
  <h2>On this page</h2>
  {{ page.toc | safe }}
</nav>
{% endif %}

That's it. Bengal parses headings and generates nested HTML lists.

What's Happening

Component Purpose
page.toc Pre-rendered HTML list of headings
\| safe Render as HTML, not escaped text

Control Which Headings

Configure inbengal.toml:

1
2
3
[markup.toc]
start_level = 2  # Start at H2 (skip H1 page title)
end_level = 3    # Stop at H3

Variations

Per-Page Disable

1
2
3
4
---
title: Simple Page
toc: false
---

Then in template:

1
2
3
{% if page.toc and page.metadata.toc != false %}
  {{ page.toc | safe }}
{% endif %}

With Article Layout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<div class="article-layout">
  <aside class="sidebar">
    {% if page.toc %}
    <nav class="toc">
      <h2>On this page</h2>
      {{ page.toc | safe }}
    </nav>
    {% endif %}
  </aside>

  <article>
    <h1>{{ page.title }}</h1>
    {{ page.rendered_html | safe }}
  </article>
</div>

Conditional by Section

1
2
3
4
5
6
{# Only show TOC for docs section #}
{% if page.section == 'docs' and page.toc %}
  <nav class="toc">
    {{ page.toc | safe }}
  </nav>
{% endif %}

Scroll Highlighting (JavaScript)

Highlight current section as user scrolls — this part is standard JS, not Bengal-specific:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      document.querySelectorAll('.toc a').forEach(a => a.classList.remove('active'));
      document.querySelector(`.toc a[href="#${entry.target.id}"]`)?.classList.add('active');
    }
  });
}, { rootMargin: '-20% 0px -80% 0px' });

document.querySelectorAll('h2[id], h3[id]').forEach(h => observer.observe(h));

Seealso