Updated April 2019! Improvements:

  • Automatically places the table of contents before the first heading
  • Works on multi-level headings
  • Changed it to pure JavaScript to avoid having to load jQuery
  • Simple version just to show h2 headings

When I published the Professional Australian Glossary I decided to add a table of contents. It was a large article. But I thought... there's got to be a way of automating this. And if there isn't, I'll build one!

I pieced this together from around the web, but had to redo it a bit to make it work in Ghost.

Introduction

Tables of contents are almost mandatory to making a blog post easier to read. There are plugins for WordPress, but no easy way to do it on a Ghost blog.

Sub-heading

This is a lower-level nested block, to make sure it works with h3 and below headings.

How to install

Put this code at the bottom in an HTML block. This looks through the code and finds all the headings (tagged with h2... did you use headings?) and creates the contents.

Notes:

  • Your theme might use h1 instead of h2. Adjust the code below accordingly.
  • You don't want to include the heading for Table of Contents in the table of contents. So I included code to add that title in, and then also to look out for that heading, so as not to index it.

You have two options. First is jQuery. It's effective and cleaner. But there's pure JavaScript below.

jQuery version

<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha256-3edrmyuQ0w65f8gfBsqowzjJe2iM6n0nKciPUp8y+7E=" crossorigin="anonymous"></script>
<script>
    var contentsTitle = "Table of Contents"; // Set your title here, to avoid making a heading for it later
	var ToC = "<h2>"+contentsTitle+"</h2>";
    ToC += "<nav role='navigation' class='table-of-contents'><ul>";
    var first = false;
	$("h2,h3").each(function() {
		var el = $(this);
        if (first === false) {
        	first = true;
            $('<span id="dynamictoc"></span>').insertBefore(el);
        }
        var title = el.text();
		var link = "#" + el.attr("id");
        if (el.is("h2")) {
			ToC += "<li><a href='" + link + "'>" + title + "</a></li>";
        } else if (el.is("h3")) {
        	ToC += "<li style='margin-left:2em'><a href='" + link + "'>" + title + "</a></li>";
        }
	});
	ToC += "</ul></nav>";
    console.log(ToC);
	$("#dynamictoc").html(ToC);
</script>

Pure JavaScript version

I wrote this version because I felt like loading extra resources was lazy, and Ghost doesn't use it (and I wouldn't want to rely on whatever version they were using anyway). Secondly, this is pretty basic DOM manipulation and I thought: why not just do it in pure JS anyway? ES6 is amazing, anyway.

<script>    
   var contentsTitle = "Table of Contents"; // Set your title here, to avoid making a heading for it later
	var ToC = "<h2>"+contentsTitle+"</h2>";
    ToC += "<nav role='navigation' class='table-of-contents'><ul>";
    var first = false;
 
    document.querySelectorAll('h2,h3').forEach(function(el,index) {
        if (first === false) {
        	first = true;
            var newSpan = document.createElement("SPAN");
            newSpan.id="dynamictocnative";
            el.parentNode.insertBefore(newSpan, el);
        }       
		var title = el.textContent;
		var link = "#" + el.id;
        if (el.nodeName === "H2" && el.className != "post-card-title") {
			ToC += "<li><a href='" + link + "'>" + title + "</a></li>";
        } else if (el.nodeName === "H3" && el.className != "post-card-title"){
        	ToC += "<li style='margin-left:2em'><a href='" + link + "'>" + title + "</a></li>";
        }
    });
	
	ToC += "</ul></nav>";
    var tocDiv = document.getElementById('dynamictocnative');
    ToC += '<style>#dynamictocnative { width: 100%; }</style>';
    tocDiv.outerHTML = ToC;
</script>

Here's the version I use in this blog, which uses the Casper theme. It doesn't do h3 tags and it also omits headings used in cards at the bottom of the page.

<script>    
	var ToC = "<h2>Table of Contents</h2>";
    ToC += "<nav role=""navigation""<ul>";
    var first = false;
 
    document.querySelectorAll('h2').forEach(function(el,index) {
        if (first === false) {
        	first = true;
            var newSpan = document.createElement("SPAN");
            newSpan.id="dynamictocnative";
            el.parentNode.insertBefore(newSpan, el);
        }       
		var title = el.textContent;
		var link = "#" + el.id;
        if (el.nodeName === "H2" && el.className != "post-card-title") {
			ToC += "<li><a href='" + link + "'>" + title + "</a></li>";
        }
    });
	
	ToC += "</ul></nav>";
    ToC += '<style>#dynamictocnative { width: 100%; }</style>';
    var tocDiv = document.getElementById('dynamictocnative');
    tocDiv.outerHTML = ToC;
</script>

And here's a simple version just for h2 headings (what I use in this blog)

<script>    
   var contentsTitle = "Contents"; // Set your title here, to avoid making a heading for it later
	var ToC = "<h2>"+contentsTitle+"</h2>";
    ToC += '<nav role="navigation" class="table-of-contents"><ul>';
    var first = false;
 
    document.querySelectorAll('h2,h3').forEach(function(el,index) {
        if (first === false) {
        	first = true;
            var newSpan = document.createElement("SPAN");
            newSpan.id='dynamictocnative';
            el.parentNode.insertBefore(newSpan, el);
        }       
		var title = el.textContent;
		var link = '#' + el.id;
        if (el.nodeName === 'H2') {
			ToC += '<li><a href="' + link + '">' + title + '</a></li>';
        } else if (el.nodeName === "H3"){
        	ToC += '<li style="margin-left:2em"><a href="' + link + '">' + title + '</a></li>';
        }
    });
	
	ToC += '</ul></nav>';
    ToC += '<style>#dynamictocnative { width: 100%; }</style>';
    var tocDiv = document.getElementById('dynamictocnative');
    tocDiv.innerHTML = ToC;
</script>

Next steps

Things I want to make this do—coming soon, but help out if you can!

  • Make this a js plugin so I can embed it in a website, and if there are more than 2 headings it makes a table of contents. You can do this by putting it as as a JS file on your local server in [your server directory]/content/themes/casper/assets/built/toc.js

Things already done:

  • Make it work without using jQuery - done!
  • Nested headings (h1, h2, h3 and so on) - done!
  • Automatic placement before the first heading (other than the title) - done!