Ghost CMS - Post und Page Inhaltsverzeichnis erzeugen

Warum ein Custom Inhaltsverzeichnis?
Diese Seite basiert auf dem Ghost CMS. Ghost ist Open Source, fabelhaft modular und äußerst anpassbar. Was allerdings fehlt, ist ein Post-Modul für ein Inhaltsverzeichnis. Ein entsprechendes Standard-Modul existiert einfach nicht.
Die Ghost-Webseite beschreibt zwar einen Weg (siehe hier), dieser allerdings bedingt Anpassungen im Theme und ist relativ komplex und nicht Updatefähig einzubauen.
Ein interessanter Reddit-Post (siehe hier) brachte mich auf die Idee, ein modular integrierbares Inhaltsverzeichnis zu erstellen. Es sollte sich mit nur einem Einzeiler in jeden Post einfügen lassen und sich automatisch erstellen sowie aktualisieren. Ich habe die Code-Basis von Reddit übernommen, komplett für Performance und Generalisierung umgeschrieben und entsprechend erweitert und optimiert. Anschließend habe ich noch das StyleSheet so angepasst, dass es die Ghost Akzent-Farben nutzt und eine visuell ansprechendere Einrückung vornimmt.
Danke an NormisB für die Anregung!
Konzeptgedanken
Das Inhaltsverzeichnis nutzt Hashtags in den Posts, um neben der globalen Systemsprache eine postspezifische Sprache definieren zu können (z. B. #de
, #en
).
Die Sprache der Inhaltsverzeichnis-Überschrift wird in drei Stufen bestimmt:
- Über den Hashtag des Posts.
- Falls kein Hashtag eine bekannte Sprache definiert, wird das
html-lang
-Element (die globale Systemsprache) verwendet. - Falls auch hier keine gültige Sprache erkannt wird, wird standardmäßig Englisch genutzt.
Zusätzlich sucht das Inhaltsverzeichnis automatisch nach Überschriften im Post und listet diese hierarchisch auf, basierend auf deren Ebene (h2
, h3
, etc.).
Tag-Listen einiger Ghost-Themes werden ausgeschlossen, da sie in der Regel nicht zum eigentlichen Inhalt gehören.
Eine weitere Besonderheit bildet das beliebte Liebling-Theme, das von den Ghost-Standards bei den CSS-Klassen abweicht. Auch dies wurde berücksichtigt.
Installation
Die Integration des Inhaltsverzeichnisses ist denkbar einfach und erfordert lediglich zwei Schritte:
1. CSS-Styles einfügen
Füge die folgenden CSS-Styles in den <head>
-Bereich deiner Ghost-Seite ein. Dies kannst du über den Admin-Bereich unter Einstellungen → Code Injection → Head tun.
<style>
/* Container für das Inhaltsverzeichnis */
.gh-toc-container {
margin-bottom: 20px; /* Abstand nach unten */
padding: 15px; /* Innenabstand */
background: var(--ghost-accent-bg, #f9f9f9); /* Hintergrundfarbe, kann durch eine CSS-Variable ersetzt werden */
border-left: 4px solid var(--ghost-accent-color, #007acc); /* Linke Seitenlinie als Farbakzent */
border-radius: 6px; /* Abgerundete Ecken */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); /* Leichter Schatten für eine bessere Abhebung */
}
/* Überschrift des Inhaltsverzeichnisses */
.gh-toc-title {
font-size: 1.4em; /* Größere Schriftgröße */
font-weight: bold; /* Fettschrift */
margin-bottom: 12px; /* Abstand zur Liste */
color: var(--ghost-heading-color, #222); /* Farbe der Überschrift */
}
/* Hauptliste des Inhaltsverzeichnisses */
.gh-toc {
list-style: none; /* Entfernt Standard-Punkte oder Zahlen */
padding-left: 0; /* Entfernt Standard-Einrückung */
margin: 0; /* Entfernt Standard-Abstand */
}
/* Erste Ebene der Liste */
.gh-toc > li {
margin-bottom: 8px; /* Abstand zwischen den Hauptelementen */
font-size: 1.05em; /* Leicht vergrößerte Schriftgröße */
font-weight: 600; /* Etwas fettere Schrift für bessere Lesbarkeit */
}
/* Verschachtelte Listen (Unterpunkte) */
.gh-toc ul {
padding-left: 18px; /* Einrückung für Unterpunkte */
border-left: 2px solid var(--ghost-border-color, #ddd); /* Dünne Linie zur besseren Orientierung */
margin-top: 6px; /* Abstand nach oben */
}
/* Stil der untergeordneten Listenelemente */
.gh-toc ul li {
font-size: 0.95em; /* Kleinere Schriftgröße für Unterpunkte */
font-weight: 400; /* Normale Schriftstärke */
position: relative; /* Positionierung für das Listen-Symbol */
margin-bottom: 6px; /* Abstand zwischen Unterpunkten */
padding-left: 10px; /* Leichte Einrückung für eine saubere Struktur */
}
/* Kleine Kreise als Listensymbole für Unterpunkte */
.gh-toc ul li::before {
content: "•"; /* Punkt als Symbol */
position: absolute; /* Absolute Positionierung relativ zum Listenelement */
left: -12px; /* Positionierung links vom Text */
color: var(--ghost-accent-color, #007acc); /* Farbe des Symbols */
font-size: 1.2em; /* Größe des Symbols */
line-height: 1; /* Vertikale Ausrichtung */
}
/* Stil für die Links im Inhaltsverzeichnis */
.gh-toc a {
text-decoration: none; /* Entfernt die Unterstreichung */
color: var(--ghost-text-color, #444); /* Standardfarbe für Links */
transition: color 0.2s ease-in-out, transform 0.1s ease-in-out; /* Sanfter Farb- und Bewegungseffekt */
}
/* Hover-Effekt für Links */
.gh-toc a:hover {
text-decoration: underline; /* Unterstreichung beim Überfahren */
color: var(--ghost-link-hover-color, #005f99); /* Farbe ändert sich beim Überfahren */
transform: translateX(3px); /* Leichte Bewegung nach rechts für eine dynamische Interaktion */
}
</style>
2. JavaScript-Code hinzufügen
Ebenso muss der folgende JavaScript-Code in den Footer-Bereich eingefügt werden (unter Einstellungen → Code Injection → Footer).
<script>
document.addEventListener('DOMContentLoaded', function () {
// Alle Platzhalter für das Inhaltsverzeichnis finden
const tocPlaceholders = document.querySelectorAll('.toc-placeholder');
// Übersetzungen für den Titel des Inhaltsverzeichnisses in verschiedenen Sprachen
const tocTitles = {
de: "Inhaltsverzeichnis", fr: "Table des matières", es: "Tabla de contenido",
it: "Indice", nl: "Inhoudsopgave", pl: "Spis treści", pt: "Índice",
ru: "Оглавление", zh: "目录", ja: "目次", ar: "جدول المحتويات",
en: "Table of Contents", default: "Table of Contents"
};
// Erlaubte Sprachcodes für die Klassenerkennung im body-Tag
const allowedTagLangs = new Set(Object.keys(tocTitles));
// Funktion zur Ermittlung der Sprache anhand von CSS-Klassen im body-Tag
function getLanguageFromBodyClass() {
return [...document.body.classList]
.find(cls => cls.startsWith("tag-hash-") && allowedTagLangs.has(cls.replace("tag-hash-", "")))
?.replace("tag-hash-", "") || null;
}
// Bestimmung der Sprache des Dokuments mit Priorität:
// 1) Falls eine gültige `tag-hash-XX` Klasse existiert, wird diese verwendet
// 2) Falls nicht, wird das <html lang="XX"> Attribut geprüft
// 3) Falls keine Sprache bestimmt werden kann, wird Englisch als Standard verwendet
let docLang = getLanguageFromBodyClass()
|| (allowedTagLangs.has(document.documentElement.lang.split("-")[0]) ? document.documentElement.lang.split("-")[0] : null)
|| "default";
// Setzt den passenden Titel für das Inhaltsverzeichnis basierend auf der ermittelten Sprache
let tocTitleText = tocTitles[docLang] || tocTitles["default"];
// Durchläuft alle TOC-Platzhalter auf der Seite
tocPlaceholders.forEach(tocPlaceholder => {
// Sucht den Hauptinhalt des Artikels
const articleContainer = document.querySelector(".gh-content") || document.querySelector(".l-post-content");
if (!articleContainer) return;
// Sammelt alle Überschriften (h2, h3, h4), ignoriert aber welche, die innerhalb von `.m-tags` liegen
const headings = [...articleContainer.querySelectorAll("h2, h3, h4")].filter(h => !h.closest(".m-tags"));
if (headings.length === 0) return;
// Erstellt das Container-Element für das Inhaltsverzeichnis
const containerElement = document.createElement("div");
containerElement.className = "gh-toc-container";
// Erstellt die Titel-Überschrift des TOC
const titleElement = document.createElement("h2");
titleElement.className = "gh-toc-title";
titleElement.textContent = tocTitleText;
containerElement.appendChild(titleElement);
// Erstellt die Hauptliste für das Inhaltsverzeichnis
const tocList = document.createElement("ul");
tocList.className = "gh-toc";
containerElement.appendChild(tocList);
// Initialisierung von Variablen zur Verwaltung der Listenstruktur
let lastLevel = 2;
let levelMap = { 2: tocList };
let currentList = tocList;
// Verarbeitung der Überschriften und Erstellen der TOC-Struktur
headings.forEach(heading => {
const level = parseInt(heading.tagName.substring(1));
// Falls die Überschrift keine ID hat, wird eine generiert
if (!heading.id) {
heading.id = heading.textContent.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]/g, "");
}
// Erstellt ein Listenelement mit einem Link zur entsprechenden Überschrift
const listItem = document.createElement("li");
const link = document.createElement("a");
link.textContent = heading.textContent;
link.href = `#${heading.id}`;
listItem.appendChild(link);
// Falls die Überschrift eine tiefere Ebene als die vorherige hat, wird eine verschachtelte Liste erstellt
if (level > lastLevel) {
const nestedList = document.createElement("ul");
levelMap[lastLevel].lastElementChild.appendChild(nestedList);
levelMap[level] = nestedList;
currentList = nestedList;
} else if (level < lastLevel) {
// Falls die Überschrift eine höhere Ebene hat, wird zur entsprechenden Liste zurückgewechselt
currentList = levelMap[level] || tocList;
}
// Fügt das Listenelement zur aktuellen Liste hinzu
currentList.appendChild(listItem);
levelMap[level] = currentList;
lastLevel = level;
});
// Fügt das komplette TOC-Element in den Platzhalter ein
tocPlaceholder.appendChild(containerElement);
});
});
</script>
Integration in einen Post
Sobald die Installation abgeschlossen ist, kannst du das Inhaltsverzeichnis in beliebigen Posts oder Seiten nutzen. Dafür fügst du einfach folgendes HTML-Element an die gewünschte Stelle ein:
<!-- Inhaltsverzeichnis -->
<div class="toc-placeholder"></div>
Beim Laden der Seite wird automatisch ein Inhaltsverzeichnis an dieser Stelle gerendert. Die Überschrift erscheint in der entsprechenden Sprache, und durch Klicken auf einen Eintrag springt der Nutzer direkt zur entsprechenden Stelle im Post.
Erstellen eines benutzerdefinierten Snippets
Nach dem Einfügen des HTML-Elements kann man auf die "Als Snippet speichern"-Funktion klicken und einen Namen vergeben:

Danach kann man den Block einfach in jedem Beitrag durch Klicken hinzufügen:

Mit dieser Methode lässt sich ein flexibles, leicht integrierbares und automatisch aktualisierendes Inhaltsverzeichnis für Ghost realisieren. Viel Spaß beim Testen!
Member discussion