Vai al contenuto
SIMONE PIZZI
Progetti ↗
Runtime Radio Web Site

Il sito di Runtime Radio era rimasto indietro. Ho rimediato

Dalla v2.6.2 alla v2.9.10: 20 versioni, 16 vulnerabilità chiuse, un redesign completo, Tiptap al posto di Quill e il bundle JavaScript dimezzato.

Data
Lettura
11 minuti
Sezioni
7
Copertina: Il sito di Runtime Radio era rimasto indietro. Ho rimediato

C'è una cosa curiosa nei siti che fanno da player a una radio: raramente vengono aggiornati. Il motivo è quasi filosofico — il sito è la radio, nel senso che basta che suoni, che il flusso arrivi, che il player non si rompa. Tutto il resto può aspettare.

E così runtimeradio.com aspettava. Mesi. Mentre il TelegramBot attraversava undici versioni e un redesign completo, mentre il libro React + PHP The Thin Stack cresceva capitolo dopo capitolo, il sito della radio restava alla v2.6.2: funzionante, stabile, e con un debito tecnico che avevo smesso di guardare perché guardarlo avrebbe significato doverlo affrontare.

A fine aprile ho smesso di guardare dall'altra parte.

Quello che è successo nei successivi quindici giorni è stato uno dei cicli di sviluppo più intensi da quando esiste il progetto. Venti versioni rilasciate, circa 53 criticità chiuse, un redesign visivo completo, un editor CMS sostituito, la compliance GDPR implementata da zero, e un bundle JavaScript ridotto del 58%. Il sito è arrivato alla v2.9.10.

Questo articolo racconta come e perché.

Prima di tutto: la sicurezza.

O meglio, l'assenza di sicurezza.

Prima di toccare l'estetica, prima di pensare a nuove funzionalità, c'era una lista di cose che non avrei mai dovuto scoprire — ma che erano lì, nel codice, ad aspettare che qualcuno con cattive intenzioni le trovasse prima di me.

Il problema più grave non era tecnico nel senso classico del termine. Era quasi imbarazzante nella sua semplicità: chiunque, senza autenticazione, senza conoscere nessuna password, poteva fare una richiesta GET a un URL specifico del sito e resettare l'account amministratore. Il file debug_db.php era accessibile via browser. Aveva un nome chiaro. Faceva esattamente quello che sembrava fare.

È stato eliminato nella v2.6.3, insieme ad altre tre vulnerabilità classificabili — senza iperbole — come gravissime. Le credenziali del database MySQL erano scritte in chiaro in db_credentials.php, che veniva versionato in git. Chiunque avesse accesso al repository aveva, di fatto, accesso al database di produzione. Il token del bot Telegram era compilato nel bundle JavaScript pubblico: bastava aprire gli strumenti di sviluppo del browser per leggerlo. Dodici file di manutenzione e debug erano raggiungibili via browser senza nessuna protezione.

In una notte sola, tutto spostato: le credenziali in un file .env non versionato letto a runtime tramite parse_ini_file() nativo PHP, il token del bot restituito solo a utenti admin autenticati da un endpoint dedicato, i file di manutenzione bloccati con HTTP 403 da una regola FilesMatch in .htaccess.

La versione successiva, v2.6.4, ha consolidato altre cinque vulnerabilità gravi. Il CORS era configurato con wildcard * — ovvero qualsiasi origine poteva fare richieste all'API. Il sistema di login non aveva nessun limite ai tentativi: attacchi a dizionario senza nessun freno. Gli errori PDO venivano propagati direttamente nelle risposte API, rivelando la struttura interna del database.

Con la v2.6.5 è arrivata la protezione CSRF su tutte le operazioni di scrittura del CMS, con token a 64 caratteri esadecimali verificati nell'header X-CSRF-Token. Con la v2.6.6 i cookie di sessione hanno ricevuto i flag Secure, HttpOnly e SameSite=Strict, e il timeout di sessione è stato impostato a un'ora.

