Automatically populating a Table of Contents in Statamic

·
3 minute read

As Heydon Pickering highlighted, dropdown menus are a difficult design pattern. One of the best alternatives is a table of contents for pages that have a lot of data.

For the Work Notes redesign, I wanted to use a Table of Contents on several pages: notably the Terms, Privacy Policy and the Freelance Pricing Guide.

One of the issues with a Table of Contents is maintaining the list. Each time a content editor adds or removes a section, they need to remember to add/remove/update the corresponding link name and URL in the Table of Contents.

This might be acceptable if the content doesn’t change, but it’s a set of broken links waiting to happen if the content is regularly updated.

Work Notes is built on Statamic, which allows you to enter content in several ways. Markdown is one option, there’s the Bard content editor or the content fields can be built out with Replicator for a page builder-y experience.

Of these, only Markdown would allow you to effortlessly add a heading with an associated ID. The syntax would be something like this:

## This is my heading {#this-is-my-heading}

This works well, but it requires the Table of Contents to be manually updated. After a little thought, I realised it would be possible to create headings in a way that auto-populate a Table of Contents.

For reasons I won’t go into here, I usually build pages with Statamic’s Replicator, adding Bard fields where necessary. The method below uses the Replicator, but it could easily be ported to a Bard custom field block – in many situations, that might make more sense.

Headings

We’ll start with the headings themselves. I created a Fieldset with two fields: heading title (a text block) and heading level (a radio button so users can select the semantic level: H2, H3, etc).

The YAML file looks like this:

sections:
  main:
    display: Main
    fields:
      heading:
        type: text
        width: 50
        display: Heading
      heading_level:
        options:
          h2: H2
          h3: H3
        inline: true
        type: radio
        width: 50
        display: 'Heading Level'
taxonomies: true
title: 'Section Heading'

That Fieldset is called as a Partial in my Replicator. The user interface for adding a heading is:

The custom heading block created in Statamic. There’s a text input field labelled “Heading” and an option for the user to select the type of heading: H2 or H3.
The user interface for content editors.

In the Replicator, that block is labelled headings, so the Antlers markup for the headings looks like this:

{{ content_section }}
    {{ if type == "headings" }}
        <{{ heading_level }} id="{{ heading | slugify }}">{{ heading }}</{{ heading_level }}>
    {{ /if }}
{{ /content_section }}

{{ content_section }} is the name of the Replicator field.

The trick here is that we’re able to convert the heading text into a slug using Statamic’s slugify modifier, and set that as the element’s ID. Very handy.

The Table

The table itself has a simple YAML set-up. I have a single text field to set the Table of Contents title:

sections:
  main:
    display: Main
    fields:
      toc_title:
        type: text
        display: 'Table of Contents Title'
taxonomies: true
title: 'Table of Contents'

This lets me set a title and check to see if a title has been set. In my template, the Table of Contents will only display if there’s a title present.

You may not need to change the title of the Table of Contents, so this could be replaced with a toggle.

The template markup looks like this:

{{ if toc_title }}
    <nav class="toc" aria-labelledby="toc-heading">
        <h2 id="toc-heading">{{ toc_title }}</h2>
        <ul class="toc-links">
            {{ content_section }}
                {{ if type == "headings" && heading_level == "h2" }}
                    <li><a href="#{{ heading | slugify }}">{{ heading }}</a></li>
                {{ /if }}
            {{ /content_section }}
        </ul>
    </nav>
{{ /if }}

Let’s break this down.

The {{ if toc_title }} checks to see if a title has been set on the Table of Contents, and only displays the table if it has. The nav uses aria-labelledby to announce the title of the navigation as the custom title of the table.

Again, {{ content_section }} is the name of the Replicator field, so that tag is used to loop through the headings data. The if checks for any headings marked as h2 – we don’t want h3 headings listed in our table – then produces a list item that’s linked using that slugified title.

Wrapping it up

That’s the functionality sorted. Pages containing a Table of Contents tend to be quite lengthy, so you may want to add a Back to top link before each heading somehow.

As for styling the links, I used CSS Columns which are quite an elegant solution for dynamic content like this.