4 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.

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, sie an vielen Stellen für Performance und Generalisierung umgeschrieben und entsprechend erweitert und optimiert.


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>
  /* 📌 Container für das Inhaltsverzeichnis */
  .gh-toc-container {
    margin-bottom: 20px;
    padding: 15px;
    background-color: var(--ghost-accent-bg, #f9f9f9); /* Hintergrundfarbe */
    border-left: 4px solid var(--ghost-accent-color, #007acc); /* Farbakzent an der Seite */
    border-radius: 4px; /* Abgerundete Ecken */
  }

  /* 📌 Überschrift des Inhaltsverzeichnisses */
  .gh-toc-title {
    font-size: 1.5em;
    font-weight: bold;
    margin-bottom: 10px;
    color: var(--ghost-heading-color, #333); /* Standard-Überschriftenfarbe */
  }

  /* 📌 Liste des Inhaltsverzeichnisses */
  .gh-toc ul {
    list-style: none; /* Keine Standard-Punkte */
    padding-left: 0;
    margin: 0;
  }

  /* 📌 Verschachtelte Listen für Unterpunkte */
  .gh-toc ul ul {
    padding-left: 20px; /* Einrückung für Unterpunkte */
  }

  /* 📌 Listenelemente */
  .gh-toc li {
    margin-bottom: 5px; /* Abstand zwischen Einträgen */
    line-height: 1.6; /* Bessere Lesbarkeit */
  }

  /* 📌 Links im Inhaltsverzeichnis */
  .gh-toc a {
    text-decoration: none; /* Keine Unterstreichung */
    color: inherit; /* Textfarbe vom Theme übernehmen */
    font-size: inherit; /* Gleiche Schriftgröße wie der Rest */
  }

  /* 📌 Hover-Effekt für Links */
  .gh-toc a:hover {
    text-decoration: underline; /* Unterstrichen beim Überfahren */
    color: var(--ghost-link-hover-color, #005f99); /* Farbe 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 () {
        const tocPlaceholders = document.querySelectorAll('.toc-placeholder');

        // Sprachabhängige Titel für das Inhaltsverzeichnis
        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 tag-hash-XX
        const allowedTagLangs = new Set(Object.keys(tocTitles));

        // Funktion zur Ermittlung der Sprache aus der body-Klasse
        function getLanguageFromBodyClass() {
            const bodyClassList = document.body.classList;
            for (const className of bodyClassList) {
                if (className.startsWith('tag-hash-')) {
                    let langCode = className.replace('tag-hash-', ''); // 'tag-hash-de' → 'de'
                    if (allowedTagLangs.has(langCode)) {
                        return langCode; // Nur wenn es eine erlaubte Sprache ist
                    }
                }
            }
            return null;
        }

        // Bestimme die Sprache:
        // 1) Falls eine gültige `tag-hash-XX` Klasse existiert, nutze diese
        // 2) Falls nicht, nutze das <html lang="XX"> Attribut (falls gültig)
        // 3) Falls nichts gefunden wird, Standard: Englisch
        let docLang = getLanguageFromBodyClass()
            || (allowedTagLangs.has(document.documentElement.lang.split('-')[0])
                ? document.documentElement.lang.split('-')[0]
                : null)
            || 'default';

        let tocTitleText = tocTitles[docLang] || tocTitles['default'];

        tocPlaceholders.forEach(tocPlaceholder => {
            // TOC-Container erstellen
            const containerElement = document.createElement("div");
            containerElement.setAttribute("class", "gh-toc-container");

            // Titel basierend auf Sprache setzen
            const titleElement = document.createElement("h2");
            titleElement.textContent = tocTitleText;
            titleElement.setAttribute("class", "gh-toc-title");
            containerElement.appendChild(titleElement);

            // Hauptliste für das TOC
            const tocList = document.createElement("ul");
            tocList.setAttribute("class", "gh-toc");

            // Artikel-Container suchen
            const articleContainer = document.querySelector('.gh-content') || document.querySelector('.l-post-content');
            if (!articleContainer) return;

            // H2, H3, H4 suchen, aber .m-tags ausschließen
            const headings = [...articleContainer.querySelectorAll('h2, h3, h4')]
                .filter(heading => !heading.closest('.m-tags'));

            if (headings.length === 0) return;

            // Helferfunktion zur Ermittlung des Überschriften-Levels
            function getHeadingLevel(tagName) {
                return parseInt(tagName.replace('H', ''), 10);
            }

            let currentList = tocList;
            let lastLevel = 2;

            headings.forEach(heading => {
                const level = getHeadingLevel(heading.tagName);

                // ID für Ankerlink setzen, falls nicht vorhanden
                if (!heading.id) {
                    heading.id = heading.textContent.trim()
                        .toLowerCase()
                        .replace(/\s+/g, '-')
                        .replace(/[^\w-]/g, '');
                }

                // Neues Listenelement mit Link erstellen
                const listItem = document.createElement('li');
                const link = document.createElement('a');
                link.textContent = heading.textContent;
                link.href = `#${heading.id}`;
                listItem.appendChild(link);

                // Verschachtelung anpassen
                if (level > lastLevel) {
                    const nestedList = document.createElement('ul');
                    if (currentList.lastChild) {
                        currentList.lastChild.appendChild(nestedList);
                        currentList = nestedList;
                    }
                } else if (level < lastLevel) {
                    while (level < lastLevel && currentList.parentNode.closest('ul')) {
                        currentList = currentList.parentNode.closest('ul');
                        lastLevel--;
                    }
                }

                currentList.appendChild(listItem);
                lastLevel = level;
            });

            containerElement.appendChild(tocList);
            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!