L'ultima versione del ciclo di sicurezza, v2.6.7, è anche la più complessa: il Dynamic Rendering completo per i crawler. Il file index.php — punto di ingresso di tutto il sito — distingue ora tra richieste umane e richieste dei bot (Googlebot, Twitterbot, facebookexternalhit, TelegramBot e una ventina di altri) tramite analisi dell'User-Agent. I crawler ricevono HTML completo con marcatori JSON-LD Schema.org senza eseguire JavaScript. Gli utenti umani ricevono la SPA React normale. Sitemap e robots.txt vengono generati dinamicamente da PHP, riflettendo il contenuto reale del database.

«Sette versioni per mettere in sicurezza quello che avrebbe dovuto essere sicuro sin dall'inizio. Non è una bella storia da raccontare. Ma raccontarla serve.»

Stratum: un design system che non c'era

Una volta che il sito era difendibile, era il momento di guardarlo per quello che era: visivamente eterogeneo, frutto di aggiunte progressive nel tempo, senza una voce coerente.

Il redesign si chiama Stratum. Non è solo una palette di colori: è un sistema completo di token CSS custom (--c-bg, --c-teal, --c-orange, --c-card, --c-elevated, --c-border, --c-text, --c-text-dim, --c-text-dimmer), una scelta tipografica precisa (DM Sans per il corpo, Playfair Display per i titoli, JetBrains Mono per le label e gli elementi tecnici), e un sistema di interazione omogeneo — hover con bordo teal, focus con bordo teal, transizioni CSS native.

Framer Motion è stato rimosso da tutto il sito. Non perché le animazioni siano una cosa sbagliata, ma perché le animazioni di default — le stesse su ogni elemento, senza intenzione — sono peggiori dell'assenza di animazioni. Le ho sostituite con un keyframe page-enter in CSS puro: leggero, prevedibile, coerente.

La v2.7.0 ha ridisegnato la shell: Navbar, Player, Footer. Il Player fisso in basso ha guadagnato un layout a tre zone — copertina e informazioni, pulsante play con glow più visualizer, controlli volume — e uno sfondo con backdropFilter: blur(24px). La Home ha un hero full-bleed con l'artwork di AzuraCast blurred in background e un pulsante «Ascolta Live» che reagisce allo stato reale del player.

Le versioni v2.7.1 e v2.7.2 hanno esteso il trattamento Stratum a tutte le pagine pubbliche e all'intera area CMS — Schedule, News, Speakers, Podcasts, ArticleEditor, SpeakerEditor, NewsletterComposer, MediaGallery. Stesso principio applicato ovunque: token CSS custom, pattern Stratum, nessun Framer Motion.

Subito dopo il deploy è emerso un bug critico, quasi comico nella sua natura: la funzione isLoggedIn() era definita due volte in admin.php, causando un fatal error PHP con HTTP 500 su qualsiasi operazione del CMS. Risolto nella v2.7.3 eliminando il duplicato. C'era anche un problema infrastrutturale più sottile: il file .env non veniva incluso nella dist/ perché Vite tratta i file nascosti in modo non ovvio. Il workflow di build è stato corretto per assicurarsi che viaggi sempre con la distribuzione.

Il bug dell'iframe che si resettava ogni dieci secondi

Tra v2.7.6 e v2.7.12 ci sono sei versioni consecutive dedicate a un singolo bug. Vale la pena raccontarlo perché è il tipo di problema che può far impazzire se non si sa dove guardare.

Gli iframe YouTube negli articoli venivano resettati ogni dieci secondi, interrompendo la riproduzione. Ogni dieci secondi, esattamente. La cadenza era sospetta: il polling di AzuraCast — lo stato della radio in diretta — veniva eseguito ogni dieci secondi. Quella richiesta causava un re-render del componente Article, che ricostruiva l'iframe da zero.

