Home / Paper / Architettura di un Micro-SaaS per la Generazione Automatica di Carousel Social con AI

Architettura di un Micro-SaaS per la Generazione Automatica di Carousel Social con AI

v1.011 min di lettura
Next.js 16Claude APIGemini FlashSharpRemotionSupabaseStripe
Architettura Micro-SaaS — AI Carousel Generation Pipeline

Abstract

Un micro-SaaS che automatizza la creazione di carousel editoriali per Instagram e LinkedIn. Pipeline multi-AI entro i 60 secondi di un ambiente serverless, rendering SVG+Sharp senza browser headless, e sistema spotlight per animazioni video informative.

Questo paper è disponibile anche in inglese

Read in English →

1. Il Problema

Creare un carousel social di qualità richiede oggi 3-4 ore di lavoro: ricerca del contenuto, copywriting, design delle slide in Canva o Figma, ottimizzazione della caption. Per un professionista che pubblica 3-5 volte a settimana, questo rappresenta un collo di bottiglia significativo.

Le soluzioni esistenti cadono in due categorie: template builder (Canva, Carousel.so) che richiedono comunque lavoro manuale di contenuto, e

generatori AI che producono output generico e visivamente piatto. Nessuna combina generazione di contenuto editoriale, design professionale, e rendering in un'unica pipeline automatizzata.

Il progetto si posiziona come soluzione end-to-end: da un topic a un carousel pubblicabile in ~30 secondi, con qualità editoriale paragonabile a contenuti creati manualmente.

2. Architettura del Sistema

2.1 Overview della Pipeline

Topic (utente)
    ↓
Claude API → JSON strutturato (slide + caption)
    ↓
Gemini Flash → Immagine di copertina (photorealistic)
    ↓
SVG Builder → Sharp → PNG per ogni slide
    ↓
Supabase Storage → URL pubbliche
    ↓
[Opzionale] Remotion → Video MP4 con animazioni
Pipeline di generazione end-to-end

L'intera pipeline è orchestrata da una singola API route (POST /api/generate) con un budget di tempo di 55 secondi, lasciando 5 secondi di margine sul limite di 60s di Vercel.

2.2 Stack Tecnologico

LayerTecnologiaMotivazione
FrameworkNext.js 16, React 19App Router, Server Components, ISR
AI ContenutoClaude Sonnet (via OpenRouter)Migliore qualità di output strutturato in italiano
AI ImmaginiGemini 2.5 Flash (via OpenRouter)Rapido, costo contenuto, buona qualità fotografica
Rendering StaticoSharp + SVGServerless-friendly, nessun browser headless
Rendering VideoRemotion 4.xComposizione video programmatica in React
DatabaseSupabase (PostgreSQL + Auth + Storage)Auth integrata, RLS, storage con CDN
PagamentiStripeCheckout, webhook, gestione subscription

3. Generazione del Contenuto con Claude

3.1 System Prompt Engineering

Il cuore della qualità di output sta nel system prompt. Dopo decine di iterazioni, il prompt definisce:

  • Struttura rigida: 5-7 slide, con isCover e isLast obbligatori
  • Stile editoriale: tono conversazionale in italiano, forma "tu", 150-250 caratteri per slide
  • Formattazione: enfasi con **grassetto** sui concetti chiave
  • Anti-slop: lista esplicita di pattern da evitare ("In un mondo sempre più digitale...", catene di emoji, CTA generiche)
  • Caption ottimizzata: 800-1500 caratteri con struttura hook → contesto → thread → takeaway → CTA → hashtag

Il prompt include anche template hints che modificano il comportamento in base al template selezionato. Per i template infografici, Claude genera strutture dati (chart, stats, progressBars) oltre al testo:

// Esempio di output Claude per template infographic-data
{
  headline: ["Crescita del", "**Mercato AI**"],
  chart: {
    type: "bar",
    items: [
      { label: "2023", value: 420 },
      { label: "2024", value: 580 },
      { label: "2025", value: 780 },
      { label: "2026", value: 1050 }
    ]
  },
  takeaway: "Gli investimenti in **intelligenza artificiale** superano il miliardo nel 2026",
  source: "Fonte: Osservatorio AI, Politecnico di Milano"
}

3.2 Gestione degli URL

Quando l'utente include un URL nel topic, il sistema:

  1. Estrae gli URL con regex
  2. Fetcha il contenuto HTML (timeout 8s)
  3. Estrae il testo principale con Cheerio (limit 4000 caratteri)
  4. Inietta il contenuto come contesto aggiuntivo nel prompt Claude

