← Zurueck zum Blog
LinkedInPlaywrightAutomatisierungPM2Next.jsTypeScriptSelf-HostedSocial-Media

LinkedIn-Integration — Playwright statt API, Queue statt Chaos

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

LinkedIn-Integration — Playwright statt API, Queue statt Chaos

Nach X.com via Playwright, Bluesky via AT Protocol und Mastodon via fetch() fehlte noch eine Plattform: LinkedIn — das professionelle Netzwerk, in dem technische Blog-Artikel tatsaechlich gelesen und geteilt werden.

Das Ergebnis: Eine Queue-basierte Playwright-Integration, die das gleiche Muster wie die X.com-Automatisierung nutzt — mit strengeren Rate-Limits und einem robusteren Fehlerhandling.

Warum nicht die LinkedIn API?

LinkedIn bietet zwar eine API, aber sie ist fuer Einzelentwickler de facto unbrauchbar:

  • Community Management API erfordert ein LinkedIn-Partner-Programm
  • Marketing Developer Platform verlangt eine Unternehmensseite + Approval-Prozess
  • Self-Service-Zugang fuer persoenliche Profile gibt es nicht

Wer einfach nur einen Post auf seinem eigenen Profil veroeffentlichen will, steht vor verschlossenen Tueren. Die Situation ist aehnlich wie bei X.com — nur dass X wenigstens eine (teure) API anbietet.

Die Konsequenz: Wieder Playwright.

Architektur im Vergleich

Die vier Social-Media-Integrationen unterscheiden sich deutlich in ihrer Komplexitaet:

AspektX.comBlueskyMastodonLinkedIn
MethodePlaywrightAT Protocol SDKREST APIPlaywright
AuthSession-CookiesApp PasswordBearer TokenSession-Cookies
Worker noetigJa (PM2)NeinNeinJa (PM2)
Rate-Limit60s AbstandKeinsKeins60 Min + 2/Tag
Link-VorschauAutomatischVia FacetsOpenGraphOpenGraph
Neue Dateien~8226
KomplexitaetHochMittelNiedrigHoch

LinkedIn und X.com teilen sich die gleiche Architektur-Kategorie: Browser-Automatisierung mit Queue und Worker. Der Link Shortener wird bei LinkedIn nicht benoetigt — das Zeichenlimit ist kein Problem.

Der Posting-Flow

Dashboard → API Route (202) → Supabase Queue → PM2 Worker → Playwright → LinkedIn
  1. Admin klickt "Auf LinkedIn posten" im Blog-Editor
  2. Die API-Route validiert (Admin? Veroeffentlicht? Teaser vorhanden?) und setzt linkedin_publish_status = 'queued'
  3. Der PM2-Worker pollt alle 5 Minuten nach neuen Eintraegen
  4. Playwright startet Chrome, laedt die gespeicherte Session und postet

Das gleiche Pattern wie beim X.com-Worker — mit einem entscheidenden Unterschied: LinkedIn ist deutlich strenger bei der Bot-Erkennung.

Session-Management

LinkedIn authentifiziert ueber Session-Cookies. Die Session wird einmalig per Bootstrap-Script erstellt:

npm run linkedin:bootstrap

Das oeffnet einen sichtbaren Chrome-Browser. Man loggt sich manuell bei LinkedIn ein und das Script speichert die Login-Cookies. Das muss nur einmal gemacht werden — danach funktioniert das automatische Posten, bis die Session irgendwann ablaeuft (typisch mehrere Wochen).

Bei jedem Posting speichert der Worker automatisch den aktuellen Cookie-Stand zurueck. Wenn LinkedIn die Cookies im Hintergrund refresht, gehen sie nicht verloren.

Anti-Bot-Massnahmen

LinkedIn erkennt Browser-Automatisierung aggressiv. Die Gegenstrategie ist dieselbe wie bei X.com:

  • Menschenaehnliches Timing — variables Tippen, natuerliche Pausen zwischen Aktionen, Jitter bei Wartezeiten
  • Browser-Fingerprint — Einstellungen, die den Browser wie einen regulaeren Desktop-Client erscheinen lassen
  • Deutsche Locale (de-DE) und Berliner Zeitzone

Die Timing-Funktionen werden aus dem X.com-Modul wiederverwendet. Kein Grund, das Rad neu zu erfinden.

Rate-Limiting — strenger als X.com

LinkedIn ist deutlich empfindlicher als X.com. Der Worker erzwingt drei Limits:

const POLL_INTERVAL_MS = 5 * 60 * 1000;   // Alle 5 Min pruefen
const MIN_INTERVAL_MS  = 60 * 60 * 1000;  // 60 Min zwischen Posts
const MAX_POSTS_PER_DAY = 2;               // Max. 2 Posts pro Tag
const MAX_ATTEMPTS = 3;                    // 3 Versuche, dann aufgeben

Zum Vergleich: Der X.com-Worker pollt jede Minute und hat kein Tageslimit. Bei LinkedIn ist Zurueckhaltung Pflicht — zu viele Posts fuehren zu Account-Einschraenkungen.

Challenge-Erkennung

