← Zurueck zum Blog
MastodonAPINext.jsTypeScriptSelf-HostedSocial-Media

Mastodon-Integration — Ein fetch() reicht

Marco Carstensen·9. März 2026·5 Min. Lesezeit

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:

AspektX.comBlueskyMastodon
AuthPlaywright SessionApp Password + SDKBearer Token (permanent)
SDK noetigPlaywright@atproto/apiKeins — ein fetch()
Token-AblaufSession-CookiesPro LoginNie (bis widerrufen)
Worker noetigJa (PM2)NeinNein
Link-VorschauAutomatischVia FacetsAutomatisch (OpenGraph)
Zeichenlimit280300500
Neue Dateien~822

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:

  1. Einloggen auf der Mastodon-Instanz
  2. Einstellungen → Entwicklung → "Neue Anwendung"
  3. Berechtigung: nur write:statuses
  4. 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.tsunter 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:

  1. Admin-Authentifizierung pruefen
  2. Post laden und validieren
  3. Pruefen ob bereits auf Mastodon gepostet
  4. publishToMastodon() aufrufen
  5. Ergebnis in der Datenbank speichern
  6. 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:

SpalteTypBeschreibung
mastodon_post_idtextPost-ID
mastodon_post_urltextWeb-URL zum Post
mastodon_publish_statustextnull / published / failed
mastodon_publish_errortextFehlermeldung
mastodon_shared_attimestamptzVeroeffentlichungszeitpunkt

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-token

Fazit

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: