Ich schreibe gerade den IT-Survival-Guide für Selbständige – einen praxisorientierten Ratgeber zu IT-Sicherheit, Datenschutz und digitalem Krisenmanagement für Selbständige ohne IT-Hintergrund. Und natürlich habe ich das Tooling dafür zu einem eigenen kleinen Projekt gemacht.
Dieser Artikel beschreibt das technische Setup: Warum Markdown als Single Source of Truth, wie Website, PDF und EPUB aus denselben Quelldateien entstehen, wo ich gegen Wände gelaufen bin – und was ich beim nächsten Mal anders machen würde.
Das Grundprinzip: Eine Quelle, drei Ausgaben
Das Ziel war von Anfang an klar: Ich möchte den Inhalt nur einmal schreiben und ihn in drei verschiedenen Formaten veröffentlichen:
- Website – navigierbar, verlinkbar, suchmaschinenfreundlich
- PDF – druckfertig, mit Seitenumbrüchen, Kopf- und Fußzeilen, Anhang-Nummerierung
- EPUB – für E-Reader, mit angepasstem Layout
Die gesamte Struktur liegt in einem Git-Repository. Alle Inhalte sind schlichte Markdown-Dateien. Gebaut wird mit einem einzigen Befehl:
make deploy-all
Das baut PDF, EPUB und Website in der richtigen Reihenfolge, kopiert die Binaries in den Website-Build und deployt alles per rsync auf den Server.
Verzeichnisstruktur
it-survival-guide/
├── kapitel/ # Markdown-Quelltexte
│ ├── assets/ # CSS, Bilder
│ ├── index.md
│ ├── teil1-*.md
│ ├── teil2-*.md
│ └── ...
├── bilder/ # Druckbilder für Pandoc
├── output/ # Build-Artefakte (gitignored)
├── overrides/ # MkDocs Theme-Overrides
├── hooks/ # MkDocs Python-Hooks
│ └── preprocess.py
├── appendix.lua # Pandoc Lua-Filter
├── split_html.py # HTML-Splitter für CMS-Import
├── Makefile
├── mkdocs.yml
└── metadata.yaml
Die kapitel/-Dateien sind die einzige Quelle für alle Ausgaben. Nichts wird doppelt gepflegt.
Website: MkDocs mit Material Theme
Für die Website verwende ich MkDocs mit dem Material Theme. Das Theme ist ausgezeichnet dokumentiert, aktiv gepflegt und liefert out of the box: Navigation, Suche, Mobile-Ansicht, Dark Mode und Syntax-Highlighting.
Ein paar bewusste Konfigurationsentscheidungen:
# mkdocs.yml (Auszug)
theme:
name: material
font: false # keine Google Fonts → DSGVO-konform
features:
- toc.integrate # TOC in linke Navigation, nicht rechte Sidebar
use_directory_urls: false # einfachere URLs, besser für rsync-Deploy
font: false ist ein Detail, das leicht übersehen wird. Ohne diese Option lädt das Material Theme Google Fonts nach – ein DSGVO-Problem, das keiner braucht. Mit font: false greift das Theme auf Systemfonts zurück; die Website lädt schneller und ohne externe Anfragen.
use_directory_urls: false bedeutet, dass kapitel/teil2-01-domains.md als teil2-01-domains.html gebaut wird, nicht als teil2-01-domains/index.html. Das klingt wie ein Detail, macht aber das Deployment und lokale Vorschau einfacher, weil keine Verzeichnisstruktur auf dem Server gespiegelt werden muss.
Der Python-Preprocessing-Hook
MkDocs erlaubt es, Python-Hooks einzuklinken, die vor dem Build laufen. Ich nutze das, um Markdown-Eigenheiten zu normalisieren, die Pandoc und MkDocs unterschiedlich interpretieren:
# hooks/preprocess.py
def on_page_markdown(markdown, **kwargs):
# Pandoc-spezifische Syntax bereinigen
# Fußnoten normalisieren
# Interne Links anpassen
return markdown
Das ist eine kleine Stellschraube, die aber viel Schmerz erspart: Ohne sie müsste ich entweder zwei verschiedene Markdown-Dialekte pflegen oder in beiden Tools mit Ausnahmen arbeiten.
PDF: Pandoc + XeLaTeX
Für das PDF setze ich auf Pandoc als Konverter und XeLaTeX als Typsatz-Engine. Das klingt nach Overhead – und ist es auch. Aber das Ergebnis ist professionell: echte Typografie, korrekte Silbentrennung, Inhaltsverzeichnis mit Seitenzahlen, Fußnoten als Fußnoten.
pandoc \
--from=markdown \
--to=pdf \
--pdf-engine=xelatex \
--metadata-file=metadata.yaml \
--lua-filter=appendix.lua \
--toc \
--number-sections \
-o output/survival-guide.pdf \
kapitel/*.md
Die metadata.yaml enthält Autor, Titel, Sprache und LaTeX-Präambel für Schriftarten, Seitenränder und Kopfzeilen.
Das Lua-Filter-Problem: Anhang-Nummerierung
Das war die kniffligste Stelle im gesamten Setup – und hat mich länger beschäftigt als der gesamte Rest zusammen.
Das Problem: Im Buch gibt es einen Anhang mit mehreren Abschnitten. In LaTeX macht man das mit \appendix, woraufhin Kapitel automatisch als „Anhang A", „Anhang B" nummeriert werden. MkDocs kennt \appendix nicht. Pandoc mit --number-sections nummeriert alle Header durch – auch den Anhang, der dann plötzlich Kapitel 8, 9, 10 heißt.
Die Lösung: Ein eigener Lua-Filter für Pandoc, der Anhang-Header erkennt und umbenennt:
-- appendix.lua
local in_appendix = false
local appendix_counter = 0
local section_counters = {}
function Header(el)
-- Erkenne den Einstiegspunkt in den Anhang
if el.identifier == "appendix" then
in_appendix = true
appendix_counter = 0
return el
end
if not in_appendix then
return el
end
if el.level == 1 then
appendix_counter = appendix_counter + 1
section_counters = {}
local letter = string.char(64 + appendix_counter) -- A, B, C, ...
-- Präfix vor den Titel setzen
table.insert(el.content, 1, pandoc.Str("Anhang " .. letter .. ": "))
return el
end
-- Level 2: A.1, A.2, ...
if el.level == 2 then
local letter = string.char(64 + appendix_counter)
section_counters[2] = (section_counters[2] or 0) + 1
section_counters[3] = nil
local prefix = letter .. "." .. section_counters[2]
table.insert(el.content, 1, pandoc.Str(prefix .. " "))
return el
end
return el
end
Der Filter läuft bei jedem Pandoc-Build. Was vorher manuelles Eingreifen erforderte, läuft jetzt vollautomatisch – egal wie viele Anhang-Abschnitte hinzukommen.
EPUB: Pandoc als Konverter
EPUB ist der einfachste Teil des Setups, weil Pandoc das Format nativ unterstützt und kaum Konfiguration braucht:
pandoc \
--from=markdown \
--to=epub \
--epub-cover-image=bilder/cover.png \
--metadata-file=metadata.yaml \
--lua-filter=appendix.lua \
-o output/survival-guide.epub \
kapitel/*.md
Der gleiche Lua-Filter läuft auch hier, damit die Anhang-Nummerierung konsistent bleibt. Das EPUB landet dann automatisch im Website-Build, damit Leser es direkt herunterladen können.
Der HTML-Splitter: Für den CMS-Import
Ein Bonusfeature, das ich für eine mögliche spätere Nutzung gebaut habe: split_html.py teilt den Pandoc-HTML-Output in einzelne Dateien auf – eine pro Abschnitt.
Das ist nützlich, wenn man Inhalte in ein CMS importieren möchte, das keine großen HTML-Blöcke verarbeiten kann. Der Splitter:
- Splittet an
<h1>und<h2>-Grenzen - Bereinigt Pandoc-Artefakte (class/id-Attribute)
- Ersetzt Checkbox-Listen durch Platzhalter
- Benennt Dateien nach Schema
<index>--<quelldatei>--<slug>.html
Aktuell nutze ich das nicht aktiv – aber der Code ist da, wenn ich ihn brauche.
Das Makefile
Alles wird über ein Makefile gesteuert. Die wichtigsten Targets:
pdf:
pandoc $(PDF_FLAGS) -o output/survival-guide-entwurf.pdf $(KAPITEL)
epub:
pandoc $(EPUB_FLAGS) -o output/survival-guide-entwurf.epub $(KAPITEL)
website:
mkdocs build
cp output/*.pdf output/*.epub site/ 2>/dev/null || true
deploy:
rsync -avz --delete site/ $(DEPLOY_TARGET)
deploy-all: pdf epub website deploy
make deploy-all ist der einzige Befehl, den ich für ein vollständiges Release brauche. Das Makefile übernimmt die Reihenfolge, kopiert PDF und EPUB in den Website-Build und schiebt alles auf den Server.
Was ich bewusst weggelassen habe
Genauso wichtig wie das, was ich gebaut habe, ist das, was ich nicht gebaut habe:
Kein CMS. Kein WordPress, kein Headless-CMS, kein Strapi. Markdown-Dateien in Git sind mein CMS. Das ist simpler, versionierbar und braucht keine Wartung.
Kein JavaScript-Framework. Die Website hat kein React, kein Vue, kein Next.js. MkDocs baut statisches HTML. Die Seite lädt in unter einer Sekunde – weil da schlicht nichts ist, was laden müsste.
Keine Google Fonts. Wie oben beschrieben: font: false in der MkDocs-Konfiguration. Systemfonts sind schnell, datenschutzkonform und für Fließtext mehr als ausreichend.
Kein separates Staging-System. Ich baue lokal vor, schaue es mir an, deploye. Für ein Einzelprojekt ohne Team reicht das vollständig.
Was ich beim nächsten Mal anders machen würde
Früher mit dem Lua-Filter anfangen. Ich habe die Anhang-Nummerierung lange manuell gepflegt, bevor ich den Filter geschrieben habe. Das war unnötige Arbeit.
use_directory_urls: false von Anfang an setzen. Das nachträglich zu ändern bricht alle internen Links. Einmal richtig konfigurieren, nie wieder anfassen.
Den Preprocessing-Hook früher strukturieren. Der Hook ist über die Zeit gewachsen und könnte sauberer sein. Bei einem neuen Projekt würde ich ihn von Anfang an als eigenständiges Modul denken.
Fazit
Das Setup ist für ein Buchprojekt dieser Größe erstaunlich leistungsfähig. Der Einstieg in Pandoc + LaTeX hat eine Lernkurve – aber die zahlt sich aus. Was ich einmal konfiguriert habe, läuft seitdem ohne Wartung.
Und das Beste: Ich kann mich auf das Schreiben konzentrieren, nicht auf das Tooling.
Den IT-Survival-Guide findest du unter it-survival-guide.de.