Statische Websites mit Pandoc
Für den Umzug weg von GitHub Pages wollte ich Jekyll durch ein bereits von mir verwendetes Werkzeug ersetzen.1 Als Markdown-Nerd habe ich mich entschieden, Pandoc als sogenannten Static Website Generator zweckzuentfremden.
Pandoc ist ein universeller Dokumentumwandler, der eine Vielzahl von Textformaten ineinander überführen kann. Mithilfe von Templates lässt sich die Umwandlung anpassen. Damit aus einer Sammlung von Markdown-Dokumenten mit YAML-Headern überhaupt eine vollständige Website erzeugt werden kann, sind einige Vorarbeiten nötig.
Inhalt
- Der Rahmen
- Ze List
- Die Verwandlung
- Der Feed
- Die Sitemap
- Pandoc-Superkraft
- Lokal testen
- Informationsquellen
Der Rahmen
Die Umsetzung dieser statischen Website benötigt lediglich Standard-Befehle der Konsole, wie sie z. B. in macOS verfügbar sind, und eben Pandoc. Der Rest sind Struktur und Inhalt.
Die Feinheiten bei der Verarbeitung der Markdown-Dateien mit Pandoc ergeben sich dann aus Daten in den jeweiligen YAML-Headern sowie den Ablageorten der Dateien selbst.
Ein Bash-Skript kopiert, konvertiert und erstellt die für jeden
Prozessschritt benötigten Daten, Dateien und Ordner in der richtigen
Reihenfolge im build-Verzeichnis. Dort entsteht so die
fertige statische Website.
Die Quelle mit Beispielprojekt ist unter dem Artikel verlinkt.
Ze List
Pandoc lebt von einem Dokument zum nächsten. Zwar können mehrere Dokumente zur Erstellung eines Zieldokumentes herangezogen werden, aber Pandoc behält keinen Überblick über Verzeichnisse oder Dokumentensammlungen. Daher wird als Erstes eine Liste aller zu konvertierender Markdown-Dateien benötigt. »Ze List« ist also das Inhaltsverzeichnis, an dem sich der Rest des Skripts orientiert.
Ein Bash-Einzeiler leistet die Basisarbeit:
find . -name "*.md" -type f -print0 | sort -rz | tr '\0' '\n' > ze_list.txtDie vollständigen Pfade aller Markdown-Dateien werden mit
find aufgelistet und mit der Option -print0
durch einen \0-Charakter aneinander gereiht. Danach
sortiert sort die Pfade alphabetisch in umgekehrter
Reihenfolge.2 Am Ende ersetzt tr die
\0-en durch Zeilenumbrüche und alles wird in
ze_list.txt gespeichert.
Aus dieser Liste wird dann das Dokument posts.md
erzeugt, ein einfacher YAML-Header, in dem nur die Blog-Beiträge im
Verzeichnis _posts enthalten sind. Aus dieser Information
werden später der RSS-Feed und die Anzeige der neuesten Beiträge
abgeleitet.
Die Verwandlung
Im nächsten Schritt werden alle Markdown-Dateien in HTML-Dokumente
verwandelt. In einer Schleife wird Ze List ausgelesen und für
jede Datei write_file mit dem kompletten Pfad der
.md-Datei und dem relativen Ziel-Verzeichnis
aufgerufen.
while IFS= read -r line; do
DESTINATION_DIR=""
if [[ $line == *"_posts"* ]]; then
DESTINATION_DIR="blog/"
fi
write_file "$line" "${DESTINATION_DIR}"
done < ze_list.txtDie Funktion write_file gibt Pandoc je nach Datei-Art
die entsprechenden Argumente mit, die zur Konvertierung benötigt werden.
Für Index-Dateien werden die entsprechenden Daten der kürzlich
veröffentlichten Beiträge aus posts.md mitgegeben. Dafür
bekommen Blog-Beiträge dann den Lua-Filter zur Berechnung der
Lesezeit.
# $1 = source file path
# $2 = relative destination folder
function write_file {
echo -e "\x1b[1;37m- /$2 \x1b[0m $(basename $1 .md)"
BASENAME="$(basename ${1} .md)"
FILTER=""
if [[ $1 == *"_posts"* ]] && [[ $1 != *"index"* ]]; then
BASENAME=${BASENAME:11}
FILTER="--lua-filter ./_filters/filter-reading-time.lua"
fi
if [[ $1 == *"index"* ]]; then
FILTER="--lua-filter ./_filters/filter-latest-posts.lua"
fi
pandoc "./_data/posts.md" "${1}" -f markdown -t html \
-o "build/${2}${BASENAME}.html" \
--defaults "./_data/config.md" \
--template="./_templates/default.template.html" \
--metadata filename="${BASENAME}.html" \
${FILTER}
}Ist »Ze List« abgearbeitet, sind alle Beiträge, Index- und normale Seiten umgewandelt.
Der Feed
Zu einer klassischen Website gehört ein RSS-Feed. Für die Erstellung
kommt die posts.md zum Einsatz, gepaart mit einem
XML-Template. Allerdings kann Pandoc kein XML, zumindest nicht richtig.
Die erzeugte Datei muss also nachbearbeitet werden.
Zuerst schneidet sed überfüssige HTML-Tags heraus.3 Danach wird das XML mit
xmllint »aufgehübscht« und valide gemacht.
mkdir -p build/feed
pandoc -i "./_data/posts.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"Die Sitemap
Die einfachste Sitemap ist laut Google eine Textdatei, die
zeilenweise die URLs aller HTML-Seiten und sonstiger indizierbarer
Dokumente enthält. Eine solche ist mit find und
sed schnell erstellt.
BASEURL="https\:\/\/www\.foo\.bar\/"
cd build
find . -name "*.html" > sitemap.txt
sed -i '' -e "s/\.\//$BASEURL/g" sitemap.txt
cd ..Znächst findet find alle .html-Dateien
finden. Dann wird der lokale Verzeichnispfad durch die Basis-URL
ersetzt.
Pandoc-Superkraft
Pandoc-Templates fehlt die Möglichkeit, Teile von Listen zu verwenden, z. B. Elemente 3 bis 8 oder die ersten fünf. Genau das braucht es allerdings für die neuesten Beiträge.
Das lässt sich bei Pandoc mit einem Lua-Filter umsetzen. Im
YAML-Header der jeweiligen Markdown-Datei wird ein Wert für
showInLatest: angegeben, z. B. 5 oder auch
all, wenn alle Beiträge aus posts.md
aufgelistet werden sollen.
Die Datei posts.md muss Pandoc dann zusätzlich zum
Filter mitgegeben werden. Der Filter erzeugt daraus die benötigten
Metadaten bestehends aus Titel, Artikelbild, Beschreibung und Link, die
dann wiederum für Pandoc in der Variable latest zur
Verfügung stehen.
Hier ein Auszug aus dem Skript des Lua-Filters.
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
-- ...
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
endEine weitere Superkraft ist die Berechnung der ungefähren Lesezeit eines Artikels. Auch das ist nur mit einem Lua-Filter nötig, da Pandoc selbst keine Berechnungen zum Umfang des Inhalts anstellt.
Hier ein Auszug aus meinem Lua-Filter, der grob alles zählt, was nach einem Wort aussieht, und Ende diese Anzahl durch die durchschnittliche Lesegeschwindigkeit von 250 Wörtern pro Minute teilt.
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)
-- ...
doc.meta.readingTime = rt
return doc
endLokal testen
Um die generierte Website richtig ausprobieren zu können, ist ein Webserver nötig. Ansonsten funktionieren die URLs nicht und das Stylesheet sowie Bilder werden nicht geladen, wenn HTML-Dokumente direkt im Browser geöffnet werden.
Wie der Zufall es will, verfügt Python im Standard über einen einfachen Webserver. Simpel, aber für statische Seiten ausreichend. Dieser lässt sich für jedes beliebige Verzeichnis starten.
In macOS funktioniert das wie folgt:
cd build
open "http://localhost:8080"
python -m http.server 8080Wechsel in das Verzeichnis mit der fertig generierten Website, Öffnen der Local-Host-URL im Standard-Browser und Starten des Webservers mit Python. Soll ein anderer Browser verwendet werden, geht das natürlich auch.
open -a /Applications/Firefox.app --url "http://localhost:8080"Informationsquellen
Basis-Beispiel als Quelltext-Projekt:
Diese drei Quellen enthalten weitere Informationen. Neben der Pandoc-Dokumentation ist es noch spannend zu sehen, dass auch andere Menschen versuchen, die ausgetretenen Pfade gängiger Website-Lösungen zu verlassen.
Jekyll wird von GitHub Pages als Website-Generator verwendet, sodass Quelltexte in Form von Markdown, YAML etc. per Git zur Verfügung gestellt werden können. Auf Neocities müssen jedoch die fertigen, statischen Seiten selbst bereitgestellt werden.↩︎
Das ist notwendig, wenn die Beiträge nach absteigender Aktualität angezeigt werden sollen. Dazu beginnt jeder Beitrags-Dateiname mit dem Erstellungsdatum in der Form
yyyy-mm-dd-.↩︎Bei der Verwendung von
sedauf macOS ist zu beachten, dass nach der Option-i,--in-placeein Leerzeichen notwendig ist. Dann mittels''kennzeichnen, dass die Änderungen in die selbe Datei geschrieben werden sollen. Unter Linux reichtsed -i 's/..//g' file.↩︎