← casi-studio /

frontwire

// caso-studio

Frontwire

Un archivio di notizie su frontend e AI per chi sviluppa, senza database e senza server — una GitHub Action ogni 6 ore importa i feed, committa un JSON nel repo e fa ripartire la build statica su Nuxt 4.

Nuxt 4Vue 3TypeScriptTailwind v4GitHub ActionsVitest
Frontwire

Cos'è

Frontwire è la rassegna di notizie di web development che volevo leggere: framework frontend (Nuxt, Next, Vue, React, Vite) e l'AI che riguarda chi scrive codice — modelli, API, strumenti per sviluppatori. L'AI applicata ad altri campi, le notizie aziendali e i round di finanziamento restano fuori.

L'archivio è pensato per essere letto per intero: al massimo 3 articoli per fonte, i più recenti, entro 30 giorni. Una trentina in tutto. Non è un feed infinito, è una pagina che finisce.

Il problema

Le notizie che mi servono sono sparse su venti blog ufficiali, tre newsletter settimanali e Hacker News. Gli aggregatori generalisti annegano il frontend nel resto; i reader RSS mi restituivano il problema di partenza, cioè troppa roba.

Il vincolo vero però era un altro: zero costi. Niente database, niente server, niente API a pagamento. Il che esclude in partenza la soluzione ovvia (un backend che fa polling e una tabella) e obbliga a chiedersi dove vivano i dati.

Come funziona

I dati vivono nel repository. Il sito è statico.

  • Una GitHub Action pianificata ogni 6 ore scarica le fonti, normalizza gli articoli, rimuove i duplicati, assegna i tag e scarta gli articoli AI che non riguardano chi sviluppa
  • Il risultato viene scritto in data/articles.json e committato su main dal bot
  • Ogni push su main, compresi quelli del bot, fa ripartire il deploy statico su Vercel

Il commit del bot è il trigger del deploy. Non serve nient'altro: niente webhook, niente revalidate, niente cron su un server che non ho.

Solo feed RSS/Atom e API ufficiali, mai scraping. Gli articoli non vengono mai ripubblicati per intero: l'archivio tiene l'estratto della fonte e linka l'originale, con le note di rilascio di GitHub come unica eccezione.

Il tipo Article, il vocabolario dei tag e il registro delle fonti vivono in shared/, importato sia dall'app che dalla pipeline: una sola fonte di verità per lo schema.

Dettagli di cui vado fiero

Lo stesso articolo da due fonti collassa in uno. L'id è un hash dell'URL normalizzato: parametri di tracking, www., slash finale, fragment e ordine dei parametri non contano. Quando un post del blog di React arriva anche da This Week In React, è già lo stesso record.

const normalized = new URL(url);
normalized.hostname = normalized.hostname.replace(/^www\./, "");
normalized.hash = "";
// Copy the keys first: deleting while iterating searchParams skips entries.
for (const key of [...normalized.searchParams.keys()]) {
  if (TRACKING_PARAM.test(key)) normalized.searchParams.delete(key);
}
normalized.searchParams.sort();

Il merge non riscrive mai il passato. Un articolo già archiviato non viene aggiornato dal feed: entra solo se l'id è nuovo. Poi valgono i due limiti — finestra di 30 giorni e tetto di 3 per fonte — e l'ordinamento è stabile, ties risolti per id. Gli stessi dati serializzano sempre negli stessi byte, quindi il bot non genera commit rumore quando non è cambiato niente.

Il filtro di pertinenza è una regex, non un classificatore. Un articolo AI entra solo se il titolo cita un modello con la versione o un termine da sviluppatore. È una scelta di budget diventata una scelta di design: le regole sono leggibili, testabili e si tarano in un file solo.

// A model family followed by a version: `GPT-6`, `o3`, `Claude Opus 5`, `DeepSeek-R2`…
const MODEL_VERSION = /gpt[-‑ ]?\d|\bo\d\b|\bclaude(?: (?:opus|sonnet|haiku))? ?\d|…/i;

