Statische Websites mit Pandoc erstellen
Foto von Jiawei Zhao auf Unsplash

Statische Websites mit Pandoc erstellen

18. August 2025 · Etwa 4 Minuten Lesezeit.

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

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

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:

  1. Die automatische Erzeugung von Dateilisten, z. B. alle veröffentlichten Beiträge.
  2. 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:

  1. für die Darstellung aller Beiträge auf einer Blog-Übersichtsseite,
  2. die Anzeige neuesten Artikel auf der Startseite und
  3. 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
end

Schließ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
end

Im 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:

  1. Mit Pandoc aus Markdown und Template die rss.xml-Datei erzeugen.
  2. Die von Pandoc erzeugten überflüssigen <p></p>-Elemente mit sed und dem entsprechenden regulären Ausdruck entfernen.
  3. Das XML mit xmllint valide 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 8080

Informationsquellen


  1. Codeberg Pages bietet, anders als GitHub Pages, keinen integrierten Generator für statische Websites. Ich empfinde das als Vorteil, da man so nicht festgelegt ist.↩︎

  2. Nach der initialen Einrichtung ist der Folgeaufwand überschaubar, wenn man nicht gerade ein Vielschreiber ist.↩︎