Goodbye Wordpress. Hello Metalsmith.

Veröffentlicht am 06.03.2016.

Wordpress besticht durch seine Einfachheit. Es bietet einen Installer und eine Fülle an Plugins und Themes. Die eigene Webseite ist in wenigen Stunden eingerichtet und nach eigenem Gusto designed. Das Betreiben einer Wordpress-Installation ist leider alles andere als bequem. WPScan Vulnerability Database listet zur Zeit weit über 4000 bekannte Sicherheitslücken. Auch wenn die meisten bereits geschlossen wurden, ist Wordpress grundsätzlich ein Honeypot für Skript-Kiddies.

Die von mir (privat) betriebenen Webseiten sind zudem meist nur wenige “Seiten” stark und benötigen neben den üblichen Verdächtigen keine dynamischen Inhalte. Dazu zählen Kontaktformular, Kommentarfunktion und die Einbindung der letzten Belanglosigkeiten auf Twitter. Für all dies gibt es externe Dienste, die einem die Administration und das Spamfiltern abnehmen. Übrig bleiben eine handvoll statischer Seiten, die den Betrieb eines Content Management Systems (CMS) wie Wordpress eigentlich nicht rechtfertigen.

Eine zentrale Stärke eines CMS ist jedoch die Trennung von Inhalt und Layout. Auf diese möchte ich auf keinen Fall verzichten. Layout-Änderungen auf zig HTML-Seiten händisch vorzunehmen ist ein No-Go. Die Lösung: Ein Static Site Generator. Also eine Art lokales CMS, dass HTML Seiten zum Upload auf den Webserver generiert.

Metalsmith

Metalsmith ist ein auf node.js basierender Static Site Generator. Das Grundprinzip ist denkbar einfach: Nehme alle Dateien aus Order A, wende eine definierte Anzahl von Filtern auf diese an und schreibe das Ergebnis nach Order B.

Das Grundgerüst

mkdir website
cd website
npm init

Die Prompts einfach alle mit <ENTER> bestätigen.

npm install --save harmonize metalsmith

Eine neue Datei index.js erstellen mit folgendem Inhalt:

require('harmonize')();
var metalsmith = require('metalsmith');

metalsmith(__dirname)
    .source('contents') 
    .destination('build') 
    .build(function(err){ 
            if (err) console.log(err);
    });

Dann legen wir noch einen Ordner contents an und legen darin die Datei index.md mit dem üblichen Hello, World! ab.

Lassen wir metalsmith mal laufen:

node index.js

Was ist passiert? Metalsmith hat einen neuen Ordner build angelegt und dort die Datei index.md mit bekanntem Inhalt abgelegt. Soweit nicht spektakulär. Wir haben ja noch keine Filter eingebaut…

Inhalte mit Markdown formatieren

Markdown ist eine Auszeichnungssprache, die z.B. auch bei Wikipedia Anwendung findet. Für Metalsmith gibt es zahlreiche Module, die Markdown verarbeiten. Ich nutze metalsmith-markdown-remarkable:

npm install --save metalsmith-markdown-remarkable

Die Datei index.js ändern wir wie folgt ab:

require('harmonize')();
var metalsmith = require('metalsmith'),
markdown = require('metalsmith-markdown-remarkable');

metalsmith(__dirname)
    .use(markdown({
        "typographer": true,
        "linkify": true,
        "html": true,
        "breaks": true
    }))
    .source('contents') 
    .destination('build') 
    .build(function(err){ 
            if (err) console.log(err);
    });

Führen wir erneut node index.js aus, fällt auf, dass im build-Ordner nun statt der Datei index.md eine index.html liegt mit folgendem Inhalt:

<p>Hello, World!</p>

Und das ist exakt das, was das Markdown-Modul tut: Es wandelt Textdateien in HTML um. Die entsprechenden Syntax-Regeln finden sich z.B. hier.

Layouts

Mit Hilfe von Layouts können Inhalte um verschiedene Designs und wieder verwendbare Elemente (wie z.B. Navigation, Kopf- und Fußzeilen, Seitenleiste, etc.) angereichert werden. Metalsmith unterstützt verschiedene Layout-Engines. Ich benutze swig.

npm install --save metalsmith-layouts

In der index.js fügen wir die Abhängigkeit zu metalsmith-layouts hinzu

[...]
var metalsmith = require('metalsmith'),
layouts = require('metalsmith-layouts'),
[...]

als auch einen weiteren .use-Block

[...]
metalsmith(__dirname)
    .use(markdown([...]))
    .use(layouts({
        engine: 'swig',
        directory: 'layouts',
        partials: 'layouts',
        pattern: '**/*.html',
        default: 'main.html'
    }))