La soluzione definitiva è stata estrarre il corpo dell'articolo in un componente separato, ArticleBody, avvolto in React.memo. Isolato dal ciclo di re-render del player, l'iframe smette di esistere e rinascere ogni dieci secondi. Banale a dirsi, meno a trovarsi.

Addio Quill, benvenuto Tiptap

Questo era uno dei debiti tecnici che pesava di più. react-quill-new aveva tre problemi fondamentali: incompatibilità con React 19 StrictMode, dipendenze problematiche che causavano comportamenti inattesi, e un'architettura chiusa, difficile da estendere nel momento in cui si volesse aggiungere qualcosa di nuovo.

La migrazione a Tiptap v3, introdotta nella v2.9.0, ha richiesto la riscrittura completa di ArticleEditor.tsx. Il nuovo editor ha una toolbar che include headings da H1 a H4, grassetto, corsivo, sottolineato, barrato, un color picker con 22 colori predefiniti e l'opzione «rimuovi colore», l'allineamento testo, le liste, l'upload diretto delle immagini con anteprima, un popup YouTube con conversione automatica da qualsiasi formato URL al player nocookie (youtube-nocookie.com), e la gestione dei link.

Il punto critico della migrazione era la backward compatibility: centinaia di articoli già scritti con Quill, con iframe nel suo formato proprietario. La funzione prepareForEditor() converte automaticamente gli iframe Quill bare nel formato che Tiptap si aspetta. Gli articoli esistenti continuano a funzionare senza toccarli.

Nella v2.9.1 è emerso un bug visivamente devastante: la classe [&_span]:!text-inherit in Article.tsx annullava tutti i colori inline applicati tramite Tiptap, perché il !important sovrascriveva gli stili inline degli span. Rimossa la classe, i colori tornano a funzionare. Annotazione permanente nel codice: non reinserire mai questa classe.

La stessa versione ha introdotto il sistema bozze: una colonna status VARCHAR(20) nella tabella news, con i pulsanti «Salva Bozza» e «Pubblica» nell'editor, badge stato nella lista articoli, e filtro automatico lato server che esclude le bozze dalle pagine pubbliche.

Le cose che cambiano silenziosamente

Non tutto in questo ciclo ha una storia drammatica. Alcune delle modifiche più importanti sono quelle che l'utente non vede mai.

Immagini WebP. Dalla v2.9.2, tutte le immagini caricate tramite upload vengono convertite automaticamente in formato WebP dalla GD Library di PHP, con qualità 82 e ridimensionamento a massimo 1920px. I metadati EXIF vengono strippati. Le 54 immagini già presenti sul server sono state convertite in batch. Un disallineamento residuo — il campo cover_image nel database che puntava ancora ai path .jpg e .png originali — è stato risolto nella v2.9.9 con un'azione dedicata che ha allineato 33 record. Nessun file WebP mancante su disco, nessun articolo con immagine rotta.

GDPR e newsletter (v2.9.3–v2.9.4). La funzione mail() nativa PHP è stata sostituita con PHPMailer v7, configurato per Gmail SMTP con STARTTLS. Il sistema di iscrizione implementa il double opt-in: dopo il form, l'utente riceve una email con un link di conferma contenente un token crittograficamente sicuro a 64 caratteri. L'iscrizione diventa attiva solo dopo il click. La pagina /privacy-policy e la pagina /unsubscribe sono state aggiunte. Il pannello admin per la gestione degli iscritti espone badge di stato, filtri e azioni per ogni iscritto — reinvia conferma, conferma manuale, revoca, riattivazione, eliminazione — tutte protette da CSRF.

Speaker dal database (v2.9.5). Le pagine Home e About mostravano i fondatori della radio attingendo a un array statico SPEAKERS hardcoded in constants.ts — oltre 700 righe di dati incluse nel bundle principale di ogni visitatore, anche per chi non ne aveva bisogno. Con la migrazione a api.getSpeakers(), il file constants.js è sceso da 47 KB a 12 KB. I dati degli speaker sono ora gestibili dall'area admin senza toccare il codice.

