Blogmanager: CI/CD für Arme (und wie man SSG-Schmerzen lindert)
Wir müssen mal über ein extrem nerviges First-World-Problem in der Nerd-Bubble reden: Statische Site Generatoren (SSGs).
Versteht mich nicht falsch. Ich liebe Tools wie Hugo, MkDocs und Jekyll. Sie generieren pfeilschnelle, sichere Websites, komplett ohne Datenbank-Overhead oder nervige PHP-Sicherheitslücken. Kein WordPress-Schrott, kein MySQL-Tuning, einfach nur pures, sauberes HTML, CSS und eine Prise JavaScript. Du schreibst deine Texte in Markdown, tippst hugo build in die Konsole und bäm – fertig ist die Webseite.
Aber (und das ist ein massives ABER): Das Veröffentlichen von Posts in der Zukunft ist bei all diesen Tools der absolute Endgegner.
SSGs haben logischerweise kein eingebautes CMS, keinen Login-Bereich und vor allem keinen Cronjob. Wenn du heute am Sonntag extrem motiviert bist und drei Blogposts schreibst, die Dienstag, Mittwoch und Donnerstag online gehen sollen... Tja. Pech gehabt. Die Generatoren ignorieren Dateien mit zukünftigem Datum standardmäßig. Wenn du willst, dass der Post am Dienstag online geht, musst du am Dienstag um 18:00 Uhr deinen Laptop aufklappen, das Repository pullen, den Build-Befehl lokal ausführen und den generierten Output manuell per FTP oder rsync auf deinen Webserver schieben.
Das hat mich massiv abgefuckt. Und genau deshalb haben wir den Blogmanager gebaut. Ein "Poor Man's CI/CD" Tool, das diese ganze sinnlose Fleißarbeit automatisiert.
1. Die Architektur: Pragmatismus über alles
Der Blogmanager ist im Grunde genommen eine Orchestrierungs-Engine, die auf einem internen Server (bei mir eine olle Debian-Büchse im Netzwerkschrank) läuft.
Ich hatte keine Lust, dafür ein riesiges Kubernetes-Cluster mit ArgoCD und Jenkins hochzuziehen. Das wäre völlig am Ziel vorbeigeschossen. Also haben wir den Tech-Stack extrem simpel gehalten:
- Backend: Python 3.10+ mit FastAPI. Purer Luxus für schnelle asynchrone APIs.
- Frontend: Vanilla JavaScript, HTML, CSS. Keine React-Hölle, keine 500 Node-Dependencies, einfach eine saubere
index.html, die man in jedem Browser sofort versteht. - Datenbank: JSON. Ja, richtig gelesen. Wir nutzen keine fette PostgreSQL-Datenbank, sondern eine simple
/opt/blogpublisher/preapproved.jsonDatei als State-Store. Warum? Weil wir hier nur eine handvoll Pfade speichern. Overengineering ist der Tod kleiner Projekte. - Job Scheduler: APScheduler (Advanced Python Scheduler). Läuft einfach als Background-Prozess direkt im FastAPI-Backend mit.
Das Ziel war eine Desktop/Web-GUI, die genau zwei Dinge perfekt beherrscht: Preview und Preapprove.
![]()
(Screenshot: Das rudimentäre, aber verdammt effektive Dashboard des Blogmanagers.)
2. Der geniale "Preview" Hack
Wenn man in Markdown schreibt, hat man im Editor (z.B. VSCode) zwar eine Vorschau, aber man sieht nie das fertige Layout der Webseite (CSS, Menüs, Sidebars). Früher musste ich lokal einen Webserver starten (hugo server -D), um zu sehen, wie der Post aussieht. Nervig.
Im Blogmanager haben wir das Problem mit einem extrem eleganten Hack über FastAPI gelöst: Dynamic Directory Mounting.
Klickt der Autor im Web-Frontend auf "Preview" neben einem noch unveröffentlichten Post, passiert im Backend (core/generator.py) folgendes:
- Der Blogmanager feuert im Hintergrund den Generator-Befehl ab, hängt aber hart das Flag
--futureposts(oder--buildFuturebei Hugo) dran. - Das Tool baut die Webseite temporär und wirft den Output in einen Wegwerf-Ordner:
/tmp/blogmanager_previews/mein_blog. - In der
main.pyvon FastAPI haben wir vorab diesen/tmp-Pfad als statische Route gemountet:
python PREVIEW_DIR = "/tmp/blogmanager_previews" app.mount("/preview", StaticFiles(directory=PREVIEW_DIR), name="preview") - Das Backend gibt dem Frontend die URL
/preview/mein_blog/index.htmlzurück, und das JavaScript öffnet automatisch einen neuen Tab im Browser.
Bäm! Der Autor sieht die exakte, gerenderte Seite mit allen zukünftigen Posts, als wäre sie schon live. Das hat uns den lokalen Dev-Server komplett obsolet gemacht.
![]()
(Screenshot: Der Preview-Modus in Action. Oben der Button, zack – neuer Tab mit dem fertig gerenderten Layout.)
3. Der Preapprove-Workflow & APScheduler
Wenn der Preview gut aussieht, kommt der eigentliche Magic-Moment: Der Preapprove Button.
Ein Klick darauf, und das Backend trägt den Dateipfad dieses Blogposts in die erwähnte preapproved.json Datenbank ein. Das signalisiert dem System: "Dieser Post ist inhaltlich freigegeben. Kümmer dich um den Rest, sobald der Tag gekommen ist."
Hier kommt der APScheduler ins Spiel. Er läuft als Background-Job in der FastAPI-App und wird jede Nacht um 00:05 Uhr getriggert. Der Code im scheduled_publisher() Job ist simpel, aber mächtig:
Er lädt die JSON-Datei und vergleicht das heutige Datum (datetime.now().strftime('%Y-%m-%d')) mit dem Datum der freigegebenen Posts. Wenn das Datum eines Posts erreicht (oder überschritten) ist, startet die Pipeline:
- Git Pull: Zieht sich die allerneusten Änderungen aus dem GitLab-Repo.
- Build: Startet den regulären Build (ohne Future-Flag!), sodass der freigegebene Post ab sofort generiert wird.
- Upload: Wirft den
safe_ftp_uploadan, der den fertigen/siteOrdner per FTP auf den Webserver schiebt.
Danach wird der Post-Pfad aus der preapproved.json gelöscht. Der Autor hat die Füße hochgelegt und schläft, während der Server nachts die harte Arbeit macht.
4. CI-Pipeline Integration (The Git Webhook)
Jetzt kommt der Teil für die Enterprise-Nerds. Was passiert, wenn ich einen Post anpasse, ihn in GitLab pushe und möchte, dass der Blogmanager (der ja auf dem internen Server liegt) diese Änderungen sofort mitbekommt?
Wir haben das Tool so gebaut, dass es direkt mit GitLab CI/CD (oder Webhooks) kommunizieren kann.
In der main.py haben wir dafür einen ungeschützten Endpunkt eingebaut (er läuft ohnehin nur im internen Netz, spart euch die Zero-Trust-Predigt):
@app.post("/api/git/pull-all")
def pull_all_repos():
config = load_config()
# ... iteriert über alle hinterlegten Blogs ...
success, msg = git_pull(blog["project_dir"], blog.get("git_url"))
return {"success": True, "results": results}
In unserer .gitlab-ci.yml für das eigentliche Blog-Repository (dort wo die Markdown-Files liegen) gibt es einen simplen curl-Aufruf, der diesen Endpunkt triggert, sobald wir einen Push in den main-Branch machen.
Sobald der Code bei GitLab liegt, schickt GitLab einen Ping an unseren Blogmanager. Der macht sofort einen git pull, lädt die Änderungen lokal herunter, und die Frontend-GUI des Blogmanagers ist instant wieder up to date. Keine manuellen Syncs, kein SSH-Gebastel. Du committest, und der Blogmanager weiß Bescheid. Ein absoluter Gamechanger für den Workflow.
5. Deployment auf dem internen Server
Die ganze Magie passiert, wie gesagt, headless auf einem internen Linux-Server in unserem Netzwerk.
Für das Deployment gibt es ein fertiges Bash-Script (deploy_to_nas.sh), das den Python-Code und das JavaScript dorthin schiebt, ein virtuelles Environment (venv) aufsetzt, alle Dependencies aus der requirements.txt installiert und dann einen systemd Service (blogpublisher.service) registriert.
Das FastAPI-Backend läuft dann als Hintergrund-Daemon via Uvicorn (uvicorn backend.main:app --host 0.0.0.0 --port 8000).
Auf den Laptops der Autoren liegt nur noch ein nackter Browser-Link (oder ein kleines Desktop-Wrapper-Skript). Kein Python auf dem Client, kein Setup, einfach im Browser öffnen, "Preapprove" klicken und vergessen.
Fazit: Pragmatismus schlägt Overengineering
Der Blogmanager ist der lebende Beweis dafür, dass man nicht immer ein SaaS-Abo abschließen oder komplizierte Kubernetes-Pipelines bauen muss, um produktiv zu sein.
Mit ein paar hundert Zeilen Python (FastAPI + APScheduler) und einem simplen Web-Frontend haben wir den nervigsten Teil von Hugo/MkDocs/Jekyll komplett eliminiert. Wir können wieder entspannt am Wochenende vorarbeiten, 5 Artikel vorschreiben, alle in die Warteschlange werfen, und der Server tröpfelt sie brav über die Woche verteilt ins Internet.
Das Tool wird momentan intern hart genutzt und spart uns pro Woche mehrere Stunden blinde Klickarbeit. Vielleicht refactore ich das Ding irgendwann mal (vor allem das Hardcoding in den FTP-Uploads) und werfe es Open-Source auf GitHub. Bis dahin bleibt es unsere persönliche "CI/CD-Pipeline für Arme".
Stay automated, write more code!
Transparency Note: Dieser Blogpost entstand mit Hilfe meines KI-Assistenten. Der Agent (basiert auf Gemini) hat den Code des Blogmanagers (FastAPI, APScheduler, GitLab Webhooks) analysiert und die Architektur-Kapitel strukturiert. Ich habe als Autor nur noch drübergelesen, die Screenshots platziert und den Rant-Level (aka "No-Bullshit-Vibe") ordentlich nach oben geschraubt. 🤖 ✍️