A crisis feed. In three languages. In seconds.
Kuratierte, KI-übersetzte Echtzeit-News für die iranische Diaspora. RSS-In, Telegram-Out, volles RTL, unter 50 €/Monat.
Why this exists.
Für die iranische Diaspora — und Menschen im Iran selbst — gibt es während Krisenereignissen keine zentrale, mehrsprachige Stelle, die verifizierte Nachrichten in Echtzeit bündelt. Relevante Meldungen verteilen sich über Dutzende persischer Telegram-Kanäle, Twitter-Accounts und westliche Medien mit Verzögerung.
Bestehende Newsportale sind entweder nur auf Farsi (für die zweite Diaspora-Generation nicht lesbar), nur auf Englisch/Deutsch (ohne persischen Kontext) oder reine Aggregatoren ohne Krisen-Klassifikation und Push. AzadiFeed kuratiert RSS-Quellen automatisiert, übersetzt KI-gestützt in EN / DE / FA und pusht Breaking News in Sekunden an Telegram-Abonnenten mit Stadt-Filter.
The box it had to fit in.
How it runs.
Four deliberate choices.
Coolify + Docker statt Vercel.
setInterval-Scheduler in src/lib/scheduler.tsvercel.json Cron JobsMongoDB als Queue, statt Redis.
pending_feeds → published_feeds) mit aiProcessed: false-Flag als Work-Queue, Distributed-Lock via Mongo-DokumentOpenAI Multi-Model-Routing zur Laufzeit.
src/lib/openai-config.tsSocket.IO statt SSE oder Pusher.
src/pages/api/socket.ts) am eingebauten Next.js-Server — gleicher Host wie die API, optionaler Redis-Adapter für horizontale SkalierungThings that were not obvious.
RTL mit mixed LTR-Segmenten
#MahsaAmini) und Zahlen. Naive RTL-Setups brechen das Layout, weil Browser den Bidi-Algorithmus inkonsistent auf gemischte Strings anwenden.Aktueller Stand:
<html dir="rtl"> per Locale, Vazirmatn als Variable-Font mit unicode-range-Fallback auf Inter für lateinische Glyphen — der Browser-Bidi-Algorithmus (UAX #9) übernimmt den Rest. Trägt Mainstream-Cases. Auf der Roadmap: unicode-bidi: plaintext plus explizites Unicode-Wrapping \u202B…\u202C der OpenAI-Outputs — Bidi gehört in die Content-Pipeline, nicht ins Stylesheet.Distributed Lock via MongoDB-Dokument
Lösung in
src/lib/cron-lock.ts: atomares findOneAndUpdate auf ein locks-Dokument plus ein expireAfterSeconds: 0-TTL-Index auf expiresAt; Lock-Dauer per Job konfigurierbar (300–900 s). Der Prozess, der den Lock verliert, returnt früh. TTL fängt auch Crashes ab — kein hängender Prozess nach OOM-Kill.Content-Enrichment Fallback-Chain
Dreistufige Chain, verdrahtet in
src/app/api/cron/rss-sync/route.ts: (1) Artikel-URL via src/lib/content-fetcher.ts per JSDOM nach og:image / twitter:image / link rel=image_src scrapen, (2) Fallback auf die RSS-Image-Felder in Priorität: media:content (höchste Auflösung) > <enclosure> > media:thumbnail > erstes <img> im Content-HTML, (3) liefern beide Stufen nichts, rendert <NewsPlaceholder> eine Gradient-Card mit Kategorie- und Krisen-Typ-passendem Lucide-Icon — niemals ein Broken-Image. Jeder Netzwerk-Fetch ist in einen 10–15 s AbortController-Timeout gewickelt — eine tote Quelle blockiert die Pipeline nicht.Crisis-Relevance als Auto-Publish-Gate
crisisRelevanceScore: 0–100 (persistiert als crisisRelevance). Admin-konfigurierbarer Threshold (minCrisisRelevanceThreshold) entscheidet autoPost — darunter landet das Item in der Mod-Queue.Löst zwei Probleme auf einmal: (a) Wetter-News werden nicht als "Breaking" gepusht, (b) der Admin senkt den Threshold bei akuten Lagen temporär, um mehr durchzulassen.
What's running.
What I learned.
Budget als Architektur-Constraint.
Die 50-€/Monat-Grenze hat mehr gute Entscheidungen erzwungen als sie verhindert hat. MongoDB-als-Queue, Multi-Model-Routing, eigener Socket-Server — alles Entscheidungen, die ich ohne den Cap nicht so konsequent durchgezogen hätte. Constraints are features.
Bidi ist nicht CSS-Eigenschaft, sondern Produkt-Problem.
RTL mit eingestreutem LTR-Content braucht eine Content-Pipeline-Entscheidung, keine Stylesheet-Property. Wer das auf die CSS-Schicht schiebt, scheitert. Beim nächsten Multilingual-Projekt plane ich Bidi-Normalisierung von Anfang an als Translation-Output-Format ein.