Let your readers know what to expect in your posts and give them quick links to navigate content quickly by adding a table of contents with the Tocbot library.
Having a table of contents on your site is a nice creature comfort for post readers – having one that's automatically generated and always up to date is nice for post authors. In this tutorial, we’ll show you how to add an automatically generated table of contents to your Ghost site in a few quick steps.
Here's an example of what we'll build:
0:00/0:161×
Tocbot
To help us add the table of contents, we’re going to use a JavaScript (JS) library called . This library does the heavy lifting for us, which includes finding out the post’s headings, creating links to those sections, and showing the reader where they are on the page. It’s a super cool library!
💡Tocbot works by looking at your post’s heading tags like <h2>. Create a heading in Ghost by selecting your heading text and clicking the H icon in the popup editor.
There are two parts to the Tocbot library: 1) a JS file that handles functionality and 2) a CSS file that handles basic styling. Both need to be loaded into your Ghost theme.
In this tutorial, we’ll be using Ghost’s default theme, , but the steps here apply to any Ghost theme.
Edit default.hbs
Open default.hbs in . In this file, add the Tocbot script and styles.
🚧At the time of writing, Tocbot is on version 4.12.3. When installing it on your site, use the latest version by updating the version number in the URLs above. Advanced developers can install Tocbot directly from npm.
Add the CSS
Near the top of the file, in the head tag and right before {{ghost_head}}, add the Tocbot CSS:
We'll add some additional styles to a style tag directly after the CSS we just loaded in default.hbs.
These styles will help Tocbot look right at home in our theme. While what's shared below is specific to Casper, you can adapt the style to work with whatever theme you’re using.
Our goal for the TOC is for it to be present alongside article content on larger screens but above it on smaller ones.
<style>
.gh-content {
position: relative;
}
.gh-toc > .toc-list {
position: relative;
}
.toc-list {
overflow: hidden;
list-style: none;
}
@media (min-width: 1300px) {
.gh-sidebar {
position: absolute;
top: 0;
bottom: 0;
margin-top: 4vmin;
grid-column: wide-start / main-start; /* Place the TOC to the left of the content */
}
.gh-toc {
position: sticky; /* On larger screens, TOC will stay in the same spot on the page */
top: 4vmin;
}
}
.gh-toc .is-active-link::before {
background-color: var(--ghost-accent-color); /* Defines TOC accent color based on Accent color set in Ghost Admin */
}
</style>
handlebars
Add the JS
In default.hbs, now near the end of the file and right before {{ghost_foot}}, add the Tocbot script:
Having loaded the Tocbot script, we now need to initialize it, which tells Tocbot where on the page we want our table of contents and what we want to add to it.
Add this script right after the code from the last step:
{{! Initialize Tocbot after you load the script }}
<script>
tocbot.init({
// Where to render the table of contents.
tocSelector: '.gh-toc',
// Where to grab the headings to build the table of contents.
contentSelector: '.gh-content',
// Which headings to grab inside of the contentSelector element.
headingSelector: 'h1, h2, h3, h4',
});
</script>
handlebars💡The classes (gh-content, gh-toc) used in this tutorial are based on the Casper theme. You’ll need to change gh-content to match the container class (what wraps around your post content) of your theme. gh-toc can be anything you want – you just need to ensure that the class in the initialization script matches the one in the template (as shown in the next step).
Edit post.hbs
Let's define where we want the table of contents to show up in our theme.
In post.hbs, add the following code snippet right before the {{content}} helper:
<aside class="gh-sidebar"><div class="gh-toc"></div></aside> {{! The TOC will be inserted here }}
handlebars
🔥 Fire up your table of contents
You've made some sweet progress: Tocbot is installed and you’ve hooked it up to your Ghost theme.
As a final check, your default.hbs and post.hbs files should look like this:
{{!< default}}
{{!-- The tag above means: insert everything in this file
into the {body} tag of the default.hbs template --}}
{{#post}}
{{!-- Everything inside the #post block pulls data from the post --}}
<main id="site-main" class="site-main">
<article class="article {{post_class}} {{#match @custom.post_image_style "Full"}}image-full{{else match @custom.post_image_style "=" "Small"}}image-small{{/match}}">
<header class="article-header gh-canvas">
<div class="article-tag post-card-tags">
{{#primary_tag}}
<span class="post-card-primary-tag">
<a href="{{url}}">{{name}}</a>
</span>
{{/primary_tag}}
{{#if featured}}
<span class="post-card-featured">{{> "icons/fire"}} Featured</span>
{{/if}}
</div>
<h1 class="article-title">{{title}}</h1>
{{#if custom_excerpt}}
<p class="article-excerpt">{{custom_excerpt}}</p>
{{/if}}
<div class="article-byline">
<section class="article-byline-content">
<ul class="author-list">
{{#foreach authors}}
<li class="author-list-item">
{{#if profile_image}}
<a href="{{url}}" class="author-avatar">
<img class="author-profile-image" src="{{img_url profile_image size="xs"}}" alt="{{name}}" />
</a>
{{else}}
<a href="{{url}}" class="author-avatar author-profile-image">{{> "icons/avatar"}}</a>
{{/if}}
</li>
{{/foreach}}
</ul>
<div class="article-byline-meta">
<h4 class="author-name">{{authors}}</h4>
<div class="byline-meta-content">
<time class="byline-meta-date" datetime="{{date format="YYYY-MM-DD"}}">{{date}}</time>
{{#if reading_time}}
<span class="byline-reading-time"><span class="bull">•</span> {{reading_time}}</span>
{{/if}}
</div>
</div>
</section>
</div>
{{#match @custom.post_image_style "!=" "Hidden"}}
{{#if feature_image}}
<figure class="article-image">
{{!-- This is a responsive image, it loads different sizes depending on device
https://medium.freecodecamp.org/a-guide-to-responsive-images-with-ready-to-use-templates-c400bd65c433 --}}
<img
srcset="{{img_url feature_image size="s"}} 300w,
{{img_url feature_image size="m"}} 600w,
{{img_url feature_image size="l"}} 1000w,
{{img_url feature_image size="xl"}} 2000w"
sizes="(min-width: 1400px) 1400px, 92vw"
src="{{img_url feature_image size="xl"}}"
alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}"
/>
{{#if feature_image_caption}}
<figcaption>{{feature_image_caption}}</figcaption>
{{/if}}
</figure>
{{/if}}
{{/match}}
</header>
<section class="gh-content gh-canvas">
<aside class="gh-sidebar"><div class="gh-toc"></div></aside> {{! The TOC will be inserted here }}
{{content}}
</section>
{{!--
<section class="article-comments gh-canvas">
If you want to embed comments, this is a good place to paste your code!
</section>
--}}
</article>
</main>
{{!-- A signup call to action is displayed here, unless viewed as a logged-in member --}}
{{#if @site.members_enabled}}
{{#unless @member}}
{{#if access}}
<section class="footer-cta outer">
<div class="inner">
{{#if @custom.email_signup_text}}<h2 class="footer-cta-title">{{@custom.email_signup_text}}</h2>{{/if}}
<a class="footer-cta-button" href="#/portal" data-portal>
<div class="footer-cta-input">Enter your email</div>
<span>Subscribe</span>
</a>
{{!-- ^ This looks like a form element, but it's just a link to Portal,
making the form validation and submission much simpler. --}}
</div>
</section>
{{/if}}
{{/unless}}
{{/if}}
{{!-- Read more links, just above the footer --}}
{{#if @custom.show_recent_posts_footer}}
{{!-- The {#get} helper below fetches some of the latest posts here
so that people have something else to read when they finish this one.
This query gets the latest 3 posts on the site, but adds a filter to
exclude the post we're currently on from being included. --}}
{{#get "posts" filter="id:-{{id}}" limit="3" as |more_posts|}}
{{#if more_posts}}
<aside class="read-more-wrap outer">
<div class="read-more inner">
{{#foreach more_posts}}
{{> "post-card"}}
{{/foreach}}
</div>
</aside>
{{/if}}
{{/get}}
{{/if}}
{{/post}}
handlebarspost.hbs
Summary
You’re now ready to to your Ghost site. Activate your new theme, refresh a post, and watch your own little table-of-contents robot work its magic 🤖
In this tutorial, you installed a third-party JS library, modified a Ghost theme template, and added custom CSS. You’re well on your way to becoming a Ghost pro. Keep on leveling up your Ghost skills by checking out more of our tutorials or head on over to to share how you built your table of contents.