Realtime-Attack-Map
SURICATA / LIVE FEED

Automatisiertes Best-of-Menü im Ghost-Blogging-System

Detaillierte Anleitung für ein Featured-Posts-Menü über die Content API von Ghost.

Automatisiertes Best-of-Menü im Ghost-Blogging-System

Oftmals entsteht der Bedarf, die besonders lesenswerten Perlen eines Blogs prominent für die Leserschaft in Szene zu setzen. Ein statisches Menü stößt durch den ständigen manuellen Pflegeaufwand schnell an seine Grenzen, weshalb eine dynamische Lösung über die Ghost Content API wesentlich eleganter ist. Ich dachte mir, ich zeige einmal auf, wie sich ein sich selbst aktualisierendes Dropdown-Menü für hervorgehobene Artikel nahtlos und unkompliziert in Ghost integrieren lässt.

Die Vorbereitung der Ghost API

Um automatisiert auf die eigenen Beiträge zugreifen zu können, wird im ersten Schritt ein API-Schlüssel benötigt. Dazu wird in der Ghost-Verwaltungsoberfläche unter den Einstellungen im Bereich Integrations eine neue benutzerdefinierte Integration angelegt, beispielsweise mit dem Namen "Featured Articles". Dort lässt sich anschließend der sogenannte Content API Key entnehmen.

Dieser Schlüssel besitzt rein lesende Rechte und kann daher absolut sicher im Frontend-Code verwendet werden, ohne ein Sicherheitsrisiko darzustellen. Wer sich für die allgemeine Konfiguration und Bereitstellung solcher Systeme interessiert, findet wertvolle Details in der Anleitung, wie ich meine Blogs online stelle.

Gestaltung des Menüs im Site-Header

Das visuelle Fundament des Dropdowns wird mittels Cascading Style Sheets (CSS) gelegt. Um das Original-Theme des Blogs nicht verändern zu müssen, wird der Code bequem über die Weboberfläche injiziert. Unter Settings -> Code Injection wird der folgende CSS-Block in das Feld Site Header eingefügt.

Hierbei kommt ein moderner Glas-Effekt ("Glassmorphismus") zum Einsatz. Durch die definierten Variablen im Kopf des Skripts lässt sich das Design mühelos an das eigene Corporate Design anpassen. Da das Individualisieren von Ghost ohnehin ein essenzieller Bestandteil der Administration ist, bietet dieser Weg maximale Flexibilität bei minimaler Invasivität.

