Skip to main content
backend21. September 202515 Min. Lesezeit

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.

automationapisocial-media
Social-Media-Posts automatisieren: Vom Content-Kalender zur API

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:

  1. Nutzer zur Autorisierungs-URL der Plattform weiterleiten mit der client_id Ihrer App und den angeforderten scopes.
  2. Nutzer erteilt die Berechtigung. Plattform leitet zurueck zu Ihrer redirect_uri mit einem Autorisierungs-code.
  3. Den code gegen ein access_token (und meist ein refresh_token) eintauschen.
  4. Das access_token fuer 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:

  1. Content wird ueber ein Web-Dashboard oder API erstellt (mit optionaler KI-gestuetzter Entwurfserstellung).
  2. Medien werden hochgeladen und fuer jede Zielplattform mit Sharp verarbeitet.
  3. Der Post gelangt in eine Freigabe-Warteschlange, in der ein Pruefer plattformspezifische Vorschauen sieht.
  4. Nach der Freigabe wird der Post mit einem UTC-Zeitstempel in der Datenbank geplant.
  5. Ein Cron-Job (oder eine geplante Cloud Function) prueft auf Posts, die innerhalb der naechsten Minute faellig sind.
  6. Fuer jeden faelligen Post authentifiziert sich der Publisher bei jeder Plattform, verarbeitet Medien-Uploads und veroeffentlicht.
  7. Ergebnisse werden pro Plattform gespeichert. Fehler loesen Wiederholungsversuche mit exponentiellem Backoff aus.
  8. Erschoepfte Wiederholungsversuche kommen in die Dead Letter Queue mit Slack-Benachrichtigungen.
  9. 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.

DU

Danil Ulmashev

Full Stack Developer

Interesse an einer Zusammenarbeit?