Questo permette di creare carousel a partire da articoli, paper, pagine di prodotto — il sistema sintetizza il contenuto fonte in slide editoriali.

3.3 Resilienza

  • Timeout: 25s per chiamata Claude, con 1 retry automatico su errori transienti
  • Validazione soft: campi infografici invalidi (stats senza valore, chart vuoti) vengono rimossi silenziosamente invece di far fallire la generazione
  • Parsing JSON robusto: rimozione di trailing comma, unescape di caratteri di controllo, gestione di output malformati

4. Rendering delle Slide

4.1 Approccio SVG + Sharp

La scelta architetturale più importante è stata non usare un browser headless per il rendering delle slide statiche. Playwright/Puppeteer richiedono risorse significative e non sono ideali in ambienti serverless con limiti di memoria e timeout.

Invece, il rendering segue un approccio SVG-first:

  1. Template Builder genera una stringa SVG completa (testo, forme, layout)
  2. Sharp converte l'SVG in PNG a 1080×1350px
  3. Per le cover, l'immagine AI viene composita sotto l'overlay SVG

Questo approccio è ~10x più veloce di un render Playwright e non richiede dipendenze binarie pesanti.

4.2 Sistema di Template

Il sistema supporta 9 template, ognuno con builder SVG dedicati:

  • Editorial (default): accent teal, font serif, sfondo chiaro, footer con dot pattern
  • Tech: mapping automatico categoria→colore accent (AI→verde, mobile→blu)
  • Bold: dark mode, tipografia ad alto contrasto
  • Minimal: bianco, elegante, spaziature ampie
  • Medical: tono clinico, terminologia specializzata
  • Infographic (3 varianti): data, compare, visual — con chart, stats, comparison layout

Ogni template espone un'interfaccia uniforme:

interface TemplateBuilders {
  buildCoverOverlay(ctx: SlideContext): string    // SVG overlay
  buildCoverFallback(ctx: SlideContext): string   // SVG senza AI
  buildContentSlide(ctx: SlideContext): string    // SVG contenuto
  buildLastSlide?(ctx: SlideContext): string      // SVG CTA
}

4.3 Pipeline Completa per Slide

SlideConfig → Template Builder → SVG string
                                      ↓
                          Sharp.svg(buffer).png()
                                      ↓
                              [IF cover] composite con bg AI
                              [IF free] composite watermark
                                      ↓
                              PNG Buffer (1080×1350)
                                      ↓
                          Upload → Supabase Storage → URL
Pipeline di rendering per singola slide

4.4 Export LinkedIn PDF

Per LinkedIn, le slide PNG vengono upscalate a 1200×1500 (stesso aspect ratio 4:5) e impacchettate in un PDF via pdf-lib. Questo permette di pubblicare carousel su LinkedIn come "document post" riutilizzando le stesse slide Instagram.

5. Sistema Video con Remotion

5.1 Perché Video oltre PNG

Le slide statiche PNG sono il prodotto principale. Ma il formato video (Reels, Stories) ha un reach organico significativamente superiore sui social. Il problema è che una semplice slideshow con fade non aggiunge valore — è solo "la versione animata di slide statiche".

La sfida era: come rendere le animazioni informative, non solo decorative?

5.2 Architettura Remotion

Remotion permette di comporre video usando React:

<TransitionSeries>
  {slides.map((slide, i) => (
    <>
      <TransitionSeries.Sequence durationInFrames={210}>
        <SlideFrame slide={slide} />
      </TransitionSeries.Sequence>
      <TransitionSeries.Transition
        presentation={zoomTransition()}
        timing={linearTiming({ durationInFrames: 24 })}
      />
    </>
  ))}
</TransitionSeries>

Ogni slide dura 7 secondi (210 frame a 30fps), con transizioni zoom di 0.8 secondi.

5.3 Sistema Spotlight: Animazioni che Informano

Il breakthrough è stato il sistema spotlight: invece di animare tutto uniformemente, il sistema identifica automaticamente l'elemento più rilevante di ogni infografica e lo mette in evidenza.

Come funziona:

  1. computeSpotlightIndex(slide) analizza i dati della slide e trova l'item con il valore più alto (la barra più grande, il segmento donut dominante, la stat con il numero più significativo)
  2. Fase Spotlight (frame 87-132): dopo che il chart è completamente entrato, gli item non-spotlight si dimezzano in opacità (30%), mentre l'item spotlight riceve un glow luminoso e un leggero scale-up (1.05x)
  3. Fase Hold (frame 132+): i numeri dell'item spotlight "respirano" con una micro-oscillazione sinusoidale (±2%, periodo 2s), un effetto sottile che mantiene l'attenzione senza distrarre
