Ghost vollständig mit KeyCDN betreiben

Um einfacher Bloggen zu können, habe ich meinen Blog von Jekyll auf Ghost umgestellt. Jekyll hatte ich zuvor komplett hinter KeyCDN betrieben. Mein Ziel war es dies auch mit Ghost umzusetzen, um den Blog zu beschleunigen.


Meine Ziele:

  1. Einen voll funktionsfähigen Adminbereich
  2. Alle Inhalte (auch HTML Seiten) in KeyCDN cachen, bis diese gepurged werden
  3. KeyCDN automatisch purgen, wenn ein Post erstellt oder editiert wird

Ghost kommt gut vorbereitet. Es werden von Haus aus passende Cache-Control Header gesetzt, dank Issue #1470. Für den kompletten Adminbereich wird das Caching verhindert. So funktioniert der Adminbereich schon ohne Änderungen direkt mit KeyCDN. Statische Inhalte werden direkt für 1 Jahr gespeichert. Fehlen nur noch die statischen Inhalte. Diese werden durch die Cache-Control Header nicht abgedeckt. Dies musste geändert werden.

Aufgrund den guten Standardwerten konnte KeyCDN angwiesen werden die Cache-Control Header des Origin Servers zu respektieren.


Ghost wurde nach der „Install and Deploy“ Anleitung installiert. Anstelle von NGINX nutze ich Apache, da es bereits auf der VM genutzt wurde. Meine Konfigurationsdatei sieht so aus:

<VirtualHost 0.0.0.0:80>
    ServerName YOUR_NAME_HERE
    ServerAlias YOUR_ALIAS_HERE

    # Make HTTPS work
    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set Host "YOUR_GHOST_URL_HERE"

    ProxyPreserveHost on
    ProxyPass / http://127.0.0.1:2368/

    # Unset advertisement from Ghost server
    Header unset X-Powered-By

    # Setup caching for a CDN
    Header append Cache-Control s-maxage=16070400 "expr=%{REQUEST_STATUS} == 200"

    # Prevent caching of drafts
    <Location ~ "^/p/">
        Header set Cache-Control "no-cache, private, max-age=0"
    </Location>
</VirtualHost>

Der großteil der Konfiguration sorgt für das korrekte Ausliefern per HTTPS. Es wird die s-maxage Direktive genutzt, um KeyCDN anzuweisen auch dynamische HTML Seiten zu cachen. Zusätzlich wird noch das Cachen von Entwürfne verhindert.

Diese Apache Konfiguration erfüllt bereits die ersten zwei meiner Ziele. Was noch fehlt ist ein automatisches Purgen, sobald ein neuer Post erstellt wird, oder ein bestehender editiert wird. Ghost bietet diese Funktionalität nicht von sich aus, also sollte es mit einem Plugin (bzw. einer App, wie es in Ghost heißt) erweitert werden.

Leider ist das App system noch nicht so fortgeschritten in der Entwicklung. Das für diese Funktion benötigte Event System ist noch immer in der Planungsphhase. Andererseits wollte ich auch nicht zu viel an Ghost ändern, um später noch gut auf eine neue Version upgraden zu können. Beim durchsuchen des Codes bin ich auf eine Slack Integration aufmerksam geworden. Diese nutzte bereits eine Art Event Listening zum erstellen eines Posts. Allerdings scheint das Eventsystem nicht für Apps vorgesehen zu sein, sondern nur für die interne Nutzung.

Um das Eventsystem trotzdem in Apps nutzen zu können, ist eine kleine Änderung an core/server/apps/proxy.js notwendig. In der Datei wird eine proxy Variable definiert, die jeder App übergeben wird. Um das Event System mit zu übergeben, habe ich es mit events: require('../events') zu der proxy Variable hinzugefügt.

Die erstellte App sieht dann so aus:

var App = require('ghost-app'),

    CdnHelper;

var http = require('https');

CdnHelper = App.extend({
    install: function () {},

    uninstall: function () {},

    activate: function () {
        console.log('CdnHelper: activate()');

        // Register events that signal modification of the site
        this.ghost.events.on('post.published', this.purge);
        this.ghost.events.on('post.published.edited', this.purge);
        this.ghost.events.on('post.unpublished', this.purge);
        this.ghost.events.on('page.published', this.purge);
        this.ghost.events.on('page.published.edited', this.purge);
        this.ghost.events.on('page.unpublished', this.purge);
    },

    deactivate: function () {},

    purge: function(model) {
        zone_id = 'YOUR_ZONE_ID';
        api_key = 'YOUR_API_KEY';
        console.log('CdnHelper: Recognized a change. Purging zone now.');

        var request = http.request({
            host: 'api.keycdn.com',
            path: '/zones/purge/' + zone_id + '.json',
            auth: api_key + ':'
        }, function(res) {
            res.on('data', function(chunk) {
                console.log('CdnHelper: Answer from KeyCDN: ' + chunk.toString('utf8'));
            });
        });
        request.end();

        request.on('error', function(e) {
          console.error(e);
        });
    }

});

module.exports = CdnHelper;

Es registriert den Listener zu allen Events die bei einer Content Änderung gefeuert werden. Der Listener führt dann ein Purge Event mit der KeyCDN API aus.

Die App dann zu installiert ist etwas kompliziert, aber sobald sie einmal läuft tut sie ihren Dienst sehr gut. Leider kann das, wie an vielen Stellen hingewiesen, mit zukünftigen Ghost Versionen ändern.