Nach X.com via Playwright und Bluesky via AT Protocol war die naechste Plattform dran: Mastodon — das dezentrale, foederierte soziale Netzwerk.
Das Ergebnis: Die mit Abstand einfachste Social-Integration, die ich bisher gebaut habe. Kein SDK, kein OAuth-Flow, kein Token-Refresh, kein Worker. Ein einzelner fetch()-Call.
Warum Mastodon?
Mastodon ist Open Source, dezentral und hat eine wachsende Tech-Community. Fuer einen technischen Blog wie macip.de ist das die ideale Zielgruppe. Und im Gegensatz zu den anderen Plattformen macht Mastodon es Entwicklern extrem einfach:
| Aspekt | X.com | Bluesky | Mastodon |
|---|---|---|---|
| Auth | Playwright Session | App Password + SDK | Bearer Token (permanent) |
| SDK noetig | Playwright | @atproto/api | Keins — ein fetch() |
| Token-Ablauf | Session-Cookies | Pro Login | Nie (bis widerrufen) |
| Worker noetig | Ja (PM2) | Nein | Nein |
| Link-Vorschau | Automatisch | Via Facets | Automatisch (OpenGraph) |
| Zeichenlimit | 280 | 300 | 500 |
| Neue Dateien | ~8 | 2 | 2 |
Token erstellen — 30 Sekunden
Waehrend man bei X.com eine komplette Playwright-Session bootstrappen und bei Bluesky zumindest ein App Password erstellen und ein SDK installieren muss, ist der Mastodon-Setup ein Kinderspiel:
- Einloggen auf der Mastodon-Instanz
- Einstellungen → Entwicklung → "Neue Anwendung"
- Berechtigung: nur
write:statuses - Speichern → Access Token kopieren
Fertig. Kein Developer Portal, kein OAuth-Redirect, keine App-Freigabe. Der Token laeuft nie ab.
Der gesamte Client
Der Mastodon-Client in src/lib/mastodon/client.ts — unter 35 Zeilen:
import "server-only";
const MASTODON_INSTANCE_URL = process.env.MASTODON_INSTANCE_URL;
const MASTODON_ACCESS_TOKEN = process.env.MASTODON_ACCESS_TOKEN;
export async function publishToMastodon(
text: string,
url: string,
) {
const fullText = `${text}\n\n${url}`;
const response = await fetch(
`${MASTODON_INSTANCE_URL}/api/v1/statuses`,
{
method: "POST",
headers: {
Authorization: `Bearer ${MASTODON_ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
status: fullText,
visibility: "public",
language: "de",
}),
}
);
const data = await response.json();
return { postId: data.id, postUrl: data.url };
}Das wars. Kein npm install, keine Klasse, kein Login-Flow, kein Session-Management. Nur ein fetch() mit Bearer Token.
Zum Vergleich: Der Bluesky-Client braucht @atproto/api, Login, RichText-Parsing und Facet-Detection. Der X.com-Client braucht Playwright, eine persistente Browser-Session und hunderte Zeilen Selektoren.
API-Route
Die Route POST /api/blog/[id]/publish-mastodon folgt dem gleichen Muster wie bei Bluesky:
- Admin-Authentifizierung pruefen
- Post laden und validieren
- Pruefen ob bereits auf Mastodon gepostet
publishToMastodon()aufrufen- Ergebnis in der Datenbank speichern
- Logging in
x_publish_logs
Der Text wird aus dem gleichen x_main_tweet-Feld uebernommen, das auch fuer X.com und Bluesky verwendet wird — generiert vom lokalen Ollama-LLM. Die Blog-URL bekommt Mastodon-spezifische UTM-Parameter angehaengt.
Link-Vorschau — geschenkt
Ein grosser Vorteil gegenueber den anderen Plattformen: Mastodon generiert automatisch eine Link-Vorschau ueber OpenGraph-Tags. Sobald eine URL im Post steht, holt sich die Mastodon-Instanz im Hintergrund Titel, Beschreibung und Bild von der verlinkten Seite.
Bei Bluesky muss man die Facets fuer Links selbst berechnen. Bei X.com hat die Browser-Automatisierung keinen Einfluss auf die Vorschau. Bei Mastodon reicht es, die URL in den Text zu schreiben.
Da macip.de bereits saubere OpenGraph-Tags im Layout hat, funktioniert die Vorschau out-of-the-box.
Datenbank-Erweiterung
Fuenf neue Spalten auf blog_posts — gleiches Pattern wie bei Bluesky:
| Spalte | Typ | Beschreibung |
|---|---|---|
mastodon_post_id | text | Post-ID |
mastodon_post_url | text | Web-URL zum Post |
mastodon_publish_status | text | null / published / failed |
mastodon_publish_error | text | Fehlermeldung |
mastodon_shared_at | timestamptz | Veroeffentlichungszeitpunkt |
Die TypeScript-Typen in types.ts wurden entsprechend erweitert — wie bereits bei der Bluesky-Integration und dem Blogsystem-Aufbau beschrieben.
Dashboard-Integration
Im Blog-Editor erscheint ein neuer Mastodon-Bereich unter dem Bluesky-Abschnitt:
- Vorschau des Post-Textes
- "Auf Mastodon posten"-Button
- Status-Badge (veroeffentlicht / fehlgeschlagen)
- Link zum Mastodon-Post
In der Blog-Tabelle im Dashboard gibt es eine neue "Masto"-Spalte neben X und Bsky — mit dem Mastodon-Logo als Status-Icon.
Vergleich: Alle drei Integrationen
Nach drei Social-Media-Integrationen zeigt sich ein klares Muster:
X.com: ~500 Zeilen | Playwright + PM2 Worker | Session-Management
Bluesky: ~60 Zeilen | @atproto/api SDK | App Password + Login
Mastodon: ~35 Zeilen | Kein SDK (raw fetch) | Bearer Token
Die Komplexitaet korreliert direkt mit der API-Offenheit der Plattform. Je mehr eine Plattform ihre API einschraenkt, desto mehr Workarounds braucht man. Mastodon als Open-Source-Projekt macht es am einfachsten.
Env-Vars
Nur zwei Umgebungsvariablen — keine Secrets, keine Client IDs:
MASTODON_INSTANCE_URL=https://mastodon.social
MASTODON_ACCESS_TOKEN=dein-tokenFazit
Mastodon ist die bisher einfachste Social-Media-Integration im macip.de-Stack:
- 2 neue Dateien (Client + API-Route)
- ~35 Zeilen Client-Code
- Kein npm-Paket installiert
- Kein Worker noetig
- Kein Token-Refresh (Token laeuft nie ab)
- Automatische Link-Vorschau via OpenGraph
Wenn eine Plattform REST API + permanente Bearer Tokens + automatische Link-Vorschau bietet, braucht man keinen SDK-Wrapper. Ein fetch() reicht.
Weiterlesen:
- Bluesky-Integration — AT Protocol API und warum kein Worker noetig ist
- Playwright fuer X.com — Browser-Automatisierung als API-Ersatz
- Teaser-Generierung mit Ollama — Lokales LLM statt Cloud-KI
- Das Blogsystem — MDX, Supabase und Admin-Editor