Realtime-Attack-Map
SURICATA / LIVE FEED

Ghost individualisieren mit Code Injection

Code Injection in Ghost: Von leuchtendem Syntax-Highlighting bis zur D3.js-Weltangriffskarte.

Ghost individualisieren mit Code Injection

Ghost bietet mit seiner Code-Injection-Funktion eine elegante Möglichkeit, das Erscheinungsbild und die Funktionalität eines Blogs tief anzupassen, ohne auch nur eine Zeile des eigentlichen Themes anfassen zu müssen. Mit der jüngsten Erweiterung des Site-Header-Codes ist der Meister-Security-Blog um drei neue Effekte reicher: einen Glitch-Effekt auf dem Menü-Titel, einen Glassmorphismus-Header und vollständig neu gestaltete Cyber-Code-Blöcke mit Laser-Entrance-Animation und Maus-Tracking. Was sich technisch dahinter verbirgt, wird hier Schicht für Schicht aufgedröselt.

Ghost Code Injection ohne eigenes Theme

Ghost erlaubt es im Adminbereich unter Settings > Code Injection, beliebigen HTML-, CSS- und JavaScript-Code in den <head> jeder Seite einzuschleusen. Das klingt zunächst nach einer Nischenfunktion, ist in der Praxis aber ungemein mächtig: Statt ein Theme zu forken, versionieren und pflegen zu müssen, reichen wenige Zeilen Code, um den Blog grundlegend zu verändern.

Das Prinzip ist simpel: Alles, was unter Site Header eingetragen wird, landet verbatim im <head> jeder ausgelieferten Seite. Externe Stylesheets lassen sich laden, CSS-Variablen überschreiben, eigene Schriften einbinden, Analytics-Tags setzen und ganze JavaScript-Applikationen starten. Das ist keine Hintertür, sondern ein explizit vorgesehenes Feature für genau diesen Zweck.

Syntax-Highlighting mit Prism.js

Das erste Element im Injection-Block lädt das Dark-Theme von Prism.js über das Cloudflare CDN:

<link rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.28.0/themes/prism-tomorrow.min.css"
  integrity="sha512-vswe+cgvic/XBoF1OcM/TeJ2FW0OofqAVdCZiEYkd6dwGXthvkSFWOoGGJgS2CW70VK5dQM5Oh+7ne47s74VTg=="
  crossorigin="anonymous" referrerpolicy="no-referrer" />

Das integrity-Attribut enthält einen Subresource Integrity (SRI)-Hash. Der Browser prüft damit, ob die geladene Datei exakt mit dem erwarteten Hash übereinstimmt. Wurde die Datei auf dem CDN manipuliert, verweigert der Browser das Laden, bevor auch nur eine Zeile des Stylesheets wirksam wird - ein eleganter Supply-Chain-Schutz mit einer einzigen Zeile HTML.

CSS-Grid und Layout-Anpassungen

Das Ghost-Casper-Theme legt seinen Inhaltsfluss über ein mehrspaltiges CSS-Grid. Durch das Überschreiben der Custom Properties greifen eigene Werte direkt in dieses Grid ein:

:root {
    --outer-padding: max(4vmin, 20px);
    --inner-padding: max(8vmin, 40px);
    --wide-max: 240px;
    --main-max: 1000px; /* Maximale Breite des Hauptinhalts */
}

.gh-canvas {
    display: grid;
    grid-template-columns:
        [full-start] minmax(var(--outer-padding), auto)
        [wide-start] minmax(auto, var(--wide-max))
        [main-start] min(var(--main-max), calc(100% - var(--inner-padding)))
        [main-end]   minmax(auto, var(--wide-max))
        [wide-end]   minmax(var(--outer-padding), auto)
        [full-end];
}

Die Variable --main-max: 1000px begrenzt die Lesebreite auf einen angenehmen Wert, ohne dass das Layout bricht. Die max()-Funktion für die Paddings sorgt dafür, dass auf kleinen Bildschirmen immer mindestens 20px bzw. 40px Abstand verbleiben, während auf großen Displays der Wert proportional zur Viewport-Höhe skaliert.

Glitch-Effekt auf dem Menü-Titel

Der auffälligste neue Effekt ist ein kurz aufflackernder Glitch auf dem Menü-Titel - der Blog-Name in der Navigationsleiste zuckt in unregelmäßigen Abständen kurz auf, erzeugt durch versetzte Cyan- und Magenta-Schichten im Stil alter Kathodenstrahlröhren-Monitore.

