Blog de Raúl Salinas

Von Markdown zu WordPress ohne Schmerzen: So funktioniert mein Blog-System in Python

Lange Zeit habe ich Artikel in Markdown geschrieben, und wenn es an der Zeit war, sie auf WordPress zu veröffentlichen, trat immer die gleiche Reibung auf: Die Diagramme (vor allem Mermaid) wurden nicht richtig übernommen, die Bilder mussten manuell vorbereitet werden, und der Ablauf endete in einer Mischung aus sich wiederholenden und leicht zu brechenden Schritten.

Die anfängliche Motivation war sehr einfach: Ich wollte ein Werkzeug, das mir, gegeben ein Markdown-Dokument mit Diagrammen, die Bilder generiert und mir einen Inhalt bereitstellt, der bereit ist, in WordPress eingebettet zu werden. Ohne endloses Kopieren und Einfügen, ohne verstreute Skripte und ohne sich an alle Schritte erinnern zu müssen.

Mit der Zeit entwickelte sich dieses anfängliche Skript zu etwas Ehrgeizigerem: einem vollständigen System zur Verwaltung mehrerer Blogs vom Terminal aus, jeder mit seinen Artikeln in verschiedenen Sprachen versioniert, kontrollierte Veröffentlichung auf einem oder mehreren WordPress-Servern, unterstützte Übersetzung, Statusprüfungen und Backups.

Das Ergebnis ist eine CLI in Python namens blog.py.

Welches Problem es löst

In meinem Fall war der Engpass nicht das Schreiben, sondern das Operieren:

  • Umwandlung von Mermaid-Diagrammen in Bilder,
  • Erstellung einer mit WordPress kompatiblen Version,
  • Synchronisierung verschiedener Sprachversionen,
  • Sicheres Veröffentlichen oder als Entwurf belassen,
  • Wissen, ob sich WordPress seit meinem letzten publish geändert hat,
  • Veröffentlichung desselben Blogs auf mehreren Servern (Spiegel),
  • und alles mit Backups wiederherstellen können.

blog.py vereint all dies unter einem einzigen Befehl.

Die zentrale Idee: Programm und Daten trennen

Das Element, das das System am meisten wachsen ließ, ist eine einfache, aber kraftvolle Unterscheidung: Das Programm geht einen Weg und die Daten einen anderen.

  • Das Programm (die .py und das Makefile) lebt im Stammverzeichnis des Repositories.
  • Die Daten leben pro Site: Jeder unabhängige Inhaltsblog hängt von sites/<site>/ ab.
templates/post.md          # gemeinsame Vorlage für 'new'
siteconfig.py              # lädt sites/<site>/site.toml + .env
sites/
    raulsa8/               # persönliche Site (raulsa8.blogs.uv.es)
        site.toml          # Sprachen, [[servers]], Telegram
        .env               # Geheimnisse (außerhalb von git): Passwörter, Token
        posts/<post>/
            <base>_ca.md   # Version auf Valencianisch
            <base>_es.md   # Version auf Spanisch …
            <base>_<lang>.<server>.state.json
        build/  backup/    # generiert (gitignored)
    veg/                   # eine andere Site, ein anderer Blog

Dies trennt klar zwei Arten von Konfiguration:

  • Was vom Blog (Site) abhängt: der Inhalt, das Frontmatter, die Diagramme und die unterstützten Sprachen. Es wird in site.toml definiert.
  • Was vom Server abhängt: die WordPress-URL, der Benutzer, das Passwort und der gesamte Status (post_id, Medien, Link). Deshalb wird der Status pro Server gespeichert.

Multi-Site und Multi-Server

Ein und dasselbe Programm verwaltet mehrere Blogs, und jeder Blog kann gleichzeitig auf mehreren WordPress-Servern veröffentlicht werden (Spiegel):

./blog.py -s raulsa8 publish el-meu-post ca     # explizite Site
./blog.py publish el-meu-post ca                  # Standardsite, auf ALLEN ihren Servern
./blog.py -S mirror publish el-meu-post ca        # auf einem bestimmten Server
  • -s/--site wählt den Blog aus. Wenn du ihn nicht angibst, wird derjenige verwendet, der default = true hat, oder $BLOG_SITE, oder der einzige verfügbare.
  • -S/--server wählt den Server aus. Bei Lesevorgängen (diff, status, tg-check) wird der primäre verwendet; bei Schreibvorgängen (publish, unpublish) wird auf alle Server zugegriffen, es sei denn, du gibst einen an.

Der Status pro Server (<base>_<lang>.<server>.state.json) speichert die post_id, die hochgeladenen Medien (mit ihrem sha256) und den Link auf diesem Server. Wenn die Datei nicht existiert, wurde dieser Beitrag dort noch nie veröffentlicht.

