5 min read

Ghost CMS - Post und Page Inhaltsverzeichnis erzeugen

Ghost CMS hat kein natives Inhaltsverzeichnis. Dieser Artikel zeigt, wie man mit CSS und JavaScript ein automatisch generiertes ToC integriert.
Ghost CMS - Post und Page Inhaltsverzeichnis erzeugen

🇬🇧 Read in english

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:

  1. Über den Hashtag des Posts.
  2. Falls kein Hashtag eine bekannte Sprache definiert, wird das html-lang-Element (die globale Systemsprache) verwendet.
  3. 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>
/* =========================
   Ghost TOC (theme-nativ)
   - transparenter Hintergrund
   - Schriftfarben werden vom Theme geerbt
   - Accent-Farbe nur für dezente Struktur
   - TOC-Titel nutzt die Theme-Überschriftenschrift (Serif)
   ========================= */

/* Container für das Inhaltsverzeichnis */
.gh-toc-container {
  margin-bottom: 20px;            /* Abstand unterhalb des TOC */
  padding: 15px;                  /* Innenabstand */

  background: transparent;        /* Kein eigener Hintergrund – Theme entscheidet */
  color: inherit;                 /* Textfarbe vom Theme übernehmen */

  border-left: 3px solid var(--ghost-accent-color, currentColor);
                                   /* Dezente Struktur mit Accent-Farbe des Themes */

  border-radius: 0;               /* Keine Kartenoptik erzwingen */
  box-shadow: none;               /* Kein eigener Schatten */
}

/* Titel des Inhaltsverzeichnisses
   Variante B: echtes <h2> ohne eigene Klasse
   Die Schrift entspricht exakt den Theme-Überschriften (Serif) */
.gh-toc-container > h2 {
  margin: 0 0 12px 0;             /* Abstand unterhalb des Titels */
  font-family: var(--gh-font-heading, var(--font-serif));
                                   /* Überschriftenschrift aus dem Theme */
  letter-spacing: -0.01em;        /* Gleiche Laufweite wie normale Headings */
  color: inherit;                 /* Farbe vom Theme übernehmen */
}

/* Hauptliste des Inhaltsverzeichnisses */
.gh-toc {
  list-style: none;               /* Entfernt Standard-Aufzählungszeichen */
  padding-left: 0;                /* Entfernt Standard-Einzug */
  margin: 0;                      /* Entfernt Standard-Abstände */
}

/* Einträge der obersten Ebene */
.gh-toc > li {
  margin-bottom: 8px;             /* Abstand zwischen Hauptpunkten */
  font-size: 1.05em;              /* Leicht größere Schrift */
  font-weight: 600;               /* Etwas stärker für bessere Lesbarkeit */
}

/* Verschachtelte Listen (Unterpunkte) */
.gh-toc ul {
  padding-left: 18px;             /* Einzug für Unterpunkte */
  border-left: 2px solid var(--ghost-border-color, currentColor);
                                   /* Dezente Trennlinie, theme-gesteuert */
  margin-top: 6px;                /* Abstand oberhalb der Unterliste */
  opacity: 0.9;                   /* Leichte visuelle Abstufung */
}

/* Einträge der Unterebenen */
.gh-toc ul li {
  font-size: 0.95em;              /* Kleinere Schrift für Unterpunkte */
  font-weight: 400;               /* Normale Schriftstärke */
  position: relative;             /* Grundlage für das Bullet-Symbol */
  margin-bottom: 6px;             /* Abstand zwischen Unterpunkten */
  padding-left: 10px;             /* Leichter Einzug für saubere Optik */
}

/* Kleine Punkte als Aufzählungszeichen für Unterpunkte */
.gh-toc ul li::before {
  content: "•";                   /* Punkt als Symbol */
  position: absolute;             /* Position relativ zum Listeneintrag */
  left: -12px;                    /* Links vom Text platzieren */
  color: var(--ghost-accent-color, currentColor);
                                   /* Accent-Farbe des Themes */
  font-size: 1.1em;               /* Größe des Punkts */
  line-height: 1;                 /* Vertikale Ausrichtung */
}

/* Links im Inhaltsverzeichnis */
.gh-toc a {
  text-decoration: none;          /* Keine Unterstreichung */
  color: inherit;                 /* Linkfarbe vom Theme übernehmen */
  transition: color 0.2s ease-in-out;
                                   /* Sanfter Farbwechsel beim Hover */
}

/* Hover-Effekt für Links */
.gh-toc a:hover {
  text-decoration: underline;     /* Unterstreichung beim Hover */
  color: var(--ghost-accent-color, currentColor);
                                   /* Accent-Farbe des Themes beim Hover */
}
</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));

    // Ermittelt die 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;
    }

    // Bestimmt die Sprache des Dokuments
    let docLang = getLanguageFromBodyClass()
        || (allowedTagLangs.has(document.documentElement.lang.split("-")[0])
            ? document.documentElement.lang.split("-")[0]
            : null)
        || "default";

    // Setzt den Titeltext für das Inhaltsverzeichnis
    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 relevanten Überschriften
        const headings = [...articleContainer.querySelectorAll("h2, h3, h4")]
            .filter(h => !h.closest(".m-tags"));
        if (headings.length === 0) return;

        // Erstellt den Container für das Inhaltsverzeichnis
        const containerElement = document.createElement("div");
        containerElement.className = "gh-toc-container";

        // Erstellt die Titel-Überschrift des Inhaltsverzeichnisses
        const titleElement = document.createElement("h2");
        titleElement.textContent = tocTitleText;
        containerElement.appendChild(titleElement);

        // Erstellt die Hauptliste des Inhaltsverzeichnisses
        const tocList = document.createElement("ul");
        tocList.className = "gh-toc";
        containerElement.appendChild(tocList);

        // Initiale Struktur für die Ebenenverwaltung
        let lastLevel = 2;
        let levelMap = { 2: tocList };
        let currentList = tocList;

        // Verarbeitet alle Überschriften und baut die TOC-Struktur auf
        headings.forEach(heading => {
            let level = parseInt(heading.tagName.substring(1), 10);

            // Glättet Ebenensprünge (z.B. h2 -> h4)
            if (level > lastLevel + 1) {
                level = lastLevel + 1;
            }

            // Generiert eine ID, falls keine vorhanden ist
            if (!heading.id) {
                heading.id = heading.textContent
                    .trim()
                    .toLowerCase()
                    .replace(/\s+/g, "-")
                    .replace(/[^\w-]/g, "");
            }

            // Erstellt einen Listeneintrag mit Link zur Überschrift
            const listItem = document.createElement("li");
            const link = document.createElement("a");
            link.textContent = heading.textContent;
            link.href = `#${heading.id}`;
            listItem.appendChild(link);

            // Wechselt bei tieferen Ebenen in eine verschachtelte Liste
            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) {
                // Wechselt bei höheren Ebenen zurück zur passenden Liste
                currentList = levelMap[level] || tocList;
            }

            // Fügt den Eintrag zur aktuellen Liste hinzu
            currentList.appendChild(listItem);

            // Aktualisiert die Ebenenverwaltung
            levelMap[level] = currentList;
            lastLevel = level;
        });

        // Fügt das vollständige Inhaltsverzeichnis 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!