JavaScript Paste in: Footer

Auto-Generated Table of Contents

Automatically generate a sticky table of contents from your Ghost post headings with smooth scroll and active section highlighting.

Auto-Generated Table of Contents

Add an auto-generated table of contents to your Ghost posts that builds itself from your H2 and H3 headings. Includes smooth scrolling, active section highlighting, and a collapsible toggle for mobile.

The Code

Paste this into your Ghost site’s Settings → Code injection → Site footer:

<script>
(function() {
var article = document.querySelector('.gh-content, .post-content, article');
if (!article) return;
var headings = article.querySelectorAll('h2, h3');
if (headings.length < 3) return;
// Add IDs to headings that don't have them
var usedIds = {};
headings.forEach(function(h) {
if (!h.id) {
var slug = h.textContent.trim().toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
if (usedIds[slug]) {
slug = slug + '-' + usedIds[slug];
}
usedIds[slug] = (usedIds[slug] || 0) + 1;
h.id = slug;
}
});
// Build TOC
var nav = document.createElement('nav');
nav.id = 'toc';
nav.setAttribute('aria-label', 'Table of contents');
var toggleBtn = document.createElement('button');
toggleBtn.textContent = 'Contents';
toggleBtn.setAttribute('aria-expanded', 'true');
toggleBtn.style.cssText =
'display:block;width:100%;padding:10px 16px;background:none;border:none;' +
'font-weight:600;font-size:14px;cursor:pointer;text-align:left;color:inherit;';
var list = document.createElement('ol');
list.style.cssText =
'list-style:none;padding:0;margin:0;';
headings.forEach(function(h) {
var li = document.createElement('li');
var a = document.createElement('a');
a.href = '#' + h.id;
a.textContent = h.textContent;
a.style.cssText =
'display:block;padding:4px 16px;font-size:13px;color:inherit;' +
'text-decoration:none;opacity:0.7;transition:opacity 0.2s;' +
'border-left:2px solid transparent;';
if (h.tagName === 'H3') {
a.style.paddingLeft = '32px';
a.style.fontSize = '12px';
}
a.addEventListener('mouseenter', function() { a.style.opacity = '1'; });
a.addEventListener('mouseleave', function() {
if (!a.classList.contains('active')) a.style.opacity = '0.7';
});
a.addEventListener('click', function(e) {
e.preventDefault();
document.getElementById(h.id).scrollIntoView({ behavior: 'smooth' });
history.pushState(null, '', '#' + h.id);
});
li.appendChild(a);
list.appendChild(li);
});
toggleBtn.addEventListener('click', function() {
var expanded = toggleBtn.getAttribute('aria-expanded') === 'true';
toggleBtn.setAttribute('aria-expanded', String(!expanded));
list.style.display = expanded ? 'none' : 'block';
});
nav.appendChild(toggleBtn);
nav.appendChild(list);
nav.style.cssText =
'position:fixed;top:100px;right:20px;width:220px;max-height:calc(100vh - 140px);' +
'overflow-y:auto;background:var(--background-color,#fff);' +
'border:1px solid rgba(0,0,0,0.1);border-radius:8px;' +
'box-shadow:0 2px 8px rgba(0,0,0,0.08);z-index:100;' +
'font-family:inherit;';
document.body.appendChild(nav);
// Active section highlighting
var links = list.querySelectorAll('a');
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
links.forEach(function(link) {
link.classList.remove('active');
link.style.opacity = '0.7';
link.style.borderLeftColor = 'transparent';
});
var active = list.querySelector('a[href="#' + entry.target.id + '"]');
if (active) {
active.classList.add('active');
active.style.opacity = '1';
active.style.borderLeftColor = 'var(--ghost-accent-color,#3b82f6)';
}
}
});
}, { rootMargin: '-80px 0px -80% 0px' });
headings.forEach(function(h) { observer.observe(h); });
// Hide on small screens
var mq = window.matchMedia('(max-width: 1280px)');
function handleResize(e) { nav.style.display = e.matches ? 'none' : 'block'; }
handleResize(mq);
mq.addEventListener('change', handleResize);
})();
</script>

How to Install

  1. In Ghost Admin, go to Settings → Code injection
  2. Paste the code into the Site footer section
  3. Click Save
  4. Open any post with 3+ headings — the TOC appears on the right side

The TOC only renders on posts with 3 or more H2/H3 headings. Posts with fewer headings won’t show it.

How It Works

The script scans the post content for h2 and h3 elements and builds a navigation list. If a heading doesn’t already have an id attribute, the script generates a URL-friendly slug from the heading text (e.g., “Setting Up Email” becomes #setting-up-email). This produces human-readable anchor links that work in shared URLs. An IntersectionObserver tracks which heading is currently visible and highlights the corresponding TOC link with a left border accent.

On screens narrower than 1280px, the TOC hides automatically to avoid overlapping the content. The toggle button lets readers collapse the TOC on desktop if they prefer a clean reading view.

Smooth scrolling uses the native scrollIntoView API with behavior: 'smooth' — no jQuery or external libraries needed.

Customization

Change position: Modify right:20px to left:20px for a left-aligned TOC. Adjust top:100px if your theme’s header is taller.

Change breakpoint: Replace 1280px with a different value to control when the TOC hides. Use 1024px for a more aggressive hide, or 1440px to show it on wider screens only.

Change accent color: The active link uses var(--ghost-accent-color,#3b82f6). Override the fallback color (#3b82f6) with your brand color if your theme doesn’t set --ghost-accent-color.