Skip to main content
backend21 settembre 202516 min di lettura

Automazione dei Post sui Social Media: Dal Calendario dei Contenuti all'API

Costruire una pipeline di automazione dei social media — dalla pianificazione dei contenuti all'integrazione API con le principali piattaforme.

automationapisocial-media
Automazione dei Post sui Social Media: Dal Calendario dei Contenuti all'API

Gestire i social media per un prodotto significa pubblicare costantemente su diverse piattaforme, ognuna con le proprie peculiarità API, flussi di autenticazione, requisiti multimediali e limiti di frequenza. Dopo aver pubblicato manualmente lo stesso contenuto su quattro piattaforme tre volte a settimana, ho costruito una pipeline di automazione che gestisce la pianificazione, la formattazione, l'elaborazione dei media e la pubblicazione — con un passaggio di approvazione in modo che nulla venga pubblicato senza revisione umana. Questo post copre l'architettura, le insidie specifiche della piattaforma e i compromessi tra la costruzione di soluzioni personalizzate e l'utilizzo di strumenti esistenti.

Panorama delle API delle Piattaforme

Ogni piattaforma principale ha il proprio ecosistema API e l'esperienza dello sviluppatore varia drasticamente. Ecco lo stato onesto di ciascuna all'inizio del 2026.

API di Twitter/X

L'API di X ha subito cambiamenti significativi dalla transizione della piattaforma. L'attuale API (v2) offre endpoint per la pubblicazione, il caricamento di media e l'analisi. Il livello gratuito consente 1.500 post al mese e l'accesso in lettura ai propri post, il che è sufficiente per la maggior parte dei casi d'uso di automazione.

Il modello di autenticazione utilizza OAuth 2.0 con PKCE per azioni nel contesto dell'utente (pubblicazione per conto di un utente) e OAuth 2.0 App-Only per operazioni di lettura. L'API è ragionevolmente ben documentata e il portale per sviluppatori fornisce chiare informazioni sui limiti di frequenza.

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;
}

Limitazioni principali: I post hanno un limite di 280 caratteri (più lungo per gli utenti premium). I caricamenti di immagini supportano JPEG, PNG, GIF e WEBP fino a 5MB. I caricamenti di video supportano MP4 fino a 512MB ma richiedono il caricamento a blocchi per file superiori a 15MB. I thread richiedono la creazione di più post con impostazioni di reply, il che aggiunge complessità.

Instagram Graph API

L'API di Instagram fa parte dell'ecosistema Meta e richiede una Pagina Facebook Business collegata a un Account Professionale Instagram. Il processo di configurazione è notoriamente contorto — hai bisogno di un'App Meta, della verifica aziendale (per alcune funzionalità) e della corretta combinazione di permessi.

Il flusso di pubblicazione è in due fasi: prima crea un contenitore multimediale, poi pubblicalo.

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;
}

Insidie principali:

  • Le immagini devono essere ospitate su un URL pubblicamente accessibile. Non è possibile caricare dati binari direttamente. Ciò significa che è necessario ospitare i media su S3 o un CDN prima di pubblicare.
  • I post a carosello (immagini multiple) richiedono la creazione di contenitori individuali per ogni immagine, quindi un contenitore carosello che li referenzia.
  • I Reels (video) hanno requisiti aggiuntivi: rapporto d'aspetto 9:16, tra 3 e 90 secondi, e il video deve essere completamente elaborato prima della pubblicazione.
  • I token di accesso scadono. I token a lunga durata durano 60 giorni e necessitano di un aggiornamento periodico.

API di LinkedIn

L'API di LinkedIn per la pubblicazione di contenuti utilizza la "Community Management API" (precedentemente "Share API"). L'autenticazione utilizza OAuth 2.0 con lo scope w_member_social per i profili personali o w_organization_social per le pagine aziendali.

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 || '';
}

Limitazioni principali: La documentazione dell'API di LinkedIn è frammentata tra più versioni e convenzioni di denominazione. Il limite di frequenza è di 100 chiamate API al giorno per utente per applicazione per la maggior parte degli endpoint, il che è generoso per la pubblicazione ma stretto se si stanno anche estraendo analisi. I caricamenti di immagini richiedono un flusso di caricamento separato con una richiesta di caricamento registrata.

API di Pubblicazione Contenuti di TikTok

L'API di TikTok per la pubblicazione di contenuti è relativamente nuova e più restrittiva rispetto ad altre piattaforme. È necessario richiedere l'accesso specifico all'API di pubblicazione di contenuti e l'approvazione può richiedere settimane.