Technische Philosophie: Python orchestriert, Make führt aus

Die Idee ist diese: Python als freundliche Orchestrierungsschicht, Make als Ausführungsmaschine.

Die meisten Unterbefehle delegieren an make, so geht keine vorherige Funktionalität verloren und es werden keine Teile neu erstellt, die bereits gut funktionierten. Aber die kompliziertere Logik (Übersetzung, diff, status, tg-check, where, edit) lebt direkt in Python, um die Wartung zu vereinfachen.

Mit anderen Worten:

  • make bleibt die technische Basis,
  • blog.py bietet Ergonomie, Autovervollständigung und eine konsistente Schnittstelle.

Sprache immer explizit

Eine bewusste Designentscheidung: Es gibt keine Standardsprache. Jeder Befehl erfordert die Sprache explizit (oder --all-lang für alle Versionen). So wird nie versehentlich eine falsche Version veröffentlicht.

Typischer Arbeitsablauf

Ein typischer realer Ablauf könnte sein:

./blog.py new el-meu-post ca
./blog.py edit el-meu-post ca
./blog.py render el-meu-post ca
./blog.py publish-draft el-meu-post ca

Und wenn du es auf mehrere Sprachen skalieren möchtest:

./blog.py translate el-meu-post en --from ca
./blog.py translate el-meu-post es --from ca
./blog.py publish-all

Der Vorteil ist, dass du dich nicht an verstreute Befehle oder interne Pfade erinnern musst: Die CLI konzentriert das mentale Modell.

Autovervollständigung mit TAB

Eines der Details, das im Alltag am produktivsten ist, ist die Autovervollständigung von Sites, Servern, Postnamen und Sprachen.

Da blog.py auf argparse basiert, bietet argcomplete die Autovervollständigung: In diesem System ist sein globaler Hook bereits installiert, sodass es ausreicht, eine neue Shell zu öffnen und zu verwenden:

./blog.py <TAB>

Wenn es in deiner Sitzung nicht aktiv ist, kannst du es so aktivieren:

eval "$(register-python-argcomplete ./blog.py)"

Es mag ein kleines Detail sein, aber es reduziert dumme Fehler und beschleunigt den täglichen Betrieb erheblich.

Hauptunterbefehle (und wofür sie dienen)

blog.py bietet eine Reihe von Befehlen, die den gesamten Lebenszyklus des Inhalts abdecken:

  • list: listet verfügbare Artikel und Sprachen auf.
  • new: erstellt einen leeren Beitrag aus der Vorlage.
  • render: generiert Diagramme und erstellt wordpress.md.
  • extract: extrahiert Mermaid und erstellt wordpress.md.
  • diagrams: rendert nur die Mermaid-Diagramme zu Bildern.
  • publish: erstellt oder aktualisiert und veröffentlicht live.
  • publish-draft: erstellt oder aktualisiert als Entwurf.
  • publish-all: veröffentlicht alle Versionen aller Artikel.
  • unpublish: setzt einen Inhalt in den Entwurfsstatus zurück.
  • translate: übersetzt mit Claude und lädt als Entwurf hoch.
  • master: zeigt oder setzt die Master-Sprache eines Artikels (Ursprung der Übersetzungen).
  • diff: zeigt Unterschiede zwischen lokal und WordPress.
  • status: überprüft (parallel), ob WordPress seit unserem letzten publish geändert wurde.
  • pull: zeigt (ohne Integration) manuell in WordPress vorgenommene Änderungen.
  • bad-slugs: erkennt kollidierte Slugs (Suffix -2/-3) zur Verknüpfung von Übersetzungen.
  • lang-check: überprüft die tatsächliche Sprache der veröffentlichten Beiträge.
  • tg-check: überprüft, ob das Telegram-Bot-Token funktioniert.
  • backup: lädt den gesamten Blog in backup/ herunter.
  • export-static: generiert einen statischen HTML-Blog in static/.
  • clean / distclean: reinigen den build/ einer Version oder vollständig.
  • where: gibt den genauen Pfad der Datei aus (zum Beispiel, um sie mit vim zu öffnen).
  • edit: öffnet die Datei in deinem $EDITOR.

Nicht alle Befehle werden täglich verwendet, aber sie vereinheitlicht zu haben, vermeidet ad-hoc-Lösungen.

Mehrsprachigkeit und Versionierung: der Teil, der am meisten Wert bringt

Wenn ein technischer Blog wächst, wird es schwierig, Versionen in verschiedenen Sprachen zu pflegen, wenn keine Struktur vorhanden ist.