Wie es funktioniert: CSS-Pseudoelemente ::before und ::after legen zwei exakte Textkopien über den Originaltitel. Beide Kopien werden mit clip-path: inset() auf schmale horizontale Streifen zugeschnitten und per text-shadow um wenige Pixel verschoben - ::before nach links in Cyan (#0ff), ::after nach rechts in Magenta (#f0f). Kurze @keyframes-Animationen verschieben diese Schnitte in schneller Folge, was den typischen chromatic-aberration-Look erzeugt:

.gh-head-logo.glitch::before {
    left: -3px;
    text-shadow: 2px 0 #0ff;
    clip-path: inset(20% 0 80% 0);
    animation: menu-glitch-1 0.2s infinite linear alternate-reverse;
}

.gh-head-logo.glitch::after {
    left: 3px;
    text-shadow: -2px 0 #f0f;
    clip-path: inset(80% 0 20% 0);
    animation: menu-glitch-2 0.3s infinite linear alternate-reverse;
}

Die Klasse .glitch wird per JavaScript nur für 100 bis 300 Millisekunden gesetzt und danach wieder entfernt. Der nächste Glitch folgt nach einer zufälligen Wartezeit zwischen 1,5 und 2,5 Sekunden. Das Ergebnis: ein Effekt, der subtil genug ist, um nicht zu nerven, aber deutlich genug, um wahrgenommen zu werden - vorausgesetzt, man schaut zum richtigen Zeitpunkt hin.

function triggerMenuGlitch() {
    const glitchDuration = Math.random() * 200 + 100; // 100–300ms
    menuTitle.classList.add('glitch');
    setTimeout(() => menuTitle.classList.remove('glitch'), glitchDuration);
}

function startMenuGlitchIntervals() {
    const timeToNextGlitch = Math.random() * 1000 + 1500; // 1,5–2,5s
    setTimeout(() => {
        triggerMenuGlitch();
        startMenuGlitchIntervals(); // rekursiv
    }, timeToNextGlitch);
}

Wichtig: Das data-text-Attribut wird per JavaScript auf den Textwert des Elements gesetzt, da CSS content: attr(data-text) benötigt, um den Text in den Pseudoelementen darzustellen. Enthält der Menü-Titel ein <img> statt Text (Logo-Bild), wird der gesamte Effekt übersprungen.

Glassmorphismus-Navigationsleiste

Zum Glitch-Effekt gesellt sich ein weiterer Trend der modernen UI-Gestaltung: der Glassmorphismus. Die Navigationsleiste erhält damit ein milchglasähnliches Aussehen, das beim Scrollen durch den Seiteninhalt den dahinterliegenden Bereich weichgezeichnet durchscheinen lässt.

.gh-head {
    background: rgba(15, 15, 20, 0.6) !important;
    backdrop-filter: blur(12px) !important;
    -webkit-backdrop-filter: blur(12px) !important; /* Safari / iOS */
    border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important;
    box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3) !important;
}

Die entscheidende Eigenschaft ist backdrop-filter: blur(): Sie rendert alles, was sich hinter dem Element befindet, weichgezeichnet. Das funktioniert nur, wenn das Element selbst eine gewisse Transparenz hat - hier durch rgba(15, 15, 20, 0.6), also 40 % transparent. Der feine border-bottom in rgba(255, 255, 255, 0.08) imitiert die Lichtkante von echtem Glas. Das -webkit--Präfix ist für Safari und iOS-Browser nach wie vor notwendig, da diese die unpräfixierte Variante erst seit Safari 18 vollständig unterstützen.

Cyber-Code-Blöcke mit Laser-Animation

Das aufwendigste neue Element sind die vollständig überarbeiteten Code-Blöcke. Die einfachen box-shadow-Glows der Vorgängerversion weichen einem komplexen System aus drei Schichten: einem Milchglas-Hintergrund, einem leuchtenden Rahmen mit Maus-Tracking und einer Eingangsanimation, die einen Laser-Cut simuliert.

Die gesamte Konfiguration liegt in CSS-Custom-Properties, sodass Farben, Geschwindigkeiten und Größen zentral angepasst werden können:

