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.