Dieses System löst dies, indem es sich auf zwei Säulen stützt:

  • Artikeldateien, die explizit nach Sprache verwaltet werden,
  • Versionierung mit Git, um Rückverfolgbarkeit, Überprüfung und Rollback zu gewährleisten.

Dies ermöglicht inkrementelles Arbeiten: Du schreibst in einer Basissprache, übersetzt bei Bedarf, überprüfst Unterschiede und veröffentlichst jede Version mit Kontrolle.

Da ich das kostenlose Polylang verwende, erfolgt die Sprachzuweisung und die Verknüpfung zwischen Übersetzungen einmal manuell im wp-admin. Ab dann aktualisiert publish den Inhalt, ohne diese Verknüpfung zu verlieren.

Veröffentlichen mit Sicherheitsnetz

Beim Veröffentlichen ist die Frage nicht, ob es einen Schreck geben wird, sondern wann. Das System berücksichtigt dies auf zwei Arten.

Erstens, publish bricht ab, wenn WordPress seit der letzten Veröffentlichung geändert wurde, um keine manuellen Änderungen zu überschreiben (du kannst mit --force erzwingen, wenn du weißt, was du tust). Der Befehl status warnt dich vor diesen Änderungen, bevor du etwas berührst, und pull zeigt sie dir als Diff (ohne sie zu integrieren). Außerdem ist publish idempotent: Es speichert eine Signatur des Inhalts und lädt nur das hoch, was sich geändert hat, sodass zwei aufeinanderfolgende publish-all keine Arbeit wiederholen. Und publish-all veröffentlicht die Artikel parallel (standardmäßig 4 auf einmal).

Zweitens ist der Befehl backup nicht optional: Das Herunterladen des vollständigen Blog-Status in einen lokalen Ordner (backup/) fügt ein echtes Sicherheitsnetz für Migrationen, schnelle Audits oder Wiederherstellung nach Fehlern hinzu.

Exportieren zu statischem HTML

Neben der Veröffentlichung auf WordPress kann das System eine vollständige statische HTML-Version des Blogs mit export-static generieren.

Es rendert alle Artikel und erstellt eine statische Site in sites/<site>/static/: eine Seite pro Artikel und Sprache, Indizes pro Sprache, Sprachwähler und ohne Kommentare. Die Suche stützt sich auf Google mit dem Operator site:, wobei base_url die öffentliche URL ist, unter der du das Statische hosten wirst (konfigurierbar in site.toml).

Es ist ideal, um eine navigierbare und unabhängige Kopie des Blogs zu haben, ohne von WordPress abhängig zu sein, um es zu lesen.

Integration mit Telegram

Neben dem Veröffentlichungsfluss berücksichtigt das System den operativen Teil von Telegram: Beim Veröffentlichen kann es in einem Kanal benachrichtigen, mit chat konfigurierbar pro Sprache.

Der Unterbefehl tg-check validiert, dass das Bot-Token korrekt funktioniert, was sehr nützlich ist, wenn du Benachrichtigungsautomatisierungen rund um den redaktionellen Prozess hast. In der Praxis vermeidet es, ein Anmeldeproblem zu spät zu entdecken: Du kannst die Integration vor dem Veröffentlichen überprüfen und die Werkzeugkette in einem gesunden Zustand halten.

Warum dieser Ansatz für mich funktioniert

Es ist kein riesiges Framework oder eine geschlossene Plattform. Es ist ein kleines, praktisches System, das aus bekannten Komponenten besteht:

  • Markdown als Quelle der Wahrheit,
  • Mermaid für die Diagramme,
  • Python zur Orchestrierung,
  • Make zur Ausführung,
  • WordPress (einer oder mehrere) als Ziel,
  • und statisches HTML als navigierbare Kopie.

Diese Kombination hat eine wichtige Tugend: Sie reduziert Reibung, ohne die technische Kontrolle zu opfern.

Abschluss

Das Werkzeug entstand, um ein spezifisches Problem zu lösen (Markdown mit Diagrammen in ein veröffentlichbares Format für WordPress zu überführen) und entwickelte sich zu einer vollständigen redaktionellen Produktionskette vom Terminal aus: mehrere Blogs, mehrere Sprachen, mehrere Server und statischer Export, alles unter einem einzigen Befehl.

Wenn du gerne lokal schreibst, alles versionierst und mit Vertrauen veröffentlichst, macht ein solcher Ansatz den Unterschied: weniger mechanische Arbeit, weniger Fehler und mehr Fokus auf das, was wirklich zählt, nämlich den Inhalt.

Figura 1
Figura 1