18. LinkedIn đ§
Implementiert, aber ohne LinkedIn-Freigabe noch nicht produktiv nutzbar. Verbinden,
Profil-/Organisationsanzeige, Content-Erstellung (echt + Mock-KI), EntwĂŒrfe, Planung und
Kalender sind gebaut, getestet und lauffÀhig. Was fehlt, sind zwei getrennte Freigaben von
LinkedIn: eine App-Konfiguration (LINKEDIN_CLIENT_ID/LINKEDIN_CLIENT_SECRET/
LINKEDIN_STATE_SECRET als Umgebungsvariablen) fĂŒr die Verbindung selbst, und zusĂ€tzlich
eine manuell von LinkedIn geprĂŒfte Bewerbung um das Produkt âCommunity Management API" fĂŒr
das tatsÀchliche Veröffentlichen. Solange Letztere fehlt, zeigt die OberflÀche ehrlich
âBerechtigung fehlt" statt eine Veröffentlichung vorzutĂ€uschen.
Anders als bei Instagram sind bei LinkedIn IdentitĂ€t (Verbinden, Profil anzeigen) und Veröffentlichen zwei komplett getrennte Freigabestufen â die erste ist self-serve, die zweite erfordert eine manuelle LinkedIn-PrĂŒfung.
Was existiert
| Bereich | Route | Funktion |
|---|---|---|
| Konto verbinden | /linkedin/einstellungen | Direkter LinkedIn-OAuth-Login, Status/Berechtigungs-/Organisationsanzeige, erneut autorisieren, Trennen |
| Content erstellen | /linkedin | Ăbersicht aller EntwĂŒrfe (Filter/Suche/Sortierung), âNeuer Beitrag" |
| Entwurf bearbeiten | /linkedin/entwurf/:id | Zielkonto (Profil/Organisation), Bild-Upload, Text/Hashtags/CTA, KI-Erstellung aus Stichpunkten (echt + Mock-Vorschau), Ăberarbeiten (kĂŒrzen/erweitern/professioneller/Hook), Live-Vorschau, Speichern/Planen/Abbrechen/Direkt veröffentlichen |
| Content Kalender | /linkedin/kalender | Monatskalender + Liste geplanter BeitrÀge |
| Veröffentlichte BeitrÀge | /linkedin/veroeffentlicht | Liste bereits veröffentlichter BeitrÀge |
Backend: src/lib/linkedin-*.server.ts/*.functions.ts, OAuth-Callback unter
/api/public/oauth/linkedin/callback, Scheduler-Endpunkt unter
/api/public/linkedin/scheduled-publish/run, Datenmodell in den Migrationen
20260924210000_..._linkedinkern001.sql (Tabellen) und
20260925090000_..._linkedinschedulercron001.sql (pg_cron-Job fĂŒr geplante BeitrĂ€ge).
Berechtigungen
Zwei getrennte LinkedIn-Produkte:
| Berechtigung | Produkt | WofĂŒr |
|---|---|---|
openid, profile, email | Sign In with LinkedIn (OpenID Connect) â self-serve | Konto verbinden, Name/Profilbild/E-Mail anzeigen |
w_member_social | Community Management API â manuelle Freigabe nötig | BeitrĂ€ge im Namen des persönlichen Profils veröffentlichen |
w_organization_social | Community Management API â manuelle Freigabe nötig | BeitrĂ€ge im Namen einer Unternehmensseite veröffentlichen (nur fĂŒr Mitglieder mit Rolle ADMINISTRATOR/CONTENT_ADMIN/DIRECT_SPONSORED_CONTENT_POSTER auf dieser Seite) |
r_organization_social | Community Management API â manuelle Freigabe nötig | Organisations-BeitrĂ€ge lesen (Vorbereitung fĂŒr spĂ€tere Erweiterungen) |
r_organization_admin | separates Organisations-Zugriffsprodukt â noch nicht live verifiziert | Liste der Unternehmensseiten abrufen, auf die das Konto Zugriff hat (organizationAcls) â best effort, blockiert nie die Grundverbindung, siehe Code-Kommentar in linkedin-scopes.ts |
OAuth-Flow
Klassischer serverseitiger Redirect-Flow â strukturell identisch zur bestehenden Google-/Instagram-Anbindung:
- âMit LinkedIn verbinden" ruft
startLinkedinConnectauf (signierte, 10 Minuten gĂŒltigestate-URL) und leitet direkt aufhttps://www.linkedin.com/oauth/v2/authorizationweiter. - LinkedIn leitet nach Zustimmung auf
/api/public/oauth/linkedin/callback?code=âŠzurĂŒck. - Server tauscht den Code gegen einen Access-Token
(
POST https://www.linkedin.com/oauth/v2/accessToken) â die tatsĂ€chlich gewĂ€hrten Scopes kommen imscope-Feld der Antwort mit, unabhĂ€ngig von den angefragten. - Server liest IdentitĂ€t ĂŒber den OIDC-Userinfo-Endpunkt
(
GET https://api.linkedin.com/v2/userinfo). - Server listet (best effort) Unternehmensseiten ĂŒber
organizationAcls. - Token wird in
linkedin_accountsgespeichert. Fehltw_member_social/w_organization_social, wird der Statuspermission_missinggesetzt â die Verbindung selbst gilt trotzdem als hergestellt, nur Veröffentlichen ist blockiert. - Redirect-URI wird immer aus
PRONTO_APP_URLabgeleitet, nie aus dem eingehenden Host-Header (Lektion aus der Instagram-Anbindung: verhindert âInvalid redirect_uri" bei www/non-www-Abweichungen).
Veröffentlichungs-Flow (Posts API)
veroeffentlicheLinkedinContent() (linkedin-publish.server.ts):
- Optionales Bild:
POST /rest/images?action=initializeUploadâuploadUrl+urn:li:image:âŠâ Bilddaten perPUTauf dieuploadUrlâ Status wird gepollt, bisAVAILABLE(max. ~10 Sekunden, sonst Fehler statt endlosem Warten). POST https://api.linkedin.com/rest/postsmitauthor(Person- oder Organisations-URN je nach gewĂ€hltem Zielkonto),commentary(Text + Hashtags + CTA â LinkedIn hat kein separates Hashtag-/CTA-Feld beim Erstellen, deshalb Teil des sichtbaren Texts, siehebaueCommentary()),visibility: PUBLIC,lifecycleState: PUBLISHED.- Erfolg = HTTP 201 und Post-ID im Header
x-restli-idâ nur dann wird der Entwurf aufveroeffentlichtgesetzt und eine AktivitĂ€t im bestehendenaktivitaeten-Verlauf protokolliert (kein kĂŒnstlicher Lead,lead_idbleibtNULL). - Jeder andere Ausgang (fehlende Verbindung/Berechtigung/API-Fehler) Ă€ndert den Draft-Status nie fĂ€lschlich auf âveröffentlicht" â ein blockierter Versuch (keine Verbindung/Berechtigung) setzt den Status auch nicht auf âfehlgeschlagen" zurĂŒck, sondern auf den Stand vor dem Versuch (kein echter API-Versuch im eigentlichen Sinn).
Statusmodell: entwurf â geplant â wird_veroeffentlicht (wĂ€hrend des Versuchs,
sowohl beim manuellen Button als auch beim Scheduler) â veroeffentlicht/fehlgeschlagen.
veroeffentlicht ist ein Endzustand â serverseitig (darfStatusWechseln()) kann ein
veröffentlichter Beitrag nie mehr zu einem Entwurf zurĂŒckgestuft werden. abgebrochen ist
ein reiner Nutzer-Aktions-Status (manuelles Abbrechen eines geplanten Beitrags), wird nie
von einem API-Ergebnis gesetzt.
Scheduler fĂŒr geplante BeitrĂ€ge
verarbeiteLinkedinGeplanteBeitraege() (linkedin-scheduler.server.ts) wird alle 5 Minuten
per pg_cron ĂŒber /api/public/linkedin/scheduled-publish/run aufgerufen (Secret-Header
x-pronto-cron-secret, wiederverwendet dieselbe Vault-Secret-Infrastruktur wie die
bestehenden Follow-up-/IMAP-Poll-Jobs â keine neue Umgebungsvariable). Findet alle EntwĂŒrfe
mit status = 'geplant' und scheduled_at <= now() (max. 50 pro Lauf), veröffentlicht sie
ĂŒber dieselbe Kernfunktion wie der manuelle Button (veroeffentlicheLinkedinDraftIntern) â
ein defekter Entwurf blockiert nie den restlichen Sweep.
FĂŒr den Tester: TestfĂ€lle
- Unter
/linkedin/einstellungenâMit LinkedIn verbinden" â Status wechselt auf âVerbunden", zeigt Name/E-Mail/Profilbild/erteilte Berechtigungen/verfĂŒgbare Unternehmensseiten. - Unter
/linkedinâNeuer Beitrag" â im Editor Stichpunkte eingeben â âTestvorschau (ohne KI)" erzeugt einen eindeutig als â[Testvorschau]" markierten Platzhalter ohne Credit-Verbrauch; âMit KI erstellen" verbraucht einen Credit und erzeugt einen echten Vorschlag (erfordert konfigurierten KI-Anbieter). - Bild hochladen, Hashtags/CTA setzen, Zielkonto (Profil oder eine der angezeigten Unternehmensseiten) wĂ€hlen â Vorschau rechts aktualisiert sich live.
- âAls Entwurf speichern" oder âZur Veröffentlichung planen" (mit Zeitpunkt in der Zukunft â ein Zeitpunkt in der Vergangenheit wird serverseitig abgelehnt).
- âDirekt veröffentlichen" â erfordert die LinkedIn-Freigabe fĂŒr die Community Management API; ohne sie erscheint eine klare Fehlermeldung statt eines vorgetĂ€uschten Erfolgs.
- Ein geplanter Beitrag wird automatisch zum gewÀhlten Zeitpunkt vom Scheduler veröffentlicht (max. 5 Minuten Verzögerung durch den Cron-Takt).
- Nach erfolgreicher Veröffentlichung: Eintrag unter
/linkedin/veroeffentlichtmit Link zum echten LinkedIn-Post, sowie eine neue AktivitÀt im CRM-AktivitÀtsverlauf.
Bekannte EinschrÀnkungen
- Nur ein Bild pro Beitrag, kein Video/Dokument/Carousel â die Datenbank (
content_type,MAX_MEDIEN) ist so angelegt, dass diese spĂ€ter ergĂ€nzt werden können, ohne bestehende Daten zu brechen. - Kein automatischer Token-Refresh: LinkedIn-Access-Tokens laufen nach der von LinkedIn angegebenen GĂŒltigkeitsdauer ab; eine Refresh-Funktion existiert noch nicht (analog zur offenen Instagram-EinschrĂ€nkung) â Nutzer verbinden nach Ablauf manuell neu.
- Token-VerschlĂŒsselung: Access-Tokens liegen wie bei Instagram/WhatsApp im Klartext in der Datenbank (RLS + ausschlieĂlich service_role-Zugriff, nie ans Frontend ausgegeben) â siehe 25-security.md.
r_organization_admin-Scope fĂŒr die Organisationsliste ist gegen keinen echten LinkedIn-Account verifiziert (siehelinkedin-scopes.ts) â vor der App-Review-Einreichung erneut gegen die dann aktuelle Doku prĂŒfen.- Keine Datenlöschungs-Anfrage-Endpunkt speziell fĂŒr LinkedIn implementiert â die bestehende âTrennen"-Funktion löscht den Access-Token, aber keine historischen Content-EntwĂŒrfe automatisch. Offene LĂŒcke vor einer tatsĂ€chlichen App-Review-Einreichung.
Datenschutz-/Nutzungsbedingungen-Anforderungen (fĂŒr die App-Review-Einreichung)
LinkedIn verlangt bei der Bewerbung um âCommunity Management API" typischerweise:
- Eine öffentlich erreichbare DatenschutzerklÀrung, die beschreibt, dass LinkedIn- Profildaten (Name, E-Mail, Profilbild) sowie Beitragsinhalte verarbeitet werden.
- Eine Beschreibung des konkreten Anwendungsfalls (Content-Erstellung/-Planung fĂŒr Unternehmen) inkl. Screenshots/Demo-Video des Flows oben.
- Nachweis, dass die App nur die tatsĂ€chlich benötigten Berechtigungen anfragt (siehe Tabelle oben â keine Berechtigung wird fĂŒr ungenutzte Funktionen angefragt).
Offene manuelle Schritte (LinkedIn-Konfiguration)
- Im LinkedIn Developer Portal eine neue App anlegen, âSign In with LinkedIn using OpenID Connect" aktivieren (self-serve).
- Unter âAuth" die Redirect-URL exakt eintragen:
https://<domain>/api/public/oauth/linkedin/callback. LINKEDIN_CLIENT_ID,LINKEDIN_CLIENT_SECRET(Werte aus dem Developer Portal) undLINKEDIN_STATE_SECRET(beliebiger zufĂ€lliger String, nur fĂŒr die CSRF-Signatur) als Umgebungsvariablen hinterlegen (Vercel-Projekteinstellungen).- Bewerbung um das Produkt âCommunity Management API" einreichen (manuelle LinkedIn-PrĂŒfung, nicht self-serve) â erst danach funktioniert âDirekt veröffentlichen" live.
- Migration
20260925090000_..._linkedinschedulercron001.sqlin Supabase ausfĂŒhren, damit geplante BeitrĂ€ge automatisch veröffentlicht werden â vor dem AusfĂŒhren prĂŒfen, ob die darin hinterlegte Produktions-URL (pronto.zentime.io, identisch zu allen bestehenden Cron-Jobs in diesem Projekt) noch die tatsĂ€chlich erreichbare Backend-Domain ist, da an anderer Stelle im Projekt inzwischen auchtrypronto.siteals Domain verwendet wird. - Bis zur Freigabe: Verbinden/Profil anzeigen/EntwĂŒrfe/Planen funktionieren bereits vollstĂ€ndig â das deckt alle TestfĂ€lle bis auf den tatsĂ€chlichen Veröffentlichungs-Schritt ab.