Wenn LinkedIn misstrauisch wird, gibt es verschiedene Hindernisse: CAPTCHAs, Checkpoint-Seiten oder ein Redirect zum Login. Der Publisher erkennt alle drei Faelle automatisch — per URL-Analyse und DOM-Pruefung.

Bei einer Challenge beendet sich der Worker mit Exit-Code 1. PM2 startet ihn nach 60 Sekunden neu — aber der Status in der Datenbank bleibt auf challenge_detected oder review_required, bis manuell eingegriffen wird.

Zentralisierte Selektoren

LinkedIn aendert seine DOM-Struktur regelmaessig. Deshalb sind alle CSS-Selektoren in einer einzigen Datei zentralisiert. Wenn LinkedIn die Klassen aendert, muss nur diese eine Datei angepasst werden — nicht der gesamte Publisher-Code. Das gleiche Pattern nutzen wir auch bei der X.com-Integration.

Die API-Route

Die Route folgt dem gleichen Schema wie Bluesky und Mastodon — mit einem Unterschied: Sie gibt 202 Accepted zurueck statt das Ergebnis direkt zu liefern.

POST /api/blog/[id]/publish-linkedin

Die Route validiert:

  • Admin-Berechtigung (JWT-Check)
  • Post ist veroeffentlicht
  • Teaser-Text existiert (aus der Ollama-Generierung)
  • Noch nicht auf LinkedIn gepostet oder in der Queue

Dann wird linkedin_publish_status = 'queued' gesetzt und der Worker uebernimmt. Der Teaser-Text (x_main_tweet) wird wiederverwendet — er passt zu allen Plattformen, da er maximal 280 Zeichen hat.

PM2-Konfiguration

Der Worker laeuft als separater PM2-Prozess im Fork-Mode:

{
  name: 'linkedin-publisher',
  script: './node_modules/.bin/tsx',
  args: 'scripts/linkedin/publish-worker.ts',
  exec_mode: 'fork',
  instances: 1,
  env: { DISPLAY: ':99' },
  restart_delay: 60000,
  max_restarts: 3,
}

Wichtig: exec_mode: "fork" statt "cluster". Der Grund ist derselbe wie beim Teaser-Worker: tsx produziert im Cluster-Mode keine Logs. DISPLAY: ':99' verweist auf einen virtuellen Framebuffer (Xvfb) auf dem Server, da Playwright auch im Headless-Mode gelegentlich ein Display braucht.

Fehler-Status-Mapping

Nicht jeder Fehler ist gleich. Der Worker mappt Error-Codes auf differenzierte Status-Werte:

Error CodeDB-StatusBedeutung
challenge_detectedchallenge_detectedLinkedIn blockiert — manueller Eingriff
session_expiredreview_requiredCookies abgelaufen — Bootstrap noetig
no_sessionreview_requiredKeine Session vorhanden
selector_failedselector_failedDOM hat sich geaendert
timeoutfailedSeite laed nicht
unknownfailedUnerwarteter Fehler

Im Dashboard zeigt die Blog-Tabelle den Status als farbiges Icon: Teal bei Erfolg, gedimmt bei Fehler, pulsierend blau bei laufender Verarbeitung.

Post-URL-Extraktion

LinkedIn liefert nach dem Posten keine direkte Bestaetigung wie Bluesky (AT-URI) oder Mastodon (Post-ID). Die Loesung: Nach dem Post navigiert der Worker zur eigenen Activity-Seite und extrahiert den neuesten Post-Link.

Das ist best-effort — wenn es scheitert, ist der Post trotzdem online. Die URL wird dann einfach nicht in der Datenbank gespeichert.

Was die vier Integrationen gemeinsam haben

Alle vier Social-Media-Kanaele teilen sich:

  • Den gleichen Teaser-Text (x_main_tweetlokal generiert mit Ollama)
  • Die gleiche Blog-URL mit UTM-Parametern (nur utm_source variiert)
  • Das gleiche Dashboard-UI-Pattern (Status-Badge, Post-Link, Action-Button)
  • Die gleiche Logging-Tabelle (x_publish_logs)

Der Unterschied liegt im Transport: API-Call (Bluesky, Mastodon) oder Browser-Automatisierung (X.com, LinkedIn). Die Queue-Architektur bei den Playwright-Integrationen puffert zuverlaessig — selbst wenn der Worker mal ausfaellt, bleibt der Post in der Warteschlange.

Fazit

LinkedIn ist die vierte und aufwaendigste Social-Media-Integration. Aber dank der bestehenden Patterns aus der X.com-Automatisierung war die Umsetzung ueberschaubar: Session-Management, menschenaehnliches Timing und Challenge-Erkennung waren schon geloest. Der LinkedIn-Adapter baut darauf auf und fuegt strengere Rate-Limits und differenziertes Fehlerhandling hinzu.

Der gesamte Social-Media-Stack — von der KI-Teaser-Generierung ueber den Link Shortener bis zum Cross-Posting auf vier Plattformen — laeuft komplett self-hosted, ohne externe APIs und ohne laufende Kosten. Ein Klick im Dashboard, den Rest erledigen die Worker.