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 writeTocToDocument
 */

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.

Those steps are generic to Ghost. Here's how I got it to work with the Wildbird theme.

  1. Modify post.hbs, near the bottom where it inserts the sidebar, so that it also passes a reference to the current page into the sidebar.

    {{!-- The tag below includes the theme sidebar - partials/sidebar.hbs --}}
    {{> sidebar this}}
    
  2. Modify the sidebar.hbs file so that, if the current page is a "post", it'll insert a new section containing your table of contents.

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

Snapshots

Here's how it looks when rendered.

A single-layer of headers:

simple table of contents

Two layers of headers:

table of contents with two levels

Multiple layers of headers:

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

table of contents with multiple levels

Some styling applied to remove the numbers and indent on linewrap

table of contents with css styling