Table of Contents in Eleventy

I set up my blog so that I could include a table of contents to long posts if I want to. It’s based on what headings I have. Currently, I’ve set it to work only with <h2> tags, as I rarely go deeper than that.

A Table of Contents listing out all the games I played.
Here's how it currently looks on my long Games 2020 post.

There are three parts to this:

  1. Use npm plugin markdown-it-anchor to add an id attribute to every heading.
  2. Set toc: true in the post’s frontmatter.
  3. Use JS to pull all of the headings, generate a list of links, and insert at the top of the post.

Anchor Plugin

Using the plugin markdown-it-anchor adds an id tag to each heading so you can do in-page links. This article explains how to install and configure it.

For example, a heading titled ‘A Short Hike’ would look like:

<h2 id="a-short-hike">A Short Hike</h2>

Liquid Markup

In my posts template post.liquid:

{% if toc %}
<details class="post__toc">
<summary class="post__toc-heading">Table of Contents</summary>
<ol class="post__toc-list"></ol>
{% endif %}

The <ol> list is empty, because each item will be inserted dynamically.

I nest all of this inside {% if toc %} so that it only shows up if I’ve designated toc: true in the post’s frontmatter.


In post.liquid:

  1. For every h2 on the page, create a list item
  2. Get the id of each heading
  3. Create a link in each list item, with the heading as the text and the id as the link URL
  4. Insert this list item into the <ol> tag.
const headings = document.querySelectorAll("h2");
const tocList = document.querySelector(".post__toc-list");

function generateTOC() {
for (let i = 0; i < headings.length; i++) {
let listItem = document.createElement("li");
listItem.setAttribute("class", "post__toc-item");

let listURL = "#" + headings[i].getAttribute("id");

let listItemLink = document.createElement("a");
listItemLink.setAttribute("href", listURL);
listItemLink.setAttribute("class", "post__toc-link");

let listItemTitle = document.createTextNode(headings[i].innerText);