A jQuery Table of Contents for your Ghost Blog Entries
January 13, 2019

A jQuery Table of Contents for your Ghost Blog Entries

There are various approaches to adding a TOC to your Ghost blog entries, and this one is a rewrite of a solution by Grant Winney. Grant's original solution is great, but I wanted to avoid having to customize the theme files. Our approach will instead be to just inject some javascript and some css and be done with it.

So, without further ado, here's our little script:

<!-- Insert a TOC based on heading levels after specified node -->
<!-- based on: https://grantwinney.com/creating-a-table-of-contents-for-your-blog/ -->
<script>
    (function (context, $, undefined) {
	'use strict';
	
	context.getToc = function (sourceSelector, minHeading, beforeContent, afterContent) {
		var prevLevel = minHeading-1;
		var output = '';
        var firstNodeProcessed = false;
		$(sourceSelector).each(function( index ) {
			var currLevel = $(this).prop('tagName').slice(-1);
			if (currLevel > prevLevel) {
				var ranOnce = false;
				while (currLevel > prevLevel) {
					if (ranOnce) {
						output += '&nbsp;';
					}
					output += '<ol><li>';
                    if (!firstNodeProcessed && currLevel > minHeading) {
                        output += 'Warning! First header should be H' + minHeading + ', is H' + currLevel + ' instead!';
                    }
                    firstNodeProcessed = true;
					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="#' + $(this).attr('id') + '">' + $(this).text() + '</a>';
		});

		if (output != '') {
			while (prevLevel >= minHeading) {
				output += '</li></ol>';
				prevLevel -= 1;
			}
			output = beforeContent + output + afterContent;
		}
		
		return output;
	};
	
	context.appendToc = function(containerSelector, minHeadingLevel, maxHeadingLevel, targetSelector, minNumberOfEntries, beforeContent, afterContent) {
        var sourceSelector = '';
        var currentHeadingLevel = minHeadingLevel;
        if (!beforeContent) beforeContent = '<section class="widget widget-text table-of-contents"><header class="major"><h2 class="widget-title">Table of Contents</h2></header><div>';
        if (!afterContent) afterContent = '</div></section>';
        while (currentHeadingLevel <= maxHeadingLevel)
        {
            if (sourceSelector != '') sourceSelector += ', ';
            sourceSelector += containerSelector + ' h' + currentHeadingLevel;
            currentHeadingLevel += 1;
        }
        if ($(sourceSelector).length >= minNumberOfEntries) {
			$(targetSelector).after(context.getToc(sourceSelector, minHeadingLevel, beforeContent, afterContent));
        }
	};
	
}(window.TocWriter = window.TocWriter || {}, jQuery));
window.TocWriter.appendToc('.post-template div.content', 2, 6, '#menu', 2);    
</script>

Note that we are using the "Revealing Object Pattern" for namespacing. The heart of the code - the part that generated the ordered list - has changed very little compared to Grant's solution, I've just expanded it with some error checking, moved some constants out into variables, removed some semantics that don't run on IE, and translated it to jQuery.

Deployment

All you need to do is plop this sucker into the "Blog Footer" part of your blog on the Code injection page of your Ghost admin panel, and then tweak it just a little bit. Alternatively, you can create a .js resource, upload it somewhere in your theme directory (in my case: /var/www/ghost/content/themes/editorial-master/assets/main/js) and reference it near the end of your default.hbs. Since the script makes use of jQuery, it needs to be placed after the point where jQuery is loaded.

Configuration

The script if set up to work with the "Editorial" theme, but you can easily adjust it to work with any theme you like.

If you wish to include it in all of your posts, then just edit the last line of the script:

window.TocWriter.appendToc('.post-template div.content', 2, 6, '#menu', 2);

Alternatively, if you only want a TOC in some of your posts, then you can include the above line in the footer of any posts you want to add a TOC to.

The function expects the following parameters:

  1. A selector that identifies where your post content is within your page. For the Editorial theme, a good match seemed to be '.post-template div.content'
  2. The most significant heading level that you want included in your TOC, for example 1 for "H1", 2 for "H2"
  3. The least significant heading level that you want included in your TOC, for example "6" for "H6"
  4. A selector that identifies the node in you page after which the TOC will be inserted. If your theme has a sidebar, then that is a pretty good place to put it.
  5. A minium number of headings, if the script finds fewer headers than you specify here, it will not output the TOC
  6. optional: The html to output before the actual TOC, if you enter nothing, you can see from the script that this defaults to the template below.
  7. optional: The html to output after the actual TOC.

The default template is this:

<section class="widget widget-text table-of-contents">
    <header class="major"><h2 class="widget-title">Table of Contents</h2></header>
    <div>
    THE TABLE OF CONTENTS
    </div>
</section>

You can then style your TOC as needed. For me, the following was enough:

.table-of-contents ol {
    margin-bottom: 0;
}
.table-of-contents li {
    margin-top: 0.4ex;
    padding-left: 0;
}
.table-of-contents a {
    text-decoration: none;
    border-bottom: none;
}

Sources

A jQuery Table of Contents for your Ghost Blog Entries
Share this