// Spotlight: identifica il valore massimo
const spotlightIndex = items.reduce(
  (maxI, item, i) => item.value > items[maxI].value ? i : maxI, 0
)

// Dim degli item non-spotlight
const itemOpacity = inSpotlightPhase
  ? interpolate(frame, [start, start + 12],
      [1, isSpotlight ? 1 : 0.3], { extrapolateRight: 'clamp' })
  : 1

// Breathing durante hold
const breathScale = inHoldPhase
  ? 1 + 0.02 * Math.sin((2 * Math.PI * t) / 2)
  : 1

Questo trasforma un bar chart statico in una narrazione visiva: "ecco il dato più importante, guardalo".

5.4 Transizioni Narrative (Zoom)

Le transizioni tra slide usano un effetto zoom che simula un "avvicinamento" al contenuto:

  • La slide uscente scala da 1.0x a 1.4x (zoom in), con fade nell'ultimo 30%
  • La slide entrante appare in fade underneath

Questo crea un senso di progressione narrativa — si "entra" nel contenuto — invece del classico fade generico.

5.5 Micro-Motion

Tre elementi di micro-animazione arricchiscono l'esperienza senza distrarre:

  1. Progress line: una linea verticale di 2px sul bordo sinistro che cresce dal 0% al 100% dell'altezza durante la slide, dando un senso di avanzamento temporale
  2. Separatore reattivo: la linea divisoria tra headline e chart cambia opacità (da 33% a CC hex) e acquista un glow durante la fase spotlight, sincronizzandosi con l'enfasi sui dati
  3. Breathing numerico: i numeri dello spotlight item oscillano con una sinusoide impercettibile, mantenendo l'occhio ancorato al dato chiave

5.6 Donut Chart: SVG Filter per Glow

Un problema tecnico interessante: CSS box-shadow non funziona su elementi SVG <circle>. Per il glow sullo spotlight del donut chart, è stato necessario usare un SVG filter nativo:

<filter id="donut-glow">
  <feGaussianBlur stdDeviation={glowStdDev} result="blur" />
  <feFlood floodColor={accentColor} floodOpacity="0.6" result="color" />
  <feComposite in="color" in2="blur" operator="in" result="glow" />
  <feMerge>
    <feMergeNode in="glow" />
    <feMergeNode in="SourceGraphic" />
  </feMerge>
</filter>

5.7 Timeline di una Slide (7 secondi)

Frame   0 ──── 24: Headline entrance (spring scale-up)
Frame  18 ──── 36: Context text fade-in
Frame  33 ──── 87: Chart/stats entrance (staggered)
Frame  87 ─── 132: SPOTLIGHT PHASE (dim + glow + scale)
Frame  96 ─── 135: Takeaway text (word-by-word)
Frame 132 ─── 210: HOLD PHASE (breathing, read time)
Frame 135 ─── 145: Footer fade-in
Frame 150 ─── 210: Static hold (tempo per leggere)
Timeline di animazione per singola slide (210 frame @ 30fps)

6. Modello di Business

6.1 Pricing a Tre Livelli

FeatureFreeBase (€19/mo)Pro (€49/mo)
Carousel/mese330Illimitati
WatermarkNoNo
Cover AI
Template infograficiNo
PersonalizzazioneNoNo
Export videoNoNo

6.2 Feature Gating

Le feature sono gate-ate a livello di API route, non di frontend. Ogni richiesta viene validata contro getPlanLimits(planId) prima di procedere con la generazione. Questo previene bypass client-side e garantisce che l'enforcement sia atomico.

// Esempio di plan validation
const limits = getPlanLimits(profile.plan)
if (profile.carousels_used_this_month >= limits.maxCarousels) {
  return Response.json({ error: 'Limite mensile raggiunto' }, { status: 429 })
}
if (templateId === 'custom' && !limits.customTemplate) {
  return Response.json({ error: 'Template custom richiede piano Pro' }, { status: 403 })
}

7. Database e Sicurezza

7.1 Schema Supabase

Due tabelle principali con Row-Level Security (RLS):

  • profiles: piano, usage counter, brand name — creato automaticamente al signup tramite trigger handle_new_user()
  • carousels: topic, status, slide config (JSONB), URLs, caption — RLS garantisce che ogni utente veda solo i propri carousel

7.2 Storage

Le slide PNG e i PDF LinkedIn vengono uploadati su Supabase Storage con path strutturato:

