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 animazioniL'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
| Layer | Tecnologia | Motivazione |
|---|---|---|
| Framework | Next.js 16, React 19 | App Router, Server Components, ISR |
| AI Contenuto | Claude Sonnet (via OpenRouter) | Migliore qualità di output strutturato in italiano |
| AI Immagini | Gemini 2.5 Flash (via OpenRouter) | Rapido, costo contenuto, buona qualità fotografica |
| Rendering Statico | Sharp + SVG | Serverless-friendly, nessun browser headless |
| Rendering Video | Remotion 4.x | Composizione video programmatica in React |
| Database | Supabase (PostgreSQL + Auth + Storage) | Auth integrata, RLS, storage con CDN |
| Pagamenti | Stripe | Checkout, 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:
- Estrae gli URL con regex
- Fetcha il contenuto HTML (timeout 8s)
- Estrae il testo principale con Cheerio (limit 4000 caratteri)
- 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:
- Template Builder genera una stringa SVG completa (testo, forme, layout)
- Sharp converte l'SVG in PNG a 1080×1350px
- 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 → URL4.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:
- 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)
- 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)
- 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)
: 1Questo 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:
- 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
- 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
- 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)
6. Modello di Business
6.1 Pricing a Tre Livelli
| Feature | Free | Base (€19/mo) | Pro (€49/mo) |
|---|---|---|---|
| Carousel/mese | 3 | 30 | Illimitati |
| Watermark | Sì | No | No |
| Cover AI | Sì | Sì | Sì |
| Template infografici | No | Sì | Sì |
| Personalizzazione | No | No | Sì |
| Export video | No | No | Sì |
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.pdfIl 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
Vuoi costruire qualcosa di simile?
Se hai un progetto tecnico che richiede architetture AI avanzate, parliamone.