<style>
    /* ==========================================================================
       EINSTELLUNGEN (Hier lassen sich alle Werte bequem anpassen)
       ========================================================================== */
    :root {
        --bestof-font-size-links: 16px;        /* Schriftgröße der Beiträge (z.B. 16px oder 18px) */
        --bestof-font-size-heading: 14px;      /* Schriftgröße der neuen Überschrift */
        --bestof-font-size-menu: 16px;         /* Schriftgröße für den "Best of"-Button oben im Menü */
        
        --bestof-max-height: 70vh;             /* Maximale Ausklapphöhe (70vh = 70% der Bildschirmhöhe) */
        
        --bestof-bg-color: rgba(13, 27, 42, 0.85); /* Glas-Hintergrund (Tiefblau, halbtransparent) */
        --bestof-blur-amount: 12px;            /* Stärke des Milchglas-Effekts */
        
        --bestof-text-color: rgba(255, 255, 255, 0.85); /* Textfarbe der Links */
        --bestof-heading-color: #8fa3b0;       /* Farbe der Überschrift (dezentes Blaugrau) */
        
        --bestof-hover-bg: rgba(255, 255, 255, 0.1);  /* Hintergrund bei Maus-Fokus */
        --bestof-hover-color: #ffffff;         /* Textfarbe bei Maus-Fokus */
    }

    /* ==========================================================================
       STYLES (Ab hier nichts mehr ändern, greift auf die Variablen oben zu)
       ========================================================================== */

    /* Der Toggle-Link im Menü */
    .featured-toggle-btn {
        display: inline-flex !important;
        align-items: center;
        gap: 6px;
        background: transparent !important;
        border: none !important;
        color: inherit !important;
        font-weight: 500;
        cursor: pointer;
        padding: 5px 10px;
        text-decoration: none !important;
        font-size: var(--bestof-font-size-menu) !important;
        transition: opacity 0.2s ease;
    }
    
    .featured-toggle-btn:hover {
        opacity: 0.7;
    }

    /* Der Dropdown-Container mit Glas-Effekt */
    #featured-posts-collapse {
        position: absolute;
        top: 100%;
        left: 0;
        width: 100%;
        max-height: 0;
        overflow-y: hidden;
        
        background-color: var(--bestof-bg-color);
        -webkit-backdrop-filter: blur(var(--bestof-blur-amount));
        backdrop-filter: blur(var(--bestof-blur-amount));
        
        transition: max-height 0.4s ease-out, padding 0.4s ease-out;
        z-index: 9999;
        box-shadow: 0 15px 30px rgba(0,0,0,0.3);
        
        padding: 0 4vmin; 
        box-sizing: border-box;
    }

    /* Zustand, wenn aufgeklappt */
    #featured-posts-collapse.is-open {
        max-height: var(--bestof-max-height);
        overflow-y: auto;
        padding: 24px 4vmin;
        border-bottom: 1px solid rgba(255,255,255,0.1);
        border-top: 1px solid rgba(255,255,255,0.05);
    }

    /* Scrollbar Design */
    #featured-posts-collapse::-webkit-scrollbar { width: 8px; }
    #featured-posts-collapse::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); }
    #featured-posts-collapse::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 4px; }
    #featured-posts-collapse::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.4); }

    /* Innere Ausrichtung */
    .featured-posts-inner {
        max-width: 1200px;
        margin: 0 auto;
        display: flex;
        flex-direction: column;
        gap: 8px;
    }

    /* Die neue Überschrift über den Posts */
    .bestof-heading {
        font-size: var(--bestof-font-size-heading) !important;
        color: var(--bestof-heading-color);
        text-transform: uppercase;
        letter-spacing: 0.5px;
        font-weight: 600;
        padding: 0 14px;
        margin-bottom: 8px;
        margin-top: 4px;
    }

    /* Design der einzelnen Links */
    .featured-post-link {
        text-decoration: none !important;
        color: var(--bestof-text-color) !important;
        font-weight: 500;
        font-size: var(--bestof-font-size-links) !important; 
        line-height: 1.5;
        padding: 10px 14px;
        border-radius: 6px;
        transition: all 0.2s ease;
        display: block;
    }

    .featured-post-link:hover {
        background-color: var(--bestof-hover-bg) !important;
        color: var(--bestof-hover-color) !important;
        transform: translateX(5px);
    }
</style>

Die technische Magie entfaltet sich durch ein kompaktes JavaScript, welches direkt in das Feld Site Footer eingefügt wird. Das Skript identifiziert die bestehende Navigationsebene und konstruiert vollautomatisch den Toggle-Button. Anschließend fragt es im Hintergrund asynchron die Ghost Content API ab und filtert gezielt nach Beiträgen, die im Backend als featured markiert wurden.

Damit die Kommunikation zur API erfolgreich ist, muss der weiter oben erstellte API-Schlüssel zwingend in der Variable apiKey im Skript hinterlegt werden. Sollte ein Fehler auftreten, beispielsweise durch einen inkorrekten Key, sorgt das Skript für ein robustes Fallback und visualisiert das Problem innerhalb des Dropdowns.