Il flusso di pubblicazione prevede l'inizializzazione di una pubblicazione, il caricamento del video e quindi la conferma della pubblicazione:

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;
}

Insidie principali: TikTok richiede che tutti i post automatizzati siano dichiarati come tali — esistono flag API specifici per questo. I video devono soddisfare le linee guida sui contenuti e i requisiti di formato di TikTok. L'API ha limiti di pubblicazione giornalieri rigorosi per account utente. Ancora più importante, l'ecosistema API di TikTok cambia frequentemente, quindi controlla la documentazione attuale prima di implementare.

Flussi di Autenticazione

Tutte e quattro le piattaforme utilizzano OAuth 2.0, ma i dettagli di implementazione differiscono abbastanza da richiedere una gestione specifica per piattaforma.

La Danza OAuth

Il flusso generale è:

  1. Reindirizza l'utente all'URL di autorizzazione della piattaforma con il client_id della tua app e gli scopes richiesti.
  2. L'utente concede il permesso. La piattaforma reindirizza alla tua redirect_uri con un code di autorizzazione.
  3. Scambia il code con un access_token (e di solito un refresh_token).
  4. Usa l'access_token per le chiamate API. Aggiornalo quando scade.
// 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,
  };
}

Gestione dei Token

I token scadono. I token di Instagram durano 60 giorni. I token di LinkedIn durano 60 giorni. I token di Twitter durano fino a revoca (per OAuth 1.0a) o sono a breve termine con token di aggiornamento (per OAuth 2.0). I token di TikTok durano 24 ore con un token di aggiornamento valido per 365 giorni.

Hai bisogno di un sistema di aggiornamento dei token:

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;
}

Architettura di Pianificazione dei Contenuti

Il sistema di pianificazione è il cuore della pipeline di automazione. Deve gestire i fusi orari, la formattazione specifica della piattaforma, l'elaborazione dei media e il recupero dagli errori.

Modello di Dati

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;
}

Il Pattern platformOverrides

Ogni piattaforma ha limiti di caratteri, convenzioni di hashtag e norme di formattazione diversi. Invece di creare post completamente separati, utilizzo un testo base con override specifici per piattaforma:

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 riceve una versione concisa. LinkedIn riceve una versione più lunga e professionale. Instagram utilizza il testo base come didascalia. Questo approccio rispetta le norme della piattaforma senza mantenere calendari di contenuti completamente separati.

Gestione dei Media

L'elaborazione dei media è la parte più soggetta a errori della pipeline. Ogni piattaforma ha requisiti diversi per dimensioni delle immagini, dimensioni dei file, formati e rapporti d'aspetto.

Pipeline di Elaborazione

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;
}

L'utilizzo di Sharp per l'elaborazione delle immagini mantiene la pipeline veloce e gestisce casi limite come la rotazione EXIF, la conversione del profilo colore e la compatibilità dei formati. Per l'elaborazione video, FFmpeg gestisce la transcodifica, ma l'elaborazione video è significativamente più complessa e intensiva in termini di risorse — di solito la delego a un worker dedicato o a una funzione cloud.

Limiti di Frequenza e Logica di Riprova

Ogni piattaforma impone limiti di frequenza, e colpirli in modo sgarbato causa post falliti e potenzialmente ban API temporanei.

Tracciamento dei Limiti di Frequenza

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));
    }
  }
}

Riprova con Backoff Esponenziale

Non tutti i fallimenti sono permanenti. Timeout di rete, errori temporanei del server e limiti di frequenza sono tutti ritentabili. Errori del database e fallimenti di autenticazione non lo sono.

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' };
}

Il jitter è importante — senza di esso, più client che tentano di nuovo sincronizzati sullo stesso programma di backoff creano problemi di "thundering herd" in cui tutti tentano di nuovo simultaneamente.

Flusso di Lavoro di Approvazione dei Contenuti

La pubblicazione automatizzata senza revisione umana è rischiosa. Un errore di formattazione, un post insensibile durante una crisi o un link errato possono causare danni reali. Includo un passaggio di approvazione obbligatorio nella pipeline.

Il Flusso di Approvazione

Contenuto Creato → Stato: BOZZA
       │
       ▼
Anteprima Generata → Revisore notificato (Slack/email)
       │
       ▼
Revisore Approva → Stato: APPROVATO → Pianificato per la pubblicazione
       │
   o Rifiuta → Stato: BOZZA (con feedback) → Torna al creatore