carousel-slides/{userId}/{carouselId}/ig-slide-0.png
carousel-slides/{userId}/{carouselId}/ig-slide-1.png
carousel-slides/{userId}/{carouselId}/linkedin.pdf

Il bucket è configurato come public-read, authenticated-write — le URL delle slide sono direttamente servibili senza autenticazione.

8. Sfide Tecniche e Soluzioni

8.1 Il Vincolo dei 60 Secondi

Vercel impone un timeout di 60 secondi sulle API route serverless. La pipeline completa (Claude + Gemini + rendering + upload) deve rientrare in questo budget.

Soluzione: timeout aggressivi per step (Claude 25s, Gemini 20s, URL fetch 8s), fallback automatici (cover senza AI se Gemini non risponde), e parallelizzazione dove possibile (LinkedIn PDF costruito dopo upload IG).

8.2 Font in Ambiente Serverless

Sharp (via librsvg) richiede font di sistema per renderizzare testo SVG. Su Vercel, non ci sono font installati.

Soluzione: font bundlati nella cartella /fonts/ del progetto, con FONTCONFIG_PATH configurato nel next.config.ts per puntare alla directory locale.

8.3 Stripe Client e Module-Level Init

Il client Stripe si inizializza a module-level, il che causa errori durante il build di Next.js quando STRIPE_SECRET_KEY non è disponibile.

Soluzione: lazy proxy pattern — il client Stripe viene wrappato in un Proxy che deferisce l'inizializzazione al primo accesso:

export const stripe = new Proxy({} as Stripe, {
  get(_, prop) {
    if (!_instance) {
      _instance = new Stripe(process.env.STRIPE_SECRET_KEY!, { ... })
    }
    return _instance[prop]
  }
})

8.4 Donut Chart: Overlap dei Segmenti

L'animazione iniziale del donut chart causava sovrapposizione visiva tra segmenti adiacenti. Il problema: strokeDashoffset spostava la porzione visibile verso la fine del segmento, creando collisioni.

Soluzione: sostituire strokeDashoffset con strokeDasharray dinamico, dove la porzione visibile cresce dal punto di partenza:

// Prima (broken): dashoffset sposta il pattern
strokeDashoffset={segLength * (1 - progress)}

// Dopo (fixed): dasharray cresce dall'inizio
strokeDasharray={`${segLength * progress} ${circumference - segLength * progress}`}

9. Metriche e Performance

~25-35s

Tempo medio generazione

Carousel completo (5-7 slide)

8-15s + 5-12s

Claude + Gemini

Breakdown tipico per AI

80-200KB

Dimensione slide

PNG 1080×1350px

~2-5MB

Dimensione video

30 secondi (7s × 4-5 slide)

10. Conclusioni e Sviluppi Futuri

Il progetto dimostra che è possibile costruire un prodotto AI-native di qualità editoriale come micro-SaaS single-developer, sfruttando l'ecosistema serverless moderno e le API AI di ultima generazione.

Le scelte architetturali chiave — SVG+Sharp per il rendering, Claude per il contenuto strutturato, Remotion per il video — bilanciano qualità, performance e costi operativi. Il sistema di spotlight nel video rappresenta un esempio di come le animazioni possano essere informative piuttosto che puramente decorative: non si anima tutto, si enfatizza ciò che conta.

Sviluppi futuri:

  • Element Bridge Transition (C1): transizione dove l'elemento spotlight "si stacca" dalla slide uscente e si riposiziona nella slide entrante
  • Data Storytelling: animazioni che costruiscono una narrazione progressiva tra slide (es. evoluzione temporale di un grafico)
  • A/B Testing automatico: generazione di varianti di caption e test di engagement
  • Multi-lingua: supporto nativo per contenuti in inglese, spagnolo, francese
  • API pubblica: endpoint per integrazioni third-party (Zapier, Make, n8n)

Appendice: Struttura del Progetto

carousel-ai/
├── src/
│   ├── app/api/generate/route.ts    ← Orchestrazione pipeline
│   ├── lib/
│   │   ├── ai/                      ← Claude API + system prompts
│   │   ├── rendering/               ← SVG builders + Sharp + templates
│   │   ├── stripe/                  ← Pagamenti + plan gating
│   │   └── supabase/                ← Auth + database + storage
│   └── remotion/                    ← Video composition + animazioni
├── supabase/schema.sql              ← Database + RLS policies
└── fonts/                           ← Font bundlati per serverless
Struttura delle directory del progetto

Vuoi costruire qualcosa di simile?

Se hai un progetto tecnico che richiede architetture AI avanzate, parliamone.