Skip to content

Table of Contents

Press can automatically generate a table of contents by collecting elements marked as outline entries, then rendering them on a page marked as deferred.

Creating Outline Entries

Add the outline attribute to any element to register it in a named collection:

xml
<h1 outline="sections">Introduction</h1>

This adds the element to a collection called "sections". You can create multiple collections for different purposes.

Outline Levels

Use the level attribute for hierarchical outlines:

xml
<h1 outline="sections" level="1">Data Sources</h1>
<h2 outline="sections" level="2">Satellite Data</h2>
<h2 outline="sections" level="2">Ground Stations</h2>
<h1 outline="sections" level="1">Analysis</h1>

level and outline can also be set in <styles>:

xml
<styles>
  <h1>
    <level>1</level>
    <outline>custom-headings</outline>
  </h1>
</styles>

Default Headings Outline

When you use the default <h1> through <h6> tags, they are automatically added to a default collection called "headings" with appropriate levels. You don't need to add outline manually for standard headings in flows.

The level each tag carries matters: <h1> is level 1, <h2> is level 2, and the first heading you list in a contents page has to be level 1. Build one over <h2>s alone — or over a Markdown flow whose sections start at ## — and Papermill rejects the document with message Outline entry 'entry.page-number' has no page number yet — its target hasn't been laid out. Set <level>1</level> for that tag in <styles>. Add <outline> beside it only when nothing else marks the heading — a payload Markdown body, where you cannot put attributes on the elements; naming an outline also takes the heading out of headings. A Markdown body written with # needs neither, provided the contents page iterates the headings collection those tags already join — but that collection holds every heading you have not routed to an outline of your own, so a cover page's heading is listed too, and a cover heading below level 1 rejects it. Name your own outline where headings live outside the body flow.

Levels below the first may skip, and what happens then depends on how deep the contents page iterates. One that lists entries with a single <repeat> renders, silently leaving the skipped heading out. One that nests a second <repeat outline="entry"> inside the first to list sub-entries is rejected instead. So a contents page that renders is not evidence the levels are contiguous — only that nothing iterated as deep as the gap. Keep them contiguous: #, then ##, then ###.

Deferred Pages

A table of contents needs to know the page number of every entry. Papermill achieves this through deferred pages and flows.

xml
<page deferred="true">
  <h1>Contents</h1>
  <!-- This content is rendered after all other pages -->
</page>

Tables of Contents Page

Here's a page definition called <toc> which renders a custom contents page:

xml
<pages>
  <toc deferred="true" flow="contents" repeat="true">
    <flow deferred="true" name="contents">
      <h1>Contents</h1>
      <table>
        <repeat outline="sections" item="entry">
          <tr>
            <td width="40pt">{{ entry['page-number'] }}</td>
            <td width="fill">{{ entry['text-content'] }}</td>
          </tr>
        </repeat>
      </table>
    </flow>
  </toc>
</pages>

Include this in <documents> at the place where you want the table of contents to render:

xml
<documents>
  <toc />
</documents>

Outline Entry Properties

When iterating over an outline collection, each entry has these properties:

PropertyDescription
page-numberThe page number where the entry appears
text-contentThe text content of the element
indexThe position in the collection (1-based)
idThe element's unique identifier

Building a Contents Page

Here's a complete example of a report with an auto-generated table of contents:

xml
<press>
  <document format="A4" page-margin="2cm">
    <coverpage />
    <toc />
    <repeat flow="body">
      <body-page />
    </repeat>
  </document>

  <pages>
    <coverpage>
      <frame height="fill" v-align="center">
        <h1 font-size="36pt">{{ data.title }}</h1>
        <p font-size="14pt" font-color="#6b7280">{{ data.subtitle }}</p>
      </frame>
    </coverpage>

    <toc deferred="true" flow="contents" repeat="true">
      <flow deferred="true" name="contents">
        <frame font-style="bold" font-size="18pt" margin-bottom="20pt">
          Contents
        </frame>
        <table>
          <repeat outline="sections" item="entry">
            <show-if condition="entry['page-number']">
              <tr>
                <td font-size="14pt">{{ entry['page-number'] }}</td>
                <td width="fill">{{ entry['text-content'] }}</td>
              </tr>
            </show-if>
          </repeat>
        </table>
      </flow>
    </toc>

    <body-page>
      <frame padding-bottom="40pt" height="fill">
        <flow name="body" />
      </frame>
      <frame text-align="center" font-size="9pt">
        Page <page-number />
      </frame>
    </body-page>
  </pages>

  <flows>
    <body type="markdown">
      <h1 outline="sections">Executive Summary</h1>

      The organisation achieved record growth in 2025, driven by
      expansion into three new markets and a 40% increase in
      recurring revenue.
      <frame-break />
      <h1 outline="sections">Financial Overview</h1>

      Total revenue reached £12.4M, a 28% increase year-on-year.
      Operating margins improved to 18%, up from 14% in the
      previous fiscal year.
      <frame-break />

      <h1 outline="sections">Strategic Outlook</h1>

      The board has approved investment in two new product lines
      for 2026, targeting the healthcare and education sectors.
    </body>
  </flows>

  <styles>
    <table>
      <child name="td">
        <border-weight>0</border-weight>
        <border-weight-bottom>0.5pt</border-weight-bottom>
        <border-color>#e5e7eb</border-color>
        <padding-top>6pt</padding-top>
        <padding-bottom>6pt</padding-bottom>
      </child>
    </table>
  </styles>

  <data type="json">
  {
    "title": "Annual Report 2025",
    "subtitle": "Meridian Holdings Group"
  }
  </data>
</press>

Auto-Numbered Headings

Use <counter-define> to create named counters that increment automatically:

xml
<components>
  <h1 outline="sections">
    <counter-define name="h1" />
    <counter-define name="h2" value="0" />
    <frame font-size="18pt" font-style="bold" space-after-desired="16pt">
      <counter-value name="h1" /> <slot />
    </frame>
  </h1>

  <h2 outline="sections" level="2">
    <counter-define name="h2" />
    <frame font-size="14pt" font-style="bold" space-after-desired="12pt">
      <counter-value name="h1" />.<counter-value name="h2" /> <slot />
    </frame>
  </h2>
</components>

This produces numbered headings like "1 Introduction", "1.1 Background", "2 Methodology", etc. The value="0" on the counter named "h2" in the "h1" definition resets the sub-counter each time a new h1 is encountered.