Il passaggio di anteprima è importante — mostra al revisore esattamente cosa verrà pubblicato su ogni piattaforma, con conteggi di caratteri, anteprime multimediali e anteprime di link. Questo cattura problemi di formattazione che non sono ovvi nell'editor di contenuti.

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;
}

Raccolta di Analisi

Dopo la pubblicazione, la raccolta dei dati di engagement aiuta a ottimizzare i contenuti futuri. Ogni piattaforma fornisce analisi tramite la propria API, ma la forma e la disponibilità dei dati variano.

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;
  }
}

Raccolgo le analisi a 1 ora, 24 ore e 7 giorni dopo la pubblicazione. Questo cattura l'engagement immediato, le prestazioni giornaliere e la portata a lungo termine. I dati alimentano una semplice dashboard che mostra quali tipi di contenuto, orari di pubblicazione e piattaforme generano il maggior engagement.

Costruire o Acquistare: La Valutazione Onesta

Prima di costruire un sistema personalizzato, considera se gli strumenti esistenti soddisfano le tue esigenze.

Quando Usare Strumenti Esistenti

Buffer (5-100 $/mese) gestisce la pianificazione, la pubblicazione multipiattaforma e le analisi di base. Supporta Twitter, Instagram, LinkedIn, Facebook, Pinterest e TikTok. L'API è solida per la pianificazione programmatica. Per la maggior parte dei piccoli team, Buffer copre il 90% delle esigenze.

Hootsuite (99+ $/mese) aggiunge collaborazione in team, flussi di lavoro di approvazione dei contenuti e analisi più approfondite. Migliore per team più grandi con più stakeholder.

Typefully (12-29 $/mese) è specializzato in Twitter/X con supporto per thread, collaborazione su bozze e analisi del pubblico. Se Twitter è la tua piattaforma principale, è eccellente.

Later (16,67+ $/mese) è forte per le piattaforme visive (Instagram, Pinterest, TikTok) con gestione della libreria multimediale e pianificazione del calendario visivo.

Quando Costruire una Soluzione Personalizzata

Costruire una soluzione personalizzata ha senso quando:

  • Hai bisogno di una profonda integrazione con sistemi interni (CMS, database di prodotti, CRM)
  • La tua generazione di contenuti è parzialmente automatizzata (bozze generate dall'IA, post basati su template da eventi di prodotto)
  • Hai bisogno di flussi di lavoro di approvazione personalizzati che gli strumenti esistenti non supportano
  • Pubblichi a un volume tale che il prezzo del SaaS diventa costoso (50+ post/giorno su più account)
  • Le funzionalità API della piattaforma di cui hai bisogno non sono esposte da strumenti di terze parti

L'Approccio Ibrido

Usa uno strumento di pianificazione (Buffer, Hootsuite) per i contenuti manuali e costruisci un'automazione personalizzata solo per i post programmatici. Molti strumenti di pianificazione hanno API che ti consentono di creare post programmaticamente mentre utilizzi la loro dashboard per la revisione e l'approvazione.

// 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;
}

Questo ti offre l'affidabilità e l'interfaccia utente di uno strumento consolidato mantenendo la flessibilità programmatica del codice personalizzato.

Notifiche Webhook

Quando un post viene pubblicato, fallisce o riceve un engagement significativo, il sistema dovrebbe notificare il team. Utilizzo i webhook per inviare eventi a Slack.

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 },
          ],
        },
      ],
    }),
  });
}

Pattern di Gestione degli Errori

Le API dei social media sono inaffidabili. I server vanno giù, i limiti di frequenza cambiano, l'elaborazione dei media fallisce. Il sistema deve gestire i fallimenti con grazia senza perdere contenuti.

La Coda di Lettere Morte

I post che falliscono dopo tutti i tentativi vanno in una coda di lettere morte per la revisione manuale:

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,
  });
}

Una dashboard mostra tutti i post falliti con pulsanti di riprova. La maggior parte dei fallimenti sono transitori e riescono con un tentativo manuale. I fallimenti persistenti di solito indicano la scadenza del token o modifiche API che richiedono aggiornamenti del codice.

Gestione del Successo Parziale

Un post destinato a tre piattaforme potrebbe avere successo su due e fallire su una. Il sistema deve tracciare i risultati per piattaforma in modo indipendente piuttosto che trattare l'intero post come fallito:

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,
    },
  });
}

L'utilizzo di Promise.allSettled invece di Promise.all assicura che un fallimento su una piattaforma non impedisca la pubblicazione sulle altre.

Mettere

DU

Danil Ulmashev

Full Stack Developer

Interessato a collaborare?