Bengal automatically generates a table of contents from page headings. Access it viapage.toc.
The Pattern
| {% 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:
| [markup.toc]
start_level = 2 # Start at H2 (skip H1 page title)
end_level = 3 # Stop at H3
|
Variations
Per-Page Disable
| ---
title: Simple Page
toc: false
---
|
Then in template:
| {% 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
| {# Only show TOC for docs section #}
{% if page.section == 'docs' and page.toc %}
<nav class="toc">
{{ page.toc | safe }}
</nav>
{% endif %}
|
Highlight current section as user scrolls — this part is standard JS, not Bengal-specific:
| 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));
|