[...]

Es gilt zu beachten, dass die Reihenfolge der use-Anweisungen wichtig ist. Im Layouts-Block geben wir an, dass es auf alle Elemente anzuwenden ist, die den Datei-Suffix .html tragen. Ein Blick in den contents-Ordner erinnert aber daran, dass dort lediglich eine .md-Datei liegt. Diese wird durch den Markdown-Block in HTML (mit entsprechendem .html-Suffix) gewandelt. Würde man also die Reihenfolge der beiden Blöcke umdrehen, würde keine Seite im gewünschten Layout gerendert.

Fehlt noch das Layout. Dazu erstellen wir einen Ordner layouts und legen darin die Datei main.html mit folgendem Inhalt an:

<!DOCTYPE html>
<html lang="de">
    <head>
        <title>Meine tolle Webseite</title>
    </head>
    <body>
        <h1>Meine tolle Webseite</h1>
        <div id="content">
            {{ contents | safe }}
        </div>
    </body>
</html>

Die entscheidende Zeile ist hier {{ contents | safe }}. contents ist die Variable, in der der gerenderte Inhalt steht. Bei der Swig-Engine werden Variablen mit doppelten geschweiften Klammern aufgerufen. Strings werden von Swig automatisch escaped. Da unser Inhalt aber wie gewünscht HTML enthält, geben wir hinter der Pipe den Filter safe an. Dieser bewirkt, dass der Inhalt der Variable unverändert ausgegeben wird.

Die komplette Feature-Liste der Swig-Engine findet sich hier.

Führen wir node index.js erneut aus, steht in der build/index.html folgender Inhalt:

<!DOCTYPE html>
<html lang="de">
    <head>
        <title>Meine tolle Webseite</title>
    </head>
    <body>
        <h1>Meine tolle Webseite</h1>
        <div id="content">
            <p>Hallo, Welt!</p>
        </div>
    </body>
</html>

Soll ein anderes Layout-Template als das Default verwendet werden, kann dies als Metadatum layout angegeben werden. Ach ja, Metadaten…

Metadaten und Variablen

Markdown-Dateien können beliebig viele Metadaten enthalten. Dazu editieren wir index.md wie folgt:

---
title: Meine erste Seite
author: Tilman
date: 2016-01-27
---

Hello, World!

Diese Metadaten können im Layout so verwendet werden:

<!DOCTYPE html>
<html lang="de">
    <head>
        <title>{{ title }}</title>
    </head>
    <body>
        <h1>{{ title }}</h1>
        <div id="content">
            <p>{{ author }} schrieb am {{ date | date('d.m.Y') }}:</p>
            {{ contents | safe }}
        </div>
    </body>
</html>

Mit dem Resultat nach Aufruf von node index.js:

<!DOCTYPE html>
<html lang="de">
    <head>
        <title>Meine erste Seite</title>
    </head>
    <body>
        <h1>Meine erste Seite</h1>
        <div id="content">
            <p>Tilman schrieb am 27.01.2016:</p>
            <p>Hallo, Welt!</p>
        </div>
    </body>
</html>

Man beachte, dass das Datum von Metalsmith zu einem Date-Objekt geparsed wurde, dass mit dem Swig-Filter date() entsprechend formatiert wird.

Collections - ein Blog entsteht

Artikel unterscheiden sich in meinem Ökosystem von anderen Seiten durch das verwendete Layout (layout: article.html) und ihrem Speicherort. Bei mir liegt jeder Artikel in einem Unterordner von contents/posts/. Der Artikel selbst ist dann in der jeweiligen index.md niedergeschrieben. Dies hat zwei Vorteile: Zum einen habe ich mir sagen lassen, dass URLs die auf Ordner verweisen statt auf konkrete Dateien von Suchmaschinen besser bewertet werden. Zum anderen kann ich so Bilder und weitere Resourcen relativ zum Artikel innerhalb des Ordners ablegen und muss bei einer Reorganisation die abhängigen Resourcen nicht mühsam zusammenklauben.

Für den Blog-Index verwende ich vier weitere Module:

npm install metalsmith-collections metalsmith-pagination metalsmith-permalinks metalsmith-snippets

Die Reihenfolge der use()-Anweisungen ist hier wichtig!

[...]
var collections     = require('metalsmith-collections');
var pagination      = require('metalsmith-pagination');
var snippet             = require('metalsmith-snippet');
var permalinks      = require('metalsmith-permalinks'); 
[...]

