I wrote about 5 quick hacks for your Ghost theme last year, after switching to Ghost as a blogging platform. The last hack I mentioned was generating a "table of contents" using a handlebars script I'd found. Ghost was still considered beta at the time, and when 1.0 was released, the script stopped working correctly. I never bothered going back to figure out why.

But a table of contents is nice to have, and convenient for your visitors, so I wrote a new script that should work for any html page (with minor adjustments). You can get it from GitHub too.

/**
 * For displaying a table of contents - pass the entire document (DOM) to getTocMarkup
 */

function getHeaderLevel(header) {
    return Number(header.nodeName.slice(-1));
}

function createTocMarkup(headers) {
    var prevLevel = 1;
    var output = "";

    headers.forEach(function(h) {
        var currLevel = getHeaderLevel(h);
        if (currLevel > prevLevel) {
            var ranOnce = false;
            while (currLevel > prevLevel) {
                if (ranOnce) {
                    output += " ";
                }
                output += "<ol style=\"margin-bottom:0px\"><li>";
                prevLevel += 1;
                ranOnce = true;
            }
        } else if (currLevel == prevLevel) {
            output += "</li><li>";
        } else if (currLevel < prevLevel) {
            while (currLevel < prevLevel) {
                output += "</li></ol>";
                prevLevel -= 1;
            }
            output += "<li>";
        }

        output += `<a href="#${h.id}">${h.innerText}</a>`;
    });

    if (output != "") {
        // Change 2 to the max header level you want in the TOC; in my case, H2
        while (prevLevel >= 2) {
            output += "</li></ol>";
            prevLevel -= 1;
        }
        output = `<h2 class="widget-title">Table of Contents</h2><div style="margin-left:-10px">${output}</div>`;
    }

    return output;
}

function getTocMarkup(document) {
    // I was only interested in the headers within the element that had the .post-content class,
    // which is specific to the Ghost blog. If you're using this elsewhere, or are interested in
    // the entire document, delete this line and use document.querySelectorAll(...) on the next line.
    var body = document.getElementsByClassName('post-content')[0];
    
    // Add or remove header tags you do (or don't) want to include in the TOC
    var headers = body.querySelectorAll('h2, h3, h4, h5, h6');

    // Change the number to 1 if you want headers no matter what.
    // Or if you want at least 3 headers before generating a TOC, change it to 3.
    if (headers.length >= 2) {
        return createTocMarkup(headers);
    } else {
        return "";
    }
}

General Usage

Just call the function and write the return value out to the page.

<script type="text/javascript">
    document.write(getTocMarkup(document));
</script>

Usage in Ghost

Here's how I've got it displayed in the side bar in Ghost.

  1. Copy the above script into a file named toc.js, and drop it in the assets/js directory.
  2. Reference the file from default.hbs, somewhere between the <head></head> tags so it's available as the page loads. <script type="text/javascript" src="{{asset "js/toc.js"}}"></script>
  3. Call it from wherever you want to display it.

The previous steps are generic to Ghost.

Wildbird

To get it to work with the Wildbird theme specifically, I modified the sidebar.hbs file so that, if the current page is a "post", it'll insert a new section containing a table of contents.

{{!-- Table of Contents --}}
{{#is "post"}}
<section class="widget widget-text">
    <script type="text/javascript">
        document.write(getTocMarkup(document));
    </script>
</section><!-- .widget -->
{{/is}}

Casper

If you want it to work with the Casper theme, you'll need to make a couple changes.

  • First, change this line in the toc.js script, so that post-content is post-full-content, since that's the name of the class on the element that contains the body of your post.
  • Second, you'll have to update the post.hbs file directly, since there's no sidebar.hbs file like in the Wildbird theme.Look for the section that starts with <section class="post-full-content">. After that markup, setup an empty div where you'd like to display the table of contents. Leave {{content}} since that displays the body of your post. Now you can pass the document to the toc.js script and set the results to the div you just created. That has the effect of inserting the TOC at the top of your post, right after the image (if any).
<div id="toc"></div>

{{content}}

{{!-- Table of Contents --}}
<script type="text/javascript">
    var toc = document.getElementById('toc');
    toc.innerHTML = getTocMarkup(document);
</script>

Snapshots

Here's how it looks when rendered.

A single-layer of headers:

Two layers of headers:

Multiple layers of headers:

It somewhat handles omitted headers, like going right from H2 to H5, but not really nicely.

Some styling applied to remove the numbers and indent on linewrap