:root {
    --cb-pulse-rgb: 255, 255, 255;   /* Farbe der Lichtwelle auf dem Rahmen */
    --cb-pulse-width: 120px;          /* Breite der Welle */
    --cb-pulse-speed: 2500;           /* Geschwindigkeit in Millisekunden */

    --cb-bg-rgb: 8, 18, 18;           /* Hintergrundfarbe des Blocks */
    --cb-bg-opacity: 0.0;             /* Deckkraft (0 = vollständig transparent) */
    --cb-glass-blur: 5px;             /* Trübung des Milchglases */

    --cb-glow-rgb: 0, 240, 255;       /* Maus-Glow-Farbe (Cyan) */
    --cb-glow-size-inner: 200px;      /* Leuchtradius im Innenbereich */
    --cb-glow-opacity-inner: 0.25;    /* Helligkeit im Innenbereich */

    --cb-border-radius: 8px;
    --cb-laser-core: #ffffff;         /* Farbe des Laser-Cuts beim Laden */
}

Schicht 1 - Das Milchglas: Der Code-Block selbst nutzt backdrop-filter: blur() und einen radialen Gradienten für den Maus-Glow. Die Position des Mauszeigers wird per JavaScript in die CSS-Variablen --mouse-x und --mouse-y geschrieben:

html body .gh-content pre {
    background-image: radial-gradient(
        circle var(--cb-glow-size-inner) at var(--mouse-x) var(--mouse-y),
        rgba(var(--cb-glow-rgb), var(--cb-glow-opacity-inner)) 0%,
        transparent 100%
    ) !important;
}

Schicht 2 - Der leuchtende Rahmen: Das ::before-Pseudoelement erzeugt den Rahmen. Durch eine CSS-Masken-Technik (mask-composite: exclude) leuchtet der Gradient nur auf dem Rahmen selbst, nicht im Inneren:

