Cum am automatizat distribuția articolelor de jurnal cu n8n
Rar am publicat pe alte platforme articolele de pe jurnal pentru că este destul de anevoios să fac manual acest lucru. Nu este o muncă grea, dar îți ia timp să faci acest lucru.
Am rezolvat asta cu n8n, un instrument de automatizare open source pe care îl rulam deja pe NUC lângă celelalte servicii. Ideea este foarte simplă: jurnalul are un feed RSS, n8n verifică periodic acel feed și, când apare un articol nou, îl trimite automat pe toate platformele configurate.
Punctul de plecare: RSS Feed Trigger
Totul pornește de la un nod RSS Feed Trigger care ascultă feed-ul de la https://thinkroot.xyz/feed.php. L-am setat să verifice o dată la o oră, și n8n se ocupă de restul: ține evidența articolelor deja procesate, așa că nu trebuie să îmi fac griji că același articol va fi postat de două ori, pentru că am lăsat activată opțiunea Read Already Seen.
Când feed-ul conține un articol nou, nodul produce un obiect JSON cu câmpurile pe care le folosesc în nodurile următoare: title, link, contentSnippet, isoDate și altele, în funcție de ce publică feed-ul respectiv. De acolo, workflow-ul se ramifică în patru direcții, câte una pentru fiecare platformă.
Mastodon
Mastodon are un API REST simplu și bine documentat, așa că integrarea e directă. Adaugi un nod HTTP Request cu metoda POST îndreptat spre https://linux.social/api/v1/statuses, configurezi autentificarea de tip Header Auth cu un Bearer token pe care îl obții din interfața Mastodon la Preferences → Development → New Application (scope-ul necesar e write:statuses), și în body trimiți un JSON de forma:
{
"status": "{{$json.title}}\n\n{{$json.link}}",
"visibility": "public"
}
Titlul și link-ul articolului apar în statusul de pe Mastodon exact cum le-ai scris în expresie.
Discord
Pentru Discord am folosit nodul Discord unde am ales acțiunea Send a message, dar în loc de API cu token am mers pe un webhook de canal, care e mai simplu de configurat: creezi webhook-ul din setările canalului Discord, copiezi URL-ul și faci un POST cu un body JSON care conține câmpul content. Dacă vrei ceva mai îngrijit vizual, poți folosi un embed Discord cu titlu, descriere și URL separat.
Singura problemă pe care am întâlnit-o la început era că pubDate returna undefined - câmpul cu data din feed-ul meu RSS se numea isoDate, nu pubDate. Dacă întâlnești o eroare similară, verifică tab-ul JSON al nodului RSS Trigger să vezi exact cum se numesc câmpurile înainte să le referențiezi în nodurile următoare.
Matrix
Matrix a fost puțin mai complicat, în principal pentru că API-ul lui folosește PUT în loc de POST pentru trimiterea mesajelor, și URL-ul are o structură mai puțin obișnuită. Nodul Matrix arată astfel:
- Method:
PUT - URL:
https://chat.linuxromania.ro/_matrix/client/v3/rooms/!ID_CAMEREI:linuxromania.ro/send/m.room.message/{{Date.now()}} - Authentication: Header Auth cu
Authorization: Bearer <access_token> - Body:
{
"msgtype": "m.text",
"body": "{{$json.title}}\n\n{{$json.link}}"
}
{{Date.now()}} la sfârșitul URL-ului servește ca transaction ID unic, pe care Matrix îl cere ca să poată identifica fiecare mesaj în parte și să evite duplicatele în caz de retry. Token-ul de acces al contului Matrix îl găsești în Element la Settings → Help & About → Advanced.
Un lucru important pe care l-am descoperit pe parcurs: când generezi token-ul, trebuie să selectezi doar permisiunile de tip client, nu toate permisiunile disponibile - dacă bifezi totul, token-ul nu funcționează pentru trimiterea mesajelor. Această problemă am întâlnit-o pentru că nu folosesc instanța matrix.org, ci instanța chat.linuxromania.ro. La alte instanțe de Matrix poate că este de ajuns doar token-ul implicit.
ID-ul camerei arată ca !abc123:linuxromania.ro și îl găsești în Room Settings → Advanced → Internal room ID.
Bluesky
Bluesky a fost cel mai laborios dintre toate, pentru că nu există un nod nativ în n8n pentru AT Protocol, protocolul pe care îl folosește Bluesky. Am rezolvat asta cu două noduri HTTP Request legate în serie.
Primul nod face login și obține un token temporar:
- Method:
POST - URL:
https://bsky.social/xrpc/com.atproto.server.createSession - Body:
{
"identifier": "thinkroot.bsky.social",
"password": "parola-ta-de-aplicatie"
}
Răspunsul conține câmpul accessJwt pe care îl vei folosi în nodul următor. Deoarece credențialele de tip Header Auth din n8n nu acceptă expresii dinamice, autentificarea se face manual: setezi Authentication pe None, activezi Send Headers și adaugi un header Authorization cu valoarea Bearer {{$node["Bluesky Login"].json.accessJwt}}.
Al doilea nod trimite postarea efectivă:
- Method:
POST - URL:
https://bsky.social/xrpc/com.atproto.repo.createRecord - Body (Using Fields Below):
{
"repo": "thinkroot.bsky.social",
"collection": "app.bsky.feed.post",
"record": {
"$type": "app.bsky.feed.post",
"text": "{{$('RSS Feed Trigger').item.json.title}}",
"createdAt": "{{$now.toISO()}}",
"embed": {
"$type": "app.bsky.embed.external",
"external": {
"uri": "{{$('RSS Feed Trigger').item.json.link}}",
"title": "{{$('RSS Feed Trigger').item.json.title}}",
"description": "{{$('RSS Feed Trigger').item.json.contentSnippet.replace(/\\n/g, ' ').replace(/\\r/g, '').slice(0, 200)}}"
}
}
}
}
Un detaliu important: câmpul description din embed trebuie curățat manual de newline-uri, altfel caracterele speciale din contentSnippet strică JSON-ul. Expresia .replace(/\n/g, ' ').replace(/\r/g, '').slice(0, 200) rezolvă asta. De asemenea, am ales să trec la modul Using Fields Below în loc de Using JSON direct, pentru că n8n se ocupă singur de escaping în acel mod, ceea ce reduce mult riscul de erori.
Postarea apare pe Bluesky cu un card de preview care include titlul, descrierea și imaginea articolului, dacă feed-ul o oferă.
Când apare articolul pe platforme
Dacă publici un articol la 09:17, nu înseamnă că apare imediat pe Mastodon sau oriunde altundeva. RSS Feed Trigger-ul rulează din oră în oră, la fix intervalul stabilit de la prima execuție - dacă ultima rulare a fost la 01:37, următoarele vor fi la 02:37, 03:37 și tot așa. Articolul publicat la 09:17 va fi detectat abia la execuția de la 09:37, deci există un decalaj între momentul publicării și momentul distribuției.
Dacă workflow-ul a rulat și a marcat un articol ca „deja văzut" înainte să fie complet publicat (de exemplu dacă URL-ul era deja accesibil dar pagina nu era gata), n8n îl va ignora la execuțiile următoare din cauza deduplicării interne. În acest caz singura soluție e să rulezi workflow-ul manual din interfața n8n folosind butonul Test workflow.
Rezultatul final
Workflow-ul are un singur RSS Feed Trigger din care pleacă patru ramuri în paralel, câte una pentru Mastodon, Discord, Matrix și Bluesky. Când activezi workflow-ul din interfața n8n (butonul Activate din colțul dreapta sus), el rulează automat în fundal și distribuie fiecare articol nou pe toate platformele fără nicio intervenție manuală.
Acum când public ceva pe blog, în câteva minute apare peste tot. E genul de automatizare mică care economisește timp și elimină o sursă de oboseală pe care nici nu o observi că există până dispare.