Table of contents
A page can expose a table of contents from one declared Markdown document. Heine derives its headings and fragment identifiers from that source, while the template decides where and how to show the navigation.
Start with one page
Choose the declared content name, not its filename, and render it with the
source-aware markdown() function:
# content/en/guide/install.page
template = "guide.tera"
title = "Install Heine"
[content]
main = "main.md"
[toc]
content = "main"
min_level = 2
max_level = 3
An effective ToC requires a rendered authored page. Its selected declaration
must be one unsegmented .md file, rendered exactly once with
markdown(content="…").
Navigation labels are interface text, so keep them in Fluent even for a page that currently exists in one language:
# locales/en/site.ftl
toc-label = Table of contents
Add the corresponding message to every configured locale.
{# templates/guide.tera #}
{% extends "base.tera" %}
{% block content %}
<article>
<h1>{{ page.title }}</h1>
{% if page.toc and page.toc.headings %}
<nav aria-label="{{ t(id="toc-label") }}">
<ol>
{% for heading in page.toc.headings %}
<li class="toc-depth-{{ heading.depth }}">
<a href="#{{ heading.fragment }}">{{ heading.text }}</a>
</li>
{% endfor %}
</ol>
</nav>
{% endif %}
{{ markdown(content="main") }}
</article>
{% endblock %}
The selected main.md may contain headings at every level. When the template
renders it with markdown(content="main"), Heine adds IDs to all of them, but
only levels 2 and 3 appear in page.toc.headings here. A template can use the
numeric depth class for indentation without constructing a fragile nested
list.
page.toc is null when a page has no effective setting. It is an object
with the selected content name and a flat source-order headings array. Each
heading has level, depth, text, and fragment fields.
If the same content is also selected for margin footnotes,
this one markdown(content="…") call deliberately supplies both the heading
anchors and the local notes.
Apply one default to a directory
Documentation sections often use the same primary document. A descriptor can provide that recurring policy for its descendants:
# content/en/guide/_directory.toml
[pages]
template = "guide.tera"
[pages.toc]
content = "main"
min_level = 2
max_level = 3
The closest descriptor's complete ToC table is the default. A page can provide
its own complete [toc] table to replace it. The tables do not merge, so a
page that changes the heading range repeats content as well.
When one inherited page should not have a ToC, use this page-level opt-out:
toc = false
It is permitted only when it suppresses an inherited setting. The same
toc = false form under [pages] suppresses an inherited directory default
for a nested subtree. This prevents inactive configuration from accumulating.
Anchors and rendering
Heine derives fragments from parsed Markdown text. It uses NFC-normalized,
Unicode-lowercase text, preserves letters and numbers, turns punctuation and
spaces into hyphens, and adds deterministic suffixes for duplicates. For
example, two ## Introduction headings receive introduction and
introduction-2. A heading with no usable letter or number begins with
section instead.
Raw HTML headings are not Markdown headings, so they neither appear in the ToC nor receive generated IDs. Heine also does not inspect manually authored HTML IDs for collisions.
Use markdown(content="…") exactly once for the selected source. The familiar
filter is still useful for arbitrary text:
{{ data.notice | markdown }}
If a ToC-enabled page omits its selected source, renders it more than once, or
passes the exact source through | markdown, Heine reports a template error.
This keeps every generated fragment attached to one rendered heading. A
transformed copy cannot be recognized reliably, so do not use it as the source
of an enabled ToC.
The full field and template contract is in the reference, including errors for missing, non-Markdown, segmented, and unrendered-page cases. The starter site includes a compact guide section using this pattern in normal document flow, which works without a fixed sidebar on narrow screens.