TypeScript strict: zero any (v2.9.6). Otto nuove interfacce in types.ts — RawApiArticle, AdminUser, AdminArticle, ArticlePayload, SpeakerPayload, NewUser, PodcastPayload, ApiResponse — e tutti i parametri any rimossi da api.ts, Admin.tsx, SpeakerEditor.tsx e dagli altri file principali. L'unica eccezione documentata rimane (window as any).webkitAudioContext in useAudioPlayer.ts: è necessaria per la compatibilità con Safari, che espone l'AudioContext sotto il nome vendor-prefissato, e non esiste un modo tipizzato per accedervi.

Performance (v2.8.1–v2.8.2). Il bundle JavaScript iniziale era 843 KB. Con React.lazy e Suspense su tutte le pagine, il caricamento iniziale è sceso a circa 350 KB — una riduzione del 58%. Le immagini below-the-fold nelle pagine principali hanno ricevuto l'attributo loading="lazy".

L'ultimo pezzo: iOS e il buffering

La v2.9.10 — l'ultima del ciclo — ha risolto due problemi di compatibilità con Safari iOS che si trascinavano da mesi.

Safari iOS può sospendere l'AudioContext in qualsiasi momento: dopo una chiamata, dopo il blocco dello schermo, dopo un cambio app. Il codice precedente eseguiva il resume() solo durante l'inizializzazione. La fix aggiunge un listener sull'evento play dell'elemento audio: ogni volta che il browser avvia la riproduzione, si controlla lo stato del contesto e si chiama resume() se è suspended. Il cleanup al dismount del componente è stato corretto: viene ora eseguito il disconnect() del source node prima di chiudere il contesto, prevenendo leak di nodi Web Audio.

L'altra correzione riguarda il crossOrigin="anonymous" sull'elemento audio — necessario per la Web Audio API, ma fonte di SecurityError su server audio che non rispondono con header CORS corretti. Il catch in initAudioContext distingue ora questo errore specifico: l'audio continua a funzionare normalmente senza visualizer, con un warning in console che è specifico e informativo invece di generico.

Infine, è stato aggiunto il campo isBuffering nell'AudioState, sincronizzato sugli eventi nativi dell'elemento audio (waiting → vero, playing/canplay → falso). Il Player mostra lo spinner durante il buffering. Il pulsante è disabilitato durante il caricamento. Piccolo, ma il tipo di dettaglio che si nota quando manca.

Dove siamo adesso

In numeri: 20 versioni rilasciate in quindici giorni, 16 vulnerabilità di sicurezza chiuse, 17 fix frontend, 9 interventi architetturali backend, 54 immagini convertite in WebP, 8 nuove interfacce TypeScript, bundle ridotto del 58%.

Restano aperte alcune cose, tutte di priorità media o lieve. Compatibilità con Safari < 15.4 per accent-color. La Navbar mobile ha un'altezza 75vh che può tagliare le voci su schermi piccoli. Il polling di AzuraCast non si sospende quando il tab è in background. constants.ts contiene ancora i feed dei podcast nel bundle principale. L'area admin è in inglese mentre il resto del sito è in italiano — una piccola incoerenza che aspetta il momento giusto per essere corretta.

Il sito suona. Adesso è anche sicuro, coerente e mantenibile.

Per chi segue il progetto del libro React + PHP The Thin Stack: questo ciclo è entrato nei capitoli dedicati alla sicurezza delle applicazioni PHP, alla migrazione tra editor rich-text, e all'ottimizzazione del bundle con React lazy loading. Il codice di riferimento è su runtimeradio.com.

Ogni tanto scrivo una lettera

Nessuna cadenza fissa, nessun riassunto automatico: parte solo quando ho qualcosa da raccontare. In fondo a ognuna c'è il link per uscire.

↑ Su

Cerca nel sito

Condividi

L'indirizzo di questa pagina:

Scrivi a Simone

Segnalazioni, correzioni, o solo per dire che una cosa non funziona: quest'ultima è la più utile di tutte.