metalsmith(__dirname)
.use(collections({
    posts: {
        pattern: 'posts/**/*.md',
        sortBy: 'date',
        reverse: true
    },
}))
.use(markdown( [...] ))
.use(permalinks())
.use(pagination({
    'collections.posts': {
        perPage: 5,
        first: 'index.html',
        path: 'posts/:num/index.html',
        layout: 'blog.html',
        pageMetadata: {
            title: 'Mein Blog'
        }
    }
}))
.use(snippet({
    stop: '\n',
    suffix: '...'
}))
[...]

Was passiert hier? Gehen wir die Module der Reihe nach durch:

  1. Collections nimmt alle Dateien in die Liste posts auf, die unterhalb von contents/posts/ liegen und ein .md-Suffix haben. Diese werden nach dem Metadatum date in umgekehrter Reihenfolge sortiert.
  2. Markdown-Dateien werden wie gewohnt in HTML verwandelt.
  3. Das Permalinks-Modul fügt allen Seiten eine Variable path in den Metadaten hinzu - dies ist die relative URL der Seite.
  4. Pagination gruppiert die Liste posts in Päckchen von je 5 Artikeln und rendert Index-Seiten. Die erste wird als index.html im build-Ordner abgelegt, alle weiteren unter build/posts//index.html. Die Index-Seiten werden mit dem Layout blog.html formatiert.
  5. Snippet speichert den Text einer jeden Seite bis zum ersten Zeilenumbruch (stop: '\n') als Metadatum snippet und hängt drei Punkte hinten an.

Das Layout blog.html sieht wie folgt aus:

{% block contents %}
{% for post in pagination.files %}
    <article>
        <h2><a href="/{{ post.path }}">{{ post.title }}</a></h2>
        <p class="date">Veröffentlicht am {{ post.date | date('d.m.Y') }}.</p>
        <p>{{ post.snippet }} <a class="btn btn-default" href="/{{ post.path }}">weiterlesen</a></p>
    </article>
{% endfor %}
 
 <nav>
    <ul class="pager">
         {% if pagination.previous %}
         <li class="previous"><a href="/{{ pagination.previous.path }}"><span aria-hidden="true">&larr;</span> Neuer</a></li>
         {% endif %}
         {% if pagination.next %}
         <li class="next"><a href="/{{ pagination.next.path }}">Älter <span aria-hidden="true">&rarr;</span></a></li>
         {% endif %}
     </ul>
 </nav>
 {% endblock %}

Die pagination-Variable beinhaltet hier die komplette Magie:

  • pagination.files ist eine Liste aller Posts der aktuellen Index-Seite. Die Posts enthalten alle Metadaten der Seite, also auch den path, der über das Permalinks-Modul hinzugefügt wurde und den Snippet.
  • pagination.previous beinhaltet, wenn gesetzt, den path zur vorherigen Index-Seite.
  • pagination.next entsprechend den zur nächsten Index-Seite.

Ad-hoc Webserver für die statische Seite

Sinn der Übung ist es, eine statische Webseite zu generieren, die man auf jeden handelsüblichen Webspace hochladen kann. Während man die Seite lokal bearbeitet, wäre es aber wünschenswert das Resultat auch lokal begutachten zu können. Über die Module metalsmith-serve und metalsmith-watch kann man dazu einen lokalen Webserver starten.

npm install --save metalsmith-if metalsmith-serve metalsmith-watch

Die index.js ändern wir hierzu wie folgt:

[...]
var msif                    = require('metalsmith-if');
var serve                   = require('metalsmith-serve');
var watch                   = require('metalsmith-watch');

metalsmith(__dirname)
    .use(msif(process.env.SERVE, serve()))
    .use(msif(process.env.SERVE, watch({
        paths: {
            "${source}/**/*" : true,
            "layouts/**/*": "**/*.html",
        },
        livereload: false
    })))
[...]

Durch die Verwendung von metalsmith-if lässt sich node index.js wie bisher aufrufen. Ist aber die Umgebungsvariable SERVE gesetzt, wird ein lokaler Webserver auf Port 8080 gestartet:

SERVE=1:* node index.js

Im Browser ist die Seite nun unter http://localhost:8080 erreichbar. Das Modul metalsmith-watch überwacht das contents-Verzeichnis, so dass Änderungen an der Webseite nach einem Refresh im Browser angezeigt werden.

Der lokale Server lässt sich mit <CTRL + C> wieder stoppen.

Tilman Moser

Hejdå! Jag är Tilman Moser.

Ich bin Management Berater in der IT Branche.

Im wahren Leben bin ich Vater, Ehemann, Gewicht reduzierender Freizeit-Sportler, Hobby-Linux-Admin und Programmierer. Ich interessiere mich für Darts, Whiskey und Skandinavien.

Auf dieser Seite schreibe ich Dinge auf, die ich sonst wahrscheinlich wieder vergesse. Manches davon ist vielleicht auch für andere interessant... :-)