I filtri stanno nella query, non in uno stato locale. Categoria e tag vivono nell'URL (/?category=ai&tags=react,nextjs), quindi ogni vista filtrata è condivisibile e il tasto Indietro riporta ai filtri precedenti. I filtri però restano vuoti fino al mount: la home è prerenderizzata e non ha query, e filtrare durante l'hydration farebbe divergere l'HTML del client da quello del server.

I preferiti salvano una copia, non un riferimento. Niente account e niente database: la lista sta nel localStorage. Quello che si legge da lì non è di fiducia — si modifica a mano e sopravvive ai rilasci — quindi ogni voce passa da un controllo di forma contro i vocabolari reali di fonti e tag, non solo contro i tipi: un sourceId inventato manderebbe la card a cercare l'icona di una fonte che non esiste.

Il processo

Qui il confronto con Echoes è la parte interessante.

Su Echoes il vincolo era scrivere tutto a mano: l'AI faceva da reviewer, mai da autore, perché stavo imparando React e Next.js e non volevo farmeli generare. Su Frontwire il vincolo è l'opposto: il codice lo scrive l'agente, io dirigo. Non perché sia più comodo, ma perché è l'altra competenza da esercitare, e perché il tempo per seguire riga per riga un progetto personale non ce l'avevo.

Dirigere però non vuol dire delegare e sperare. Quello che ho scritto io è il contorno:

  • Un working agreement nel repo (CLAUDE.md): prodotto, architettura, stile del codice, regole per i commenti, policy di git. È il documento che l'agente legge a ogni sessione, e le regole valgono anche per il codice che scrive lui
  • Una spec di design per ogni tappa, discussa prima di scrivere codice. Sette tappe, sette documenti — il perché delle scelte sta lì, non nei messaggi di commit
  • Le decisioni di prodotto e infrastruttura, sempre mie: quali fonti entrano, quale tetto per fonte, riassunti AI sì o no, come si gestisce il deploy
  • Le azioni verso l'esterno (push, merge, lancio dei workflow) solo su mio via libera

I commenti seguono A Philosophy of Software Design: registrano quello che era nella testa di chi ha progettato il codice e che il codice da solo non dice. È la regola che ha alzato di più la qualità dell'output, perché costringe a spiegare le decisioni invece di descrivere le righe.

I test sono Vitest sulla logica pura della pipeline — parsing dei feed, normalizzazione, deduplica, tag — con fixture locali, mai chiamate di rete. Un test d'integrità su articles.json fa anche da cancello nell'Action: se fallisce, niente commit.

Cosa ho imparato

  • Un vincolo di budget è un vincolo di architettura. "Niente servizi a pagamento" ha deciso il filtro (regex e non classificatore), l'assenza di riassunti, la persistenza (JSON nel repo) e il modello di deploy. Non è stata una rinuncia in ognuno di quei punti: è stato il criterio che li ha resi coerenti tra loro
  • Scrivere la spec prima costa poco e si ripaga subito, e vale a prescindere da chi scrive il codice — vale con un agente, varrebbe con un collega
  • Fidarsi dei dati altrui è il bug. Feed, localStorage, id delle fonti: tutto arriva da fuori e tutto va validato contro il vocabolario reale, non contro il tipo TypeScript, che a runtime non esiste

Limiti consapevoli

Niente riassunti AI: il provider che avevo previsto è stato ritirato, e con i feed reali quasi nessun articolo ha abbastanza testo da riassumere — i blog portano una descrizione di una riga e non faccio scraping. Il feed di Anthropic è un mirror della community, quindi una dipendenza esterna che può sparire. Il filtro di pertinenza guarda solo il titolo: qualche falso negativo passa.

In roadmap: nuove fonti già valutate (Astro, Tailwind, WebKit) e una pagina che spieghi al lettore perché una certa notizia è finita lì dentro.

_casi-studio
Apri la palette per cercare file e navigare