<script>
// Autor: M. Meister - Dynamisches Best-Of Dropdown via Ghost Content API
<script>
document.addEventListener('DOMContentLoaded', function() {
    const siteHeader = document.querySelector('.gh-head') || document.querySelector('header');
    if (!siteHeader) return;

    const actionsContainer = document.querySelector('.gh-head-actions');
    
    // Toggle-Button erstellen
    const toggleBtn = document.createElement('a');
    toggleBtn.href = '#';
    toggleBtn.className = 'featured-toggle-btn';
    toggleBtn.innerHTML = 'Best of <span class="toggle-icon">▼</span>';
    
    // Button einfügen
    if (actionsContainer) {
        actionsContainer.insertBefore(toggleBtn, actionsContainer.firstChild);
    } else {
        const headInner = document.querySelector('.gh-head-inner') || siteHeader;
        headInner.appendChild(toggleBtn);
    }

    // Täusche Ghost vor, dass sich die Fenstergröße geändert hat,
    // damit das Theme den Platz für die "..."" sofort neu und korrekt berechnet.
    setTimeout(function() {
        window.dispatchEvent(new Event('resize'));
    }, 250);

    // Dropdown-Container erstellen
    const collapseContainer = document.createElement('div');
    collapseContainer.id = 'featured-posts-collapse';
    
    collapseContainer.innerHTML = `
        <div class="featured-posts-inner">
            <div class="bestof-heading">Von Usern am besten bewertete Beiträge:</div>
            <div class="featured-post-link" style="color: rgba(255,255,255,0.5);">Lade Beiträge...</div>
        </div>
    `;
    siteHeader.appendChild(collapseContainer);

    // =====================================================================
    // HIER DEN CONTENT API KEY EINTRAGEN:
    // =====================================================================
    const apiKey = '41854ce0140c459f8f088cb759'; 
    
    fetch(`/ghost/api/content/posts/?key=${apiKey}&filter=featured:true&limit=all&fields=title,url`)
        .then(response => response.json())
        .then(data => {
            let html = '<div class="featured-posts-inner">';
            html += '<div class="bestof-heading">Von Usern am besten bewertete Beiträge:</div>';
            
            if (data.posts && data.posts.length > 0) {
                data.posts.forEach(post => {
                    html += `<a class="featured-post-link" href="${post.url}">► ${post.title}</a>`;
                });
            } else {
                html += '<div class="featured-post-link">Keine markierten Beiträge gefunden.</div>';
            }
            html += '</div>';
            
            collapseContainer.innerHTML = html;
        })
        .catch(error => {
            console.error('Fehler beim Laden der Featured Posts:', error);
            collapseContainer.innerHTML = `
                <div class="featured-posts-inner">
                    <div class="bestof-heading">Von Usern am besten bewertete Beiträge:</div>
                    <div class="featured-post-link" style="color: #ffb8b8;">Fehler beim Laden (Ist der API Key korrekt?)</div>
                </div>
            `;
        });

    // Klick-Logik zum Auf- und Zuklappen
    toggleBtn.addEventListener('click', function(e) {
        e.preventDefault();
        const isOpen = collapseContainer.classList.toggle('is-open');
        const icon = toggleBtn.querySelector('.toggle-icon');
        icon.innerHTML = isOpen ? '▲' : '▼';
    });
    
    // Schließen, wenn man irgendwo anders hinklickt
    document.addEventListener('click', function(e) {
        if (!siteHeader.contains(e.target)) {
            collapseContainer.classList.remove('is-open');
            const icon = toggleBtn.querySelector('.toggle-icon');
            if(icon) icon.innerHTML = '▼';
        }
    });
});
</script>

Welche Posts werden ausgewählt

Hier werfe ich aktuell selbst einen Blick auf meine Analysen. Einerseits erhalte ich einen extrem guten Einblick durch rybbit.

Rybbit: Die Self-Hosted Analytics-Revolution
Eine Anleitung zur Installation der Analytics-Alternative Rybbit

Andererseits sehe ich das auch in meinem Security-Dashboard in Grafana.

Ob ich das automatisieren möchte, überlege ich derzeit noch. Es kommen immer mal wieder nette eMails oder linkedin Nachrichten, in denen Leser ihre Freude über bestimmte Themen zum Ausdruck bringen. Das fließt natürlich auch in meine Auswahl mit ein.

Der Vorgang in Ghost ist dann relativ einfach. Im Editor des Artikels wird lediglich ein Schalter umgelegt:

Auf der Webseite werden diese Artikel dann mit einem "Hot"-Symbol markiert:

Insgesamt beschränkt sich der Extra-Aufwand nach der einmaligen Einrichtung auf das Setzen eines Schalters.

Fazit

Mit nur wenigen Handgriffen und dem gezielten Einsatz von JavaScript wurde ein vollautomatisches Menü erschaffen, das ganz ohne Modifikationen an den Theme-Dateien auskommt. Neue Artikel, die im Ghost-Backend fortan mit einem Sternchen hervorgehoben werden, erscheinen ab sofort in Echtzeit im Dropdown. Das Resultat ist eine erhebliche Aufwertung der Benutzererfahrung und das bei einem zukünftigen Wartungsaufwand von nahezu null.