Statische Websites mit Pandoc erstellen
Auf den ersten Blick erscheint es nicht naheliegend, Pandoc als Static Website Generator zu nutzen. Es sei denn, für einen Markdown-Nerd wie mich, der eh schon alles mit Pandoc macht. Mich hat es interessiert. So ist dieses Projekt entstanden – und auch diese Website. 1
Pandoc ist ein universeller Dokumentumwandler, der Markdown-Text in eine Vielzahl von Formaten konvertieren kann. Obwohl HTML zu diesen Formaten zählt, sind einige Hürden auf dem Weg zu einer kompletten Website zu nehmen. Glücklicherweise ist es nicht so, dass diese Sachen noch nie zuvor jemand versucht hätte. Mit der richtigen Suchwort-Kombination lässt sich für viele Fragestellungen zumindest ein Ansatz finden. Einige Informationsquellen sind am Ende aufgelistet.
Inhalt
- Die Basis
- Beiträge auflisten
- Teile von Listen
- Lesedauer
- RSS-Feed
- Sitemap
- Lokal testen
- Informationsquellen
Die Basis
Pandoc ist ein Kommandozeilen-Werkzeug, das über den Aufruf
pandoc [Optionen] [Eingabe] [Ausgabe]
aus einer oder mehreren Markdown-Dateien eine andere Datei erzeugen kann. Damit daraus eine komplette Website entstehen kann, ist schon ein bisschen Fantasie nötig. Darüber hinaus braucht es
- eine Ordnerstruktur,
- einige Pandoc-Templates,
- ein Stylesheet,
- ein paar Lua-Filter,
- Markdown-Dateien (für Text und Daten) und
- ein Bash-Script, das alles »zusammennäht«.
Okay, an einigen Stellen ist dann immer noch etwas Handarbeit notwendig.2 Denn Pandoc lässt für mein Projekt vor allem zwei Dinge schmerzlich vermissen:
- Die automatische Erzeugung von Dateilisten, z. B. alle veröffentlichten Beiträge.
- Die Verwendung von beliebigen Teilen einer Liste, z. B. die Einträge 3 bis 5.
Beiträge auflisten
Eine Liste aller Beiträge wird auf jeden Fall benötigt, und das gleich für drei Szenarien:
- für die Darstellung aller Beiträge auf einer Blog-Übersichtsseite,
- die Anzeige neuesten Artikel auf der Startseite und
- für den RSS-Feed.
Lösen lässt sich das Problem durch ein paar YAML-Metadaten.
Mein rss.md sieht folgendermaßen aus:
---
title: RSS-Feed
link: https://meine.website.de
description: |
Meine private Ecke im Internet.
item:
- title: Titel des Blog-Beitrags
link: /blog/titel-des-blog-beitrags.html
cover: /images/foto-einer-katze.jpg
description: |
Dies und das und jenes.
...Aus dieser Datei lassen sich sowohl der RSS-Feed als auch die Anzeige der neuesten und aller Beiträge generieren. Man muss lediglich jeden neuen Artikel einmalig von Hand hier hinzufügen.
Teile von Listen
Pandoc-Templates fehlt die Möglichkeit, einen spezifischen Teil einer Liste zu verwenden. Wer nicht nur den ersten oder letzten Eintrag gesondert behandeln will, sondern etwa die fünf neuesten Blog-Beiträge aus einer Liste von elfundneunzig, muss sich etwas einfallen lassen.
Für diesen Fall ist meine Lösung ein
Latest-Posts-Lua-Filter. Dieser erzeugt aus der
bestehenden Dateiliste im rss.md eine neue Liste,
die nur aus den gewünschten Einträgen besteht. Im Dokument, in
dem ich die Beiträge auflisten will, muss ich dazu die Anzahl
im YAML-Block mittels showInLatest benennen.
function Pandoc(doc)
local latest = {}
local n = tonumber(pandoc.utils.stringify(doc.meta.showInLatest)) or 3
if pandoc.utils.type(doc.meta.item) == "List" then
if #doc.meta.item < n
or pandoc.utils.stringify(doc.meta.showInLatest) == "all" then
n = #doc.meta.item
end
for i = 1, n do
latest[i] = {}
latest[i].title = pandoc.utils.stringify(doc.meta.item[i].title)
latest[i].link = pandoc.utils.stringify(doc.meta.item[i].link)
latest[i].cover = pandoc.utils.stringify(doc.meta.item[i].cover)
latest[i].description = pandoc.utils.stringify(doc.meta.item[i].description)
end
end
doc.meta.latest = latest
return doc
endSchließlich der Pandoc-Aufruf, in dem alles zusammenkommt, am Beispiel der Startseite.
pandoc -f markdown -t html \
-o "build/index.html" \
"_data/rss.md" "_root/index.md" \
--defaults "_data/config.md" \
--template="_templates/index.template.html" \
--lua-filter "_filters/filter-latest-posts.lua"Lesedauer
Über einen Lua-Filter lässt sich sogar die Angabe einer ungefähren Lesedauer für Beiträge bewerkstelligen. Diese Bestimmung der Wortanzahl ist recht grob, reicht mir aber erstmal.
local wordsPerMinute = 250
function Pandoc(doc)
local wordCount = 0
local text = pandoc.utils.stringify(doc.blocks)
for words in text:gmatch('[^.,?!\n\t()–%-]+') do
if type(words) == "string" then
for word in words:gmatch("%w+") do
wordCount = wordCount + 1
end
end
end
local rt = math.ceil(wordCount / wordsPerMinute)
if rt < 2 then
rt = "Etwa " .. rt .. " Minute Lesezeit."
else
rt = "Etwa " .. rt .. " Minuten Lesezeit."
end
doc.meta.reading_time = rt
return doc
endIm Template reicht ein einfaches
$reading_time$ zur Ausgabe.
RSS-Feed
Wie schon für die Übersicht der Beiträge dient die
rss.md-Datei auch als Grundlage für den RSS-Feed.
Das Template ist denkbar einfach und enthält nur die nötigsten
Elemente.
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>$title$</title>
<link>$link$</link>
<description>$description$</description>
$for(item)$
<item>
<title>$item.title$</title>
<link>$link$$item.link$</link>
<description>$item.description$</description>
$if(item.cover)$
<image>
<url>$link$$item.cover$</url>
<title>$item.title$</title>
<link>$link$$item.link$</link>
</image>
$endif$
</item>
$endfor$
</channel>
</rss> Die Umwandlung in ein schönes XML erfolgt dann in drei Schritten:
- Mit Pandoc aus Markdown und Template die
rss.xml-Datei erzeugen. - Die von Pandoc erzeugten überflüssigen
<p></p>-Elemente mitsedund dem entsprechenden regulären Ausdruck entfernen. - Das XML mit
xmllintvalide und hübsch machen.
pandoc -i "_data/rss.md" \
-o "build/feed/rss.xml" \
-f markdown -t html \
--template="_templates/rss.template.xml"
sed -i '' -e 's/<\/\{0,1\}p>//g' "build/feed/rss.xml"
xmllint --format "build/feed/rss.xml" --output "build/feed/rss.xml"Bei der Verwendung von sed ist zu beachten,
dass es in macOS notwendig ist, nach der Option
-i, --in-place ein Leerzeichen zu
lassen und dann mittels '' zu kennzeichnen, dass
die Änderungen in die selbe Datei geschrieben werden sollen.
Unter Linux würde sed -i 's/..//g' file
reichen.
Sitemap
Die Sitemap hätte ich fast vergessen. Die ist sonst ein Nebenprodukt aller CMS-Lösungen und sogar bei Static-Website-Generatoren. Immerhin muss es kein XML sein. Eine einfache Textdatei mit den ganzen URLs reicht auch. Sagt Google.
Schnell mit find alle HTML-Dateien mit Pfad
auflisten und in eine Textdatei schreiben. Dann mit
sed den Pfadbeginn ./ durch die
Basis-URL ersetzen. Fertig.
BASEURL="https\:\/\/www\.foo\.bar\/"
cd build
find . -name "*.html" > sitemap.txt
sed -i '' -e "s/\.\//$BASEURL/g" sitemap.txt
cd ..Auch hier ist zu beachten, dass macOS die
-i ''-Option benötigt, und zwar mit
Leerzeichen.
Lokal testen
Um die generierte Website richtig ausprobieren zu können, ist ein Webserver nötig. Wie der Zufall es will, verfügt Python im Standard über einen solchen. Simpel, aber für statische Seiten ausreichend. Dieser lässt sich für jedes beliebige Verzeichnis starten.
Das Beispiel ist für macOS gedacht. Es wechselt in das Verzeichnis, startet zuerst den Standard-Browser und danach den Webserver für das gewählte Verzeichnis.
cd /path/to/built/website/
open "http://localhost:8080"
python -m http.server 8080Informationsquellen
- Pandoc User Guide – Templates
- Simple Static Sites With Pandoc
- My current HTML boilerplate
- Makefile-based Blogging
- A word count for Pandoc (StackExchange)