html body .gh-content pre::before {
    /* Maske: Schneidet den Leuchteffekt innen aus */
    -webkit-mask: linear-gradient(#fff 0 0) content-box,
                  linear-gradient(#fff 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
}

Schicht 3 - Die Laser-Entrance-Animation: Wenn ein Code-Block in den Viewport scrollt, fügt ein IntersectionObserver die Klasse .is-visible hinzu und startet die Animation. Der Block skaliert von einem hauchdünnen horizontalen Strich (scaleX(1) scaleY(0.01)) zur vollen Größe, während box-shadow und border-color von einem gleißend weißen Leuchten zu transparent übergeblendet werden:

@keyframes futuristic-transform {
    0% {
        opacity: 0;
        transform: scaleX(0) scaleY(0.01);
        box-shadow: 0 0 40px 10px rgba(var(--cb-glow-rgb), 1),
                    inset 0 0 20px 5px rgba(var(--cb-glow-rgb), 1);
        border-color: var(--cb-laser-core); /* weißer Laser */
    }
    40% {
        transform: scaleX(1) scaleY(0.01); /* volle Breite, noch dünn */
        border-color: var(--cb-laser-core);
    }
    100% {
        transform: scaleX(1) scaleY(1);    /* volle Größe */
        box-shadow: 0 4px 15px rgba(0,0,0,0.5);
        border-color: transparent;
    }
}

Parallel blendet der Code-Inhalt verzögert ein und wird von filter: blur(5px) zu blur(0) scharf gestellt - als ob der Text buchstäblich aus dem Nichts materialisiert.

Das Hover-Puls-Event startet eine JavaScript-Animation: Ein Kreis mit wachsendem Radius wandert von der Einstiegsposition des Mauszeigers aus über den Rahmen und blendet dabei sanft aus. Die aktuelle Geschwindigkeit (--cb-pulse-speed) wird live aus den CSS-Variablen gelesen, sodass sie ohne Code-Änderung angepasst werden kann:

block.addEventListener('mouseenter', (e) => {
    const PULSE_SPEED = parseInt(
        getComputedStyle(block).getPropertyValue('--cb-pulse-speed').trim()
    ) || 1500;

    function animatePulse(time) {
        let progress = (time - start) / PULSE_SPEED;
        let easeOut  = 1 - Math.pow(1 - progress, 3);
        let currentRadius  = easeOut * maxRadius;
        let currentOpacity = 1 - Math.pow(progress, 2);

        block.style.setProperty('--pulse-radius',  `${currentRadius}px`);
        block.style.setProperty('--pulse-opacity',  `${currentOpacity}`);

        if (progress < 1) requestAnimationFrame(animatePulse);
    }
    requestAnimationFrame(animatePulse);
});

Selbst-gehostete Analyse mit Rybbit

Statt Google Analytics oder ähnlicher Datensammler wird Rybbit zur Analyse eingebunden - vollständig self-hosted:

<script
    src="https://watchme.meister-security.de/api/script.js"
    data-site-id="1"
    data-session-replay="true"
    defer
></script>

Das defer-Attribut stellt sicher, dass das Analytics-Script den Seitenaufbau nicht blockiert. data-session-replay="true" aktiviert die Session-Replay-Funktion, die ermöglicht, Nutzerpfade auf dem Blog nachzuvollziehen, ohne dass Daten an Dritte übermittelt werden. Ein bemerkenswerter Kontrast zur üblichen Praxis.

Die Attack-Map - Monitoring trifft auf Ästhetik

Das aufwendigste Element im Injection-Block ist eine vollflächige Canvas-Animation im Seitenhintergrund: eine animierte Weltangriffskarte, die lose dem realen Suricata-IDS-Monitoring des Blogs nachempfunden ist, aber mit zufällig generierten Daten arbeitet, um Angreifern nicht zu zeigen, ob ihre Attacke bemerkt wurde.

Der Container liegt mit position: fixed und z-index: -1 hinter dem gesamten Seiteninhalt. Der canvas wird durch filter: blur(0.6px) opacity(0.30) auf 30 % Deckkraft reduziert. Das Ergebnis ist ein bewegter Hintergrund, der wahrgenommen, aber möglichst nicht als störend empfunden wird. Auf Mobilgeräten wird der gesamte Effekt per Media Query deaktiviert.

Die Karte baut auf zwei bewährten JavaScript-Bibliotheken auf: D3.js für die kartografische Projektion (Natural Earth 1) und das Rendering der Weltkarte sowie TopoJSON für die kompakten Geodaten der Ländergrenzen. Länder- und Gitterlinien werden einmalig auf einem unsichtbaren Off-Screen-Canvas gezeichnet und dann bei jedem Frame-Update auf den sichtbaren Canvas kopiert - das spart erheblich Rechenleistung.

Die eigentliche Animation besteht aus Missile-Objekten: Jede Instanz repräsentiert einen fiktiven Angriff von einer Quell-IP zu einem Ziel-Host. Die Flugbahn wird als quadratische Bézier-Kurve berechnet, was dem Bogen der Verbindungslinie eine realistische Krümmung gibt:

class Missile {
    constructor(src, target) {
        // Kontrollpunkt senkrecht zur Verbindungslinie versetzt
        const bow = d * (0.25 + Math.random() * 0.15);
        this.cx = mx + nx * bow;
        this.cy = my + ny * bow;
    }

    // Quadratische Bézier-Kurve: Punkt bei Fortschritt t
    bez(t) {
        const u = 1 - t;
        return [
            u*u*this.sx + 2*u*t*this.cx + t*t*this.tx,
            u*u*this.sy + 2*u*t*this.cy + t*t*this.ty
        ];
    }
}

Die Ziel-Konfiguration spiegelt das reale Infrastruktur-Inventar wider. Der Haupt-Host im Raum Bensheim/Frankfurt pulsiert mit einem stärkeren grünen Ringsignal, sekundäre Nodes in Berlin, München, Leipzig und weiteren Standorten mit einem kleineren blauen Puls. In der rechten unteren Ecke erscheint eine Log-Box im Stil eines SURICATA / LIVE FEED, die zufällige Einträge aus einem Pool von 25 fiktiven Suricata-Regelnamen anzeigt - mit Severity-Klassen (high, med, low) in Rot, Gelb und Grün.

Fazit

Die Ghost Code-Injection-Funktion ist ein mächtiges Werkzeug, das weit über einfache Style-Tweaks hinausgeht: Mit wenigen Kilobyte eigenem Code entsteht aus einem Standard-Ghost-Blog ein individuell gestaltetes, analytisch souveränes und visuell auffälliges Projekt. Die animierte Hintergrundkarte aus meinem SOC zu virtualisieren hat mir dabei besonders Freude bereitet. In gleicher Weise freut sich das Management über die gelungene Visualisierung und führt diese immer wieder vor, wenn Kunden sehen wollen, wie modern das SOC eingerichtet ist.
Wer sich Ideen abschauen möchte, findet diese ja im Quelltext dieser Webseite 😄.