Social-Media-Posts automatisieren: Vom Content-Kalender zur API
Eine Social-Media-Automatisierungspipeline bauen — von der Content-Planung bis zur API-Integration mit den grossen Plattformen.

Social Media fuer ein Produkt zu managen bedeutet, konsistent auf mehreren Plattformen zu posten, jede mit ihren eigenen API-Eigenheiten, Authentifizierungsablaeufen, Medienanforderungen und Rate Limits. Nachdem ich denselben Inhalt dreimal pro Woche manuell auf vier Plattformen gepostet hatte, baute ich eine Automatisierungspipeline, die Planung, Formatierung, Medienverarbeitung und Veroeffentlichung uebernimmt — mit einem Freigabeschritt, damit nichts ohne menschliche Pruefung rausgeht. Dieser Beitrag behandelt die Architektur, die plattformspezifischen Fallstricke und die Abwaegungen zwischen Eigenbau und vorhandenen Tools.
Die Plattform-API-Landschaft
Jede grosse Plattform hat ihr eigenes API-Oekosystem, und die Entwicklererfahrung variiert dramatisch. Hier ist der ehrliche Stand bei jeder Plattform Anfang 2026.
Twitter/X API
Die X API hat seit dem Plattformwechsel erhebliche Aenderungen durchlaufen. Die aktuelle API (v2) bietet Endpoints fuer Posting, Medien-Upload und Analytics. Der kostenlose Tier erlaubt 1.500 Posts pro Monat und Lesezugriff auf eigene Posts, was fuer die meisten Automatisierungsanwendungen ausreicht.
Das Authentifizierungsmodell verwendet OAuth 2.0 mit PKCE fuer nutzerkontext-bezogene Aktionen (im Namen eines Nutzers posten) und OAuth 2.0 App-Only fuer Leseoperationen. Die API ist vernuenftig dokumentiert und das Developer Portal bietet klare Rate-Limit-Informationen.
import { TwitterApi } from 'twitter-api-v2';
const client = new TwitterApi({
appKey: process.env.TWITTER_API_KEY!,
appSecret: process.env.TWITTER_API_SECRET!,
accessToken: process.env.TWITTER_ACCESS_TOKEN!,
accessSecret: process.env.TWITTER_ACCESS_SECRET!,
});
async function postTweet(text: string, mediaIds?: string[]): Promise<string> {
const tweet = await client.v2.tweet({
text,
...(mediaIds && { media: { media_ids: mediaIds } }),
});
return tweet.data.id;
}
async function uploadMedia(filePath: string): Promise<string> {
const mediaId = await client.v1.uploadMedia(filePath);
return mediaId;
}
Wichtige Einschraenkungen: Posts haben ein Limit von 280 Zeichen (laenger fuer Premium-Nutzer). Bild-Uploads unterstuetzen JPEG, PNG, GIF und WEBP bis 5 MB. Video-Uploads unterstuetzen MP4 bis 512 MB, erfordern aber Chunked Upload fuer Dateien ueber 15 MB. Threads erfordern das Erstellen mehrerer Posts mit reply-Einstellungen, was zusaetzliche Komplexitaet hinzufuegt.
Instagram Graph API
Die API von Instagram ist Teil des Meta-Oekosystems und erfordert eine Facebook Business Page, die mit einem Instagram Professional Account verknuepft ist. Der Einrichtungsprozess ist beruechtigterweise umstaendlich — Sie brauchen eine Meta App, Business-Verifizierung (fuer bestimmte Features) und die richtige Kombination von Berechtigungen.
Der Posting-Ablauf besteht aus zwei Schritten: zuerst einen Media Container erstellen, dann veroeffentlichen.
async function postToInstagram(
igUserId: string,
imageUrl: string,
caption: string,
accessToken: string
): Promise<string> {
// Step 1: Create media container
const containerResponse = await fetch(
`https://graph.facebook.com/v19.0/${igUserId}/media`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
image_url: imageUrl, // Must be a publicly accessible URL
caption,
access_token: accessToken,
}),
}
);
const container = await containerResponse.json();
// Step 2: Publish the container
const publishResponse = await fetch(
`https://graph.facebook.com/v19.0/${igUserId}/media_publish`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
creation_id: container.id,
access_token: accessToken,
}),
}
);
const result = await publishResponse.json();
return result.id;
}
Wichtige Fallstricke:
- Bilder muessen unter einer oeffentlich zugaenglichen URL gehostet sein. Sie koennen keine Binaerdaten direkt hochladen. Das bedeutet, Sie muessen Medien auf S3 oder einem CDN hosten, bevor Sie posten.
- Karussell-Posts (mehrere Bilder) erfordern das Erstellen einzelner Container fuer jedes Bild und dann eines Karussell-Containers, der diese referenziert.
- Reels (Video) haben zusaetzliche Anforderungen: 9:16-Seitenverhaeltnis, zwischen 3 und 90 Sekunden, und das Video muss vollstaendig verarbeitet sein, bevor es veroeffentlicht werden kann.
- Access Tokens laufen ab. Langlebige Tokens halten 60 Tage und muessen periodisch erneuert werden.
LinkedIn API
Die API von LinkedIn fuer das Posten von Inhalten verwendet die "Community Management API" (frueher die "Share API"). Die Authentifizierung nutzt OAuth 2.0 mit dem Scope w_member_social fuer persoenliche Profile oder w_organization_social fuer Unternehmensseiten.
async function postToLinkedIn(
authorUrn: string, // "urn:li:person:xxx" or "urn:li:organization:xxx"
text: string,
accessToken: string
): Promise<string> {
const response = await fetch('https://api.linkedin.com/v2/posts', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Restli-Protocol-Version': '2.0.0',
'LinkedIn-Version': '202401',
},
body: JSON.stringify({
author: authorUrn,
commentary: text,
visibility: 'PUBLIC',
distribution: {
feedDistribution: 'MAIN_FEED',
targetEntities: [],
thirdPartyDistributionChannels: [],
},
lifecycleState: 'PUBLISHED',
}),
});
const postId = response.headers.get('x-restli-id');
return postId || '';
}
Wichtige Einschraenkungen: Die API-Dokumentation von LinkedIn ist ueber mehrere Versionen und Namenskonventionen verstreut. Das Rate Limit liegt bei 100 API-Aufrufen pro Tag pro Nutzer pro Anwendung fuer die meisten Endpoints, was fuers Posten grosszuegig, aber knapp ist, wenn Sie auch Analytics abrufen. Bild-Uploads erfordern einen separaten Upload-Ablauf mit einer registrierten Upload-Anfrage.
TikTok Content Posting API
Die API von TikTok fuer die Inhaltsveroeffentlichung ist relativ neu und staerker eingeschraenkt als bei anderen Plattformen. Sie muessen den Zugang zur Content Posting API speziell beantragen, und die Genehmigung kann Wochen dauern.
Der Posting-Ablauf umfasst die Initialisierung einer Veroeffentlichung, das Hochladen des Videos und die Bestaetiging der Publikation:
async function postToTikTok(
videoUrl: string,
caption: string,
accessToken: string
): Promise<string> {
// Initialize the publish
const initResponse = await fetch(
'https://open.tiktokapis.com/v2/post/publish/video/init/',
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
post_info: {
title: caption,
privacy_level: 'SELF_ONLY', // Start as private, change after review
disable_duet: false,
disable_comment: false,
disable_stitch: false,
},
source_info: {
source: 'PULL_FROM_URL',
video_url: videoUrl,
},
}),
}
);
const result = await initResponse.json();
return result.data.publish_id;
}
Wichtige Fallstricke: TikTok verlangt, dass alle automatisierten Posts als solche gekennzeichnet werden — es gibt spezielle API-Flags dafuer. Videos muessen die Inhaltsrichtlinien und Formatanforderungen von TikTok erfuellen. Die API hat strenge taegliche Posting-Limits pro Nutzerkonto. Am wichtigsten ist, dass sich das API-Oekosystem von TikTok haeufig aendert — pruefen Sie daher die aktuelle Dokumentation, bevor Sie implementieren.
Authentifizierungsablaeufe
Alle vier Plattformen verwenden OAuth 2.0, aber die Implementierungsdetails unterscheiden sich genug, um plattformspezifische Behandlung zu erfordern.
Der OAuth-Tanz
Der allgemeine Ablauf ist:
- Nutzer zur Autorisierungs-URL der Plattform weiterleiten mit der
client_idIhrer App und den angefordertenscopes. - Nutzer erteilt die Berechtigung. Plattform leitet zurueck zu Ihrer
redirect_urimit einem Autorisierungs-code. - Den
codegegen einaccess_token(und meist einrefresh_token) eintauschen. - Das
access_tokenfuer API-Aufrufe verwenden. Erneuern, wenn es ablaeuft.
// Generic OAuth handler
interface OAuthConfig {
clientId: string;
clientSecret: string;
redirectUri: string;
authorizationUrl: string;
tokenUrl: string;
scopes: string[];
}
async function exchangeCodeForToken(
config: OAuthConfig,
code: string
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
const response = await fetch(config.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
client_secret: config.clientSecret,
}),
});
const data = await response.json();
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in,
};
}
Token-Verwaltung
Tokens laufen ab. Instagram-Tokens halten 60 Tage. LinkedIn-Tokens halten 60 Tage. Twitter-Tokens gelten bis zum Widerruf (fuer OAuth 1.0a) oder sind kurzlebig mit Refresh Tokens (fuer OAuth 2.0). TikTok-Tokens halten 24 Stunden mit einem Refresh Token, der 365 Tage gueltig ist.
Sie brauchen ein Token-Erneuerungssystem:
interface StoredToken {
platform: string;
accessToken: string;
refreshToken: string;
expiresAt: Date;
}
async function getValidToken(platform: string): Promise<string> {
const stored = await db.tokens.findUnique({ where: { platform } });
if (!stored) throw new Error(`No token stored for ${platform}`);
// Refresh if expiring within 5 minutes
if (stored.expiresAt < new Date(Date.now() + 5 * 60 * 1000)) {
const refreshed = await refreshToken(platform, stored.refreshToken);
await db.tokens.update({
where: { platform },
data: {
accessToken: refreshed.accessToken,
refreshToken: refreshed.refreshToken || stored.refreshToken,
expiresAt: new Date(Date.now() + refreshed.expiresIn * 1000),
},
});
return refreshed.accessToken;
}
return stored.accessToken;
}
Architektur der Content-Planung
Das Planungssystem ist das Herzsktueck der Automatisierungspipeline. Es muss Zeitzonen, plattformspezifische Formatierung, Medienverarbeitung und Fehlerwiederherstellung handhaben.
Datenmodell
interface ScheduledPost {
id: string;
content: {
text: string;
media?: MediaAttachment[];
platformOverrides?: Record<string, { text?: string }>;
};
platforms: Platform[];
scheduledAt: Date; // UTC
timezone: string; // IANA timezone for display
status: 'draft' | 'approved' | 'scheduled' | 'publishing' | 'published' | 'failed';
results: Record<string, PostResult>;
retryCount: number;
createdBy: string;
approvedBy?: string;
approvedAt?: Date;
}
interface MediaAttachment {
originalUrl: string;
processedUrls: Record<string, string>; // Platform-specific processed versions
type: 'image' | 'video';
altText?: string;
}
interface PostResult {
platform: string;
postId?: string;
url?: string;
error?: string;
publishedAt?: Date;
}
Das platformOverrides-Muster
Jede Plattform hat unterschiedliche Zeichenlimits, Hashtag-Konventionen und Formatierungsnormen. Anstatt voellig separate Posts zu erstellen, verwende ich einen Basistext mit plattformspezifischen Ueberschreibungen:
const post: ScheduledPost = {
id: 'post-1',
content: {
text: 'Excited to announce our new feature: real-time order tracking. Customers can now see exactly where their delivery is. #product #launch',
platformOverrides: {
twitter: {
text: 'Just shipped: real-time order tracking. Your customers can now watch their delivery in real-time. #buildinpublic',
},
linkedin: {
text: 'Excited to announce our latest feature: real-time order tracking.\n\nThis was one of the most requested features from our restaurant partners. Customers can now see exactly where their delivery is, reducing "where is my food?" support tickets by an estimated 40%.\n\nBuilt with WebSockets and a lightweight location service. Happy to share the technical architecture with anyone interested.\n\n#productdevelopment #startups #foodtech',
},
},
},
platforms: ['twitter', 'instagram', 'linkedin'],
scheduledAt: new Date('2026-03-15T14:00:00Z'),
timezone: 'America/New_York',
status: 'approved',
results: {},
retryCount: 0,
createdBy: 'user-1',
};
Twitter bekommt eine knappe Version. LinkedIn bekommt eine laengere, professionellere Version. Instagram verwendet den Basistext als Bildunterschrift. Dieser Ansatz respektiert die Plattformnormen, ohne voellig separate Content-Kalender pflegen zu muessen.
Medienverarbeitung
Die Medienverarbeitung ist der fehleranfaelligste Teil der Pipeline. Jede Plattform hat unterschiedliche Anforderungen an Bildabmessungen, Dateigroessen, Formate und Seitenverhaeltnisse.
Verarbeitungspipeline
interface MediaRequirements {
maxWidth: number;
maxHeight: number;
maxFileSize: number; // bytes
supportedFormats: string[];
aspectRatios?: { min: number; max: number };
}
const platformRequirements: Record<string, MediaRequirements> = {
twitter: {
maxWidth: 4096,
maxHeight: 4096,
maxFileSize: 5 * 1024 * 1024, // 5MB
supportedFormats: ['jpeg', 'png', 'gif', 'webp'],
},
instagram: {
maxWidth: 1440,
maxHeight: 1440,
maxFileSize: 8 * 1024 * 1024,
supportedFormats: ['jpeg', 'png'],
aspectRatios: { min: 4 / 5, max: 1.91 },
},
linkedin: {
maxWidth: 4096,
maxHeight: 4096,
maxFileSize: 10 * 1024 * 1024,
supportedFormats: ['jpeg', 'png', 'gif'],
},
};
async function processMediaForPlatform(
originalUrl: string,
platform: string
): Promise<string> {
const requirements = platformRequirements[platform];
const image = await sharp(await downloadFile(originalUrl));
const metadata = await image.metadata();
let processed = image;
// Resize if exceeding max dimensions
if (
(metadata.width && metadata.width > requirements.maxWidth) ||
(metadata.height && metadata.height > requirements.maxHeight)
) {
processed = processed.resize(requirements.maxWidth, requirements.maxHeight, {
fit: 'inside',
withoutEnlargement: true,
});
}
// Enforce aspect ratio for Instagram
if (requirements.aspectRatios && metadata.width && metadata.height) {
const currentRatio = metadata.width / metadata.height;
if (
currentRatio < requirements.aspectRatios.min ||
currentRatio > requirements.aspectRatios.max
) {
// Crop to acceptable ratio
const targetRatio = Math.max(
requirements.aspectRatios.min,
Math.min(currentRatio, requirements.aspectRatios.max)
);
const newWidth = Math.round(metadata.height * targetRatio);
processed = processed.resize(newWidth, metadata.height, { fit: 'cover' });
}
}
// Convert to supported format
const format = requirements.supportedFormats.includes('webp') ? 'webp' : 'jpeg';
const buffer = await processed.toFormat(format, { quality: 85 }).toBuffer();
// Upload processed image and return URL
const processedUrl = await uploadToStorage(buffer, `processed/${platform}/${Date.now()}.${format}`);
return processedUrl;
}
Die Verwendung von Sharp fuer die Bildverarbeitung haelt die Pipeline schnell und behandelt Randfaelle wie EXIF-Rotation, Farbprofilkonvertierung und Formatkompatibilitaet. Fuer die Videoverarbeitung uebernimmt FFmpeg das Transcoding, aber Videoverarbeitung ist deutlich komplexer und ressourcenintensiver — ich lagere sie typischerweise an einen dedizierten Worker oder eine Cloud Function aus.
Rate Limits und Retry-Logik
Jede Plattform erzwingt Rate Limits, und sie ungeschickt zu erreichen, fuehrt zu fehlgeschlagenen Posts und potenziell temporaeren API-Sperren.
Rate-Limit-Tracking
interface RateLimitState {
platform: string;
remaining: number;
resetAt: Date;
limit: number;
}
class RateLimiter {
private limits: Map<string, RateLimitState> = new Map();
updateFromHeaders(platform: string, headers: Headers): void {
const remaining = parseInt(headers.get('x-rate-limit-remaining') || '100');
const resetTimestamp = parseInt(headers.get('x-rate-limit-reset') || '0');
this.limits.set(platform, {
platform,
remaining,
resetAt: new Date(resetTimestamp * 1000),
limit: parseInt(headers.get('x-rate-limit-limit') || '100'),
});
}
async waitIfNeeded(platform: string): Promise<void> {
const state = this.limits.get(platform);
if (!state) return;
if (state.remaining <= 1 && state.resetAt > new Date()) {
const waitMs = state.resetAt.getTime() - Date.now() + 1000; // 1s buffer
console.log(`Rate limited on ${platform}. Waiting ${waitMs}ms`);
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
}
Retry mit exponentiellem Backoff
Nicht alle Fehler sind permanent. Netzwerk-Timeouts, temporaere Serverfehler und Rate Limits sind alle wiederholbar. Datenbankfehler und Authentifizierungsfehler sind es nicht.
async function publishWithRetry(
post: ScheduledPost,
platform: string,
maxRetries: number = 3
): Promise<PostResult> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
await rateLimiter.waitIfNeeded(platform);
const result = await publishToPlatform(post, platform);
return { platform, postId: result.id, url: result.url, publishedAt: new Date() };
} catch (error) {
const isRetryable =
error instanceof RateLimitError ||
error instanceof NetworkError ||
(error instanceof ApiError && error.statusCode >= 500);
if (!isRetryable || attempt === maxRetries) {
return {
platform,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
const backoffMs = Math.min(1000 * Math.pow(2, attempt), 30000);
const jitter = Math.random() * 1000;
await new Promise((resolve) => setTimeout(resolve, backoffMs + jitter));
}
}
return { platform, error: 'Max retries exceeded' };
}
Der Jitter ist wichtig — ohne ihn erzeugen mehrere wiederholende Clients, die auf den gleichen Backoff-Zeitplan synchronisiert sind, "Thundering Herd"-Probleme, bei denen sie alle gleichzeitig erneut versuchen.
Content-Freigabe-Workflow
Automatisches Posten ohne menschliche Pruefung ist riskant. Ein Formatierungsfehler, ein taktloser Post waehrend einer Krise oder ein falscher Link koennen echten Schaden anrichten. Ich integriere einen verpflichtenden Freigabeschritt in die Pipeline.
Der Freigabeablauf
Content erstellt → Status: DRAFT
│
▼
Vorschau generiert → Pruefer benachrichtigt (Slack/E-Mail)
│
▼
Pruefer genehmigt → Status: APPROVED → Zur Veroeffentlichung geplant
│
oder lehnt ab → Status: DRAFT (mit Feedback) → Zurueck zum Ersteller
Der Vorschau-Schritt ist wichtig — er zeigt dem Pruefer genau, was auf jeder Plattform gepostet wird, mit Zeichenzaehlern, Medienvorschauen und Link-Vorschauen. Das faengt Formatierungsprobleme auf, die im Content-Editor nicht offensichtlich sind.
async function generatePreview(post: ScheduledPost): Promise<PlatformPreview[]> {
return post.platforms.map((platform) => {
const text =
post.content.platformOverrides?.[platform]?.text || post.content.text;
return {
platform,
text,
characterCount: text.length,
characterLimit: getCharacterLimit(platform),
isOverLimit: text.length > getCharacterLimit(platform),
mediaPreview: post.content.media?.map((m) => ({
url: m.processedUrls[platform] || m.originalUrl,
type: m.type,
altText: m.altText,
})),
estimatedReach: getEstimatedReach(platform),
};
});
}
function getCharacterLimit(platform: string): number {
const limits: Record<string, number> = {
twitter: 280,
instagram: 2200,
linkedin: 3000,
tiktok: 2200,
};
return limits[platform] || 2200;
}
Analytics-Erfassung
Nach dem Posten hilft das Sammeln von Engagement-Daten, zukuenftige Inhalte zu optimieren. Jede Plattform stellt Analytics ueber ihre API bereit, aber die Datenstruktur und Verfuegbarkeit variiert.
interface PostAnalytics {
postId: string;
platform: string;
collectedAt: Date;
impressions: number;
engagements: number;
clicks: number;
shares: number;
comments: number;
likes: number;
engagementRate: number;
}
async function collectAnalytics(postResult: PostResult): Promise<PostAnalytics | null> {
if (!postResult.postId) return null;
switch (postResult.platform) {
case 'twitter':
return collectTwitterAnalytics(postResult.postId);
case 'linkedin':
return collectLinkedInAnalytics(postResult.postId);
case 'instagram':
return collectInstagramAnalytics(postResult.postId);
default:
return null;
}
}
Ich sammle Analytics nach 1 Stunde, 24 Stunden und 7 Tagen nach dem Posten. Das erfasst sofortiges Engagement, Tagesperformance und Langzeit-Reichweite. Die Daten fliessen in ein einfaches Dashboard, das zeigt, welche Content-Typen, Posting-Zeiten und Plattformen das meiste Engagement erzeugen.
Bauen vs. Kaufen: Die ehrliche Bewertung
Bevor Sie ein eigenes System bauen, ueberlegen Sie, ob vorhandene Tools Ihre Anforderungen erfuellen.
Wann vorhandene Tools verwenden
Buffer (5-100 $/Monat) uebernimmt Planung, Multi-Plattform-Posting und grundlegende Analytics. Es unterstuetzt Twitter, Instagram, LinkedIn, Facebook, Pinterest und TikTok. Die API ist solide fuer programmatische Planung. Fuer die meisten kleinen Teams deckt Buffer 90 % der Beduerfnisse ab.
Hootsuite (ab 99 $/Monat) fuegt Teamzusammenarbeit, Content-Freigabe-Workflows und tiefergehende Analytics hinzu. Besser fuer groessere Teams mit mehreren Beteiligten.
Typefully (12-29 $/Monat) ist auf Twitter/X spezialisiert mit Thread-Unterstuetzung, Entwurfs-Zusammenarbeit und Zielgruppen-Analytics. Wenn Twitter Ihre primaere Plattform ist, ist es ausgezeichnet.
Later (ab 16,67 $/Monat) ist stark fuer visuell orientierte Plattformen (Instagram, Pinterest, TikTok) mit Medienbibliotheksverwaltung und visueller Kalenderplanung.
Wann selbst bauen
Selbst bauen ist sinnvoll, wenn:
- Sie tiefe Integration mit internen Systemen brauchen (CMS, Produktdatenbank, CRM)
- Ihre Content-Erstellung teilweise automatisiert ist (KI-generierte Entwuerfe, Template-Posts aus Produktereignissen)
- Sie individuelle Freigabe-Workflows brauchen, die vorhandene Tools nicht unterstuetzen
- Sie in einem Volumen posten, bei dem SaaS-Preise teuer werden (50+ Posts/Tag ueber Konten hinweg)
- Plattform-API-Features, die Sie brauchen, nicht von Drittanbieter-Tools bereitgestellt werden
Der hybride Ansatz
Verwenden Sie ein Planungstool (Buffer, Hootsuite) fuer manuellen Content und bauen Sie individuelle Automatisierung nur fuer programmatische Posts. Viele Planungstools haben APIs, mit denen Sie Posts programmatisch erstellen koennen, waehrend Sie ihr Dashboard fuer Pruefung und Freigabe nutzen.
// Using Buffer's API for scheduling
async function scheduleViaBuffer(
profileIds: string[],
text: string,
mediaUrls: string[],
scheduledAt: Date
): Promise<string> {
const response = await fetch('https://api.bufferapp.com/1/updates/create.json', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
access_token: process.env.BUFFER_ACCESS_TOKEN!,
text,
'profile_ids[]': profileIds.join(','),
scheduled_at: scheduledAt.toISOString(),
...(mediaUrls.length && { 'media[photo]': mediaUrls[0] }),
}),
});
const result = await response.json();
return result.updates[0].id;
}
Das gibt Ihnen die Zuverlaessigkeit und UI eines etablierten Tools bei gleichzeitiger Beibehaltung der programmatischen Flexibilitaet von eigenem Code.
Webhook-Benachrichtigungen
Wenn ein Post veroeffentlicht wird, fehlschlaegt oder signifikantes Engagement erhaelt, sollte das System das Team benachrichtigen. Ich verwende Webhooks, um Events an Slack zu pushen.
async function notifySlack(event: PostEvent): Promise<void> {
const color = event.type === 'published' ? '#36a64f' : '#ff0000';
const title =
event.type === 'published'
? `Posted to ${event.platform}`
: `Failed to post to ${event.platform}`;
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
attachments: [
{
color,
title,
text: event.type === 'published'
? `"${event.text.substring(0, 100)}..." — ${event.url}`
: `Error: ${event.error}`,
fields: [
{ title: 'Platform', value: event.platform, short: true },
{ title: 'Time', value: new Date().toLocaleString(), short: true },
],
},
],
}),
});
}
Fehlerbehandlungsmuster
Social-Media-APIs sind unzuverlaessig. Server fallen aus, Rate Limits aendern sich, Medienverarbeitung schlaegt fehl. Das System muss Fehler elegant handhaben, ohne Inhalte zu verlieren.
Die Dead Letter Queue
Posts, die nach allen Wiederholungsversuchen fehlschlagen, kommen in eine Dead Letter Queue zur manuellen Pruefung:
async function handleFailedPost(post: ScheduledPost, platform: string, error: string): Promise<void> {
// Update post status
await db.scheduledPosts.update({
where: { id: post.id },
data: {
status: 'failed',
results: {
...post.results,
[platform]: { platform, error },
},
},
});
// Add to dead letter queue
await db.failedPosts.create({
data: {
postId: post.id,
platform,
error,
originalContent: post.content,
failedAt: new Date(),
resolved: false,
},
});
// Notify team
await notifySlack({
type: 'failed',
platform,
text: post.content.text,
error,
});
}
Ein Dashboard zeigt alle fehlgeschlagenen Posts mit Retry-Buttons. Die meisten Fehler sind voruebergehend und gelingen beim manuellen Wiederholen. Persistente Fehler deuten meist auf Token-Ablauf oder API-Aenderungen hin, die Code-Updates erfordern.
Behandlung von Teilerfolgen
Ein Post, der auf drei Plattformen abzielt, kann auf zweien gelingen und auf einer fehlschlagen. Das System muss die Ergebnisse pro Plattform unabhaengig verfolgen, anstatt den gesamten Post als fehlgeschlagen zu behandeln:
async function publishToAllPlatforms(post: ScheduledPost): Promise<void> {
const results = await Promise.allSettled(
post.platforms.map((platform) => publishWithRetry(post, platform))
);
const postResults: Record<string, PostResult> = {};
let hasFailure = false;
results.forEach((result, index) => {
const platform = post.platforms[index];
if (result.status === 'fulfilled') {
postResults[platform] = result.value;
if (result.value.error) hasFailure = true;
} else {
postResults[platform] = { platform, error: result.reason.message };
hasFailure = true;
}
});
await db.scheduledPosts.update({
where: { id: post.id },
data: {
status: hasFailure ? 'failed' : 'published',
results: postResults,
},
});
}
Die Verwendung von Promise.allSettled statt Promise.all stellt sicher, dass ein Fehler auf einer Plattform das Posten auf den anderen nicht verhindert.
Alles zusammenfuegen
Die vollstaendige Automatisierungspipeline sieht so aus:
- Content wird ueber ein Web-Dashboard oder API erstellt (mit optionaler KI-gestuetzter Entwurfserstellung).
- Medien werden hochgeladen und fuer jede Zielplattform mit Sharp verarbeitet.
- Der Post gelangt in eine Freigabe-Warteschlange, in der ein Pruefer plattformspezifische Vorschauen sieht.
- Nach der Freigabe wird der Post mit einem UTC-Zeitstempel in der Datenbank geplant.
- Ein Cron-Job (oder eine geplante Cloud Function) prueft auf Posts, die innerhalb der naechsten Minute faellig sind.
- Fuer jeden faelligen Post authentifiziert sich der Publisher bei jeder Plattform, verarbeitet Medien-Uploads und veroeffentlicht.
- Ergebnisse werden pro Plattform gespeichert. Fehler loesen Wiederholungsversuche mit exponentiellem Backoff aus.
- Erschoepfte Wiederholungsversuche kommen in die Dead Letter Queue mit Slack-Benachrichtigungen.
- Analytics werden nach 1 Stunde, 24 Stunden und 7 Tagen gesammelt.
Ist das ueberengineert fuer einen Solo-Gruender, der zweimal pro Woche postet? Absolut — verwenden Sie Buffer. Aber fuer ein Produkt mit programmatischen Content-Anforderungen, ereignisgesteuerten Posts oder komplexen Freigabe-Workflows zahlt sich eine eigene Pipeline durch Konsistenz und Kontrolle aus. Die Plattform-APIs sind stabil genug und das Tooling ist ausgereift genug, dass die Implementierung geradliniges Engineering ist und kein Kampf gegen schlecht dokumentierte APIs. Das war noch vor zwei Jahren nicht der Fall.
Danil Ulmashev
Full Stack Developer
Interesse an einer Zusammenarbeit?