Costruire un sito di documentazione senza framework
Questo sito — quello su cui stai leggendo — non ha alcun framework di build sotto il cofano. Niente 11ty, niente Astro, niente Next. docs-app/build.mjs è qualche centinaio di righe di Node puro che legge moduli di contenuto e funzioni template e scrive HTML/CSS/JS statico dentro dist/. È stata una lettura deliberata del vincolo della specifica, e ha prodotto la sua quota di bug molto reali, trovati tutti nello stesso modo: aprendo il sito in un browser vero.
Cosa significava davvero "niente framework"
La specifica escludeva un framework a runtime per il sito consegnato — nulla lato client che faccia routing, hydration o rendering. Non escludeva un piccolo script eseguito in fase di build che trasforma contenuto strutturato in file statici, che è una cosa del tutto diversa da ciò che il browser di chi legge deve eseguire.
docs-app/src/lib/highlight.mjs (un tokenizzatore a regex scritto a mano per l'evidenziazione della sintassi) e src/scripts/search.js (uno scorer di sottostringhe scritto a mano su un indice JSON precostruito) seguono la stessa lettura: niente Fuse.js, niente libreria di syntax highlighting lato client, tutto fatto in fase di build o con JavaScript vanilla puro.
Il contenuto è dato, non markup
Ogni pagina di documentazione e del blog è un piccolo modulo .mjs che esporta meta e un array blocks — un piccolo DSL (heading/p/list/code/callout/...) che un'unica funzione renderBlocks(blocks, lang) trasforma in HTML. La stessa funzione estrae anche i chunk dell'indice di ricerca (uno per ogni heading) dalla stessa identica fonte usata per renderizzare la pagina, così contenuto e indice di ricerca non possono strutturalmente disallinearsi come accadrebbe con due fonti mantenute separatamente.
Tre bug trovati solo aprendo il browser
Nessuno di questi tre è emerso leggendo il codice. Tutti e tre sono stati scoperti navigando davvero sul sito renderizzato:
- Icone SVG senza dimensione. L'icona di GitHub nell'header veniva renderizzata alla sua dimensione intrinseca grezza (~300×150) invece che a una normale dimensione da icona, perché nulla impostava una larghezza/altezza esplicita. Risolto con una regola difensiva di fallback,
svg { width: 1em; height: 1em; }, inbase.css— ogni icona ha comunque bisogno di una regola di dimensione esplicita da qualche parte, ma ora una mancante fallisce silenziosamente invece che in modo vistoso. IntersectionObserverche lancia unSyntaxErrorsilenzioso. Il primo tentativo per lo scroll-spy "in questa pagina" usavarootMargin: '-5rem'.rootMarginaccetta solo pixel o percentuali, mairem— il browser lancia un errore, ma solo in console, in nessun punto che un ricaricamento della pagina renderebbe visibile. È stato riscritto come un semplice listener di scroll throttlato conrequestAnimationFrame, il che ha risolto anche un problema di UX: una voce del sommario ora resta attiva per tutto il tempo in cui la sua sezione è a schermo, non solo nell'istante in cui il suo heading attraversa il viewport.- Un H1 in inglese hardcoded nella homepage italiana. Un bug di contenuto, non di logica — l'heading dell'hero aveva una stringa inglese incorporata anche quando si serviva
/it/. Visibile solo caricando davvero la pagina italiana.
Uno schema che merita un nome
Tutti e tre i bug sopra — più un quarto trovato successivamente (un pulsante "cancella" nativo e uno personalizzato entrambi renderizzati sul campo di ricerca) — condividono una proprietà: erano invisibili nel sorgente e visibili solo una volta renderizzati. Type checking e test unitari non intercettano una regola CSS di dimensione mancante o la violazione di un contratto di un'API specifica del browser. È l'argomento concreto, dalla storia stessa di questo progetto, per aprire davvero la pagina invece di fidarsi di un lint/type-check pulito.
Un compromesso deliberato: nessun redirect automatico di lingua
La lingua scelta persiste in localStorage, ma una pagina non corrispondente mostra solo un banner richiudibile ("Continue in English" / "Continua in italiano") — non fa mai un redirect forzato. Un redirect forzato è dannoso per i link diretti, rompe la navigazione all'indietro, e ostacola attivamente i crawler che cercano di valutare correttamente i tag hreflang. È stato un compromesso consapevole tra comodità e correttezza, e ha vinto la correttezza.
Questi tre non sono stati gli ultimi bug che questo sito ha prodotto allo stesso modo. Un giro successivo, dopo che il sito è andato davvero online, ne ha fatto emergere un'altra tornata, indipendente, raccontata nel post sulla release — una build pulita non è la stessa cosa di una pagina che funziona davvero, e a questo progetto è toccato impararlo due volte.