Add a table of contents to Ghost posts
Ghost CMS lacks a native table of contents (ToC) feature. With a small script injected into the site header and footer, a dynamic ToC can be generated automatically for any post or page.
Installation
1. Add CSS Styles
Add the following to Settings > Advanced > Code Injection > Site header:
<style>
/* =========================
Ghost TOC (theme-native)
- transparent background
- inherits text colors from the theme
- uses Ghost accent color only for subtle structure
- TOC title uses theme heading font (serif in your setup)
========================= */
/* Container for the table of contents */
.gh-toc-container {
margin-bottom: 20px; /* Space below the container */
padding: 15px; /* Inner padding */
background: transparent; /* No forced background: let the theme shine through */
color: inherit; /* Inherit text color from the theme */
border-left: 3px solid var(--ghost-accent-color, currentColor); /* Accent-based structure (theme-driven) */
border-radius: 0; /* Card styling should come from the theme, not the TOC */
box-shadow: none; /* No custom shadow: keeps it native */
}
/* Title (H2) of the table of contents
Variant B: the title is a real <h2> without a custom class.
We only enforce the heading font used by the theme (serif in your case). */
.gh-toc-container > h2 {
margin: 0 0 12px 0; /* Space below the title */
font-family: var(--gh-font-heading, var(--font-serif)); /* Match theme heading font (serif setup) */
letter-spacing: -.01em; /* Match theme heading letter spacing */
color: inherit; /* Theme decides the color */
}
/* Main list of the table of contents */
.gh-toc {
list-style: none; /* Removes default bullet points or numbering */
padding-left: 0; /* Removes default indentation */
margin: 0; /* Removes default spacing */
}
/* Top-level list items */
.gh-toc > li {
margin-bottom: 8px; /* Space between main list items */
font-size: 1.05em; /* Slightly larger font size */
font-weight: 600; /* Slightly bolder text for better readability */
}
/* Nested lists (sub-items) */
.gh-toc ul {
padding-left: 18px; /* Indentation for sub-items */
border-left: 2px solid var(--ghost-border-color, currentColor); /* Structure line (theme-driven) */
margin-top: 6px; /* Space above nested lists */
opacity: 0.9; /* Subtle hierarchy without hard colors */
}
/* Styling for sub-list items */
.gh-toc ul li {
font-size: 0.95em; /* Smaller font size for sub-items */
font-weight: 400; /* Normal font weight */
position: relative; /* Positioning for list symbol */
margin-bottom: 6px; /* Space between sub-items */
padding-left: 10px; /* Light indentation for cleaner structure */
}
/* Small circles as bullet points for sub-items */
.gh-toc ul li::before {
content: "•"; /* Bullet point as symbol */
position: absolute; /* Absolute positioning relative to the list item */
left: -12px; /* Positioning left of the text */
color: var(--ghost-accent-color, currentColor); /* Accent bullet (theme-driven) */
font-size: 1.1em; /* Size of the bullet point */
line-height: 1; /* Vertical alignment */
}
/* Styling for links in the table of contents */
.gh-toc a {
text-decoration: none; /* Removes underline */
color: inherit; /* Inherit link color from the surrounding theme text */
transition: color 0.2s ease-in-out; /* Smooth color transition */
}
/* Hover effect for links */
.gh-toc a:hover {
text-decoration: underline; /* Underline on hover */
color: var(--ghost-accent-color, currentColor); /* Use accent color on hover (theme-driven) */
}
</style>
2. Add JavaScript Code
Add the following to Settings > Advanced > Code Injection > Site footer:
<script>
document.addEventListener('DOMContentLoaded', function() {
// Find all placeholders for the table of contents (TOC)
const tocPlaceholders = document.querySelectorAll('.toc-placeholder');
// Translations for the TOC title in different languages
const tocTitles = {
ar: "جدول المحتويات",
de: "Inhaltsverzeichnis",
en: "Table of Contents",
es: "Tabla de contenido",
fr: "Table des matières",
it: "Indice",
ja: "目次",
nl: "Inhoudsopgave",
pl: "Spis treści",
pt: "Índice",
ru: "Оглавление",
zh: "目录",
default: "Table of Contents"
};
// Allowed language codes for detecting language via body classes
const allowedTagLangs = new Set(Object.keys(tocTitles));
// Detects the document language based on CSS classes on the body element
function getLanguageFromBodyClass() {
return [...document.body.classList]
.find(cls => cls.startsWith("tag-hash-") && allowedTagLangs.has(cls.replace("tag-hash-", "")))
?.replace("tag-hash-", "") || null;
}
// Determine the document language
let docLang = getLanguageFromBodyClass() ||
(allowedTagLangs.has(document.documentElement.lang.split("-")[0]) ?
document.documentElement.lang.split("-")[0] :
null) ||
"default";
// Select the TOC title text based on the detected language
let tocTitleText = tocTitles[docLang] || tocTitles["default"];
// Track generated heading IDs so duplicate heading text doesn't collide
const usedIds = new Set();
// Iterate over all TOC placeholders on the page
tocPlaceholders.forEach(tocPlaceholder => {
// Find the main article content container
const articleContainer = document.querySelector(".gh-content");
if (!articleContainer) return;
// Collect all relevant headings
const headings = [...articleContainer.querySelectorAll("h2, h3, h4")];
if (headings.length === 0) return;
// Create the TOC container element
const containerElement = document.createElement("div");
containerElement.className = "gh-toc-container";
// Create the TOC title as a real <h2> (theme styling applies naturally)
const titleElement = document.createElement("h2");
titleElement.textContent = tocTitleText;
containerElement.appendChild(titleElement);
// Create the main TOC list
const tocList = document.createElement("ul");
tocList.className = "gh-toc";
containerElement.appendChild(tocList);
// Initialize variables for managing TOC nesting levels
let lastLevel = 2;
let levelMap = {
2: tocList
};
let currentList = tocList;
// Process each heading and build the TOC structure
headings.forEach(heading => {
let level = parseInt(heading.tagName.substring(1), 10);
// Normalize level jumps (e.g. h2 -> h4 becomes h2 -> h3)
if (level > lastLevel + 1) {
level = lastLevel + 1;
}
// Generate an ID for the heading if none exists, de-duplicating
// when two headings have the same text (e.g. two "Installation" H2s)
if (!heading.id) {
const baseId = heading.textContent
.trim()
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^\w-]/g, "");
let id = baseId;
let suffix = 1;
while (usedIds.has(id)) {
id = `${baseId}-${++suffix}`;
}
usedIds.add(id);
heading.id = id;
}
// Create a list item linking to the heading
const listItem = document.createElement("li");
const link = document.createElement("a");
link.textContent = heading.textContent;
link.href = `#${heading.id}`;
listItem.appendChild(link);
// Move into a nested list when entering a deeper heading level
if (level > lastLevel) {
const parentList = levelMap[lastLevel] || tocList;
let parentLi = parentList.lastElementChild;
if (!parentLi) {
parentLi = document.createElement("li");
parentList.appendChild(parentLi);
}
const nestedList = document.createElement("ul");
parentLi.appendChild(nestedList);
levelMap[level] = nestedList;
currentList = nestedList;
}
else if (level < lastLevel) {
// Move back up to the corresponding list when heading level decreases
currentList = levelMap[level] || tocList;
}
// Append the list item to the current list
currentList.appendChild(listItem);
// Update level tracking
levelMap[level] = currentList;
lastLevel = level;
});
// Insert the completed TOC into the placeholder
tocPlaceholder.appendChild(containerElement);
});
});
</script>
Integration
After adding the injected code, you can add a table of contents to any page by inserting an HTML element with the following contents:
<!-- Table of contents -->
<div class="toc-placeholder"></div>
To simplify usage, you can save the HTML element as a snippet.
Source
Ghost CMS - Table of contents for posts and pages
Ghost CMS lacks a native table of contents (ToC) feature, but with a simple script, you can automatically generate a dynamic ToC for your posts and pages. This guide walks you through the implementation step by step.

