Diario di bordo · Fase 0 · 2/7
Lo stack tecnico di aubia.dev
Ho costruito aubia.dev su una sola applicazione Laravel, senza database. Octane e FrankenPHP la tengono in memoria tra una richiesta e l'altra, e React renderizza le pagine sul server con Inertia.
di Mike, fondatore di Aubia Pubblicato il Aggiornato il 11 min di lettura
- laravel
- octane
- frankenphp
- inertia
- vite



Quando il suo browser richiede una pagina di aubia.dev, Cloudflare inoltra la richiesta al server ospitato da Laravel Cloud, dove FrankenPHP la consegna a Laravel. Laravel prepara i dati della pagina e ottiene da un processo Node l'HTML dei suoi componenti React. Nel suo browser, React idrata quell'HTML per rendere interattiva la pagina.
Il framework Laravel al centro del progetto
Il sito è un'applicazione Laravel 13 su PHP 8.5. Laravel legge la lingua dal prefisso dell'URL, per esempio /fr o /en, prima di preparare la pagina richiesta. Uso lo stesso framework per aubia.cloud e per l'applicazione desktop, costruita con NativePHP.
Ho riunito l'intero sito in un unico repository: il codice PHP, le pagine React, i testi delle sei lingue in JSON e gli articoli del blog in Markdown vengono distribuiti insieme. Durante la build, il comando Artisan blog:compile trasforma gli articoli in un catalogo che contiene già il loro HTML.
In config/database.php non è dichiarata alcuna connessione SQL. Le iscrizioni alla lista d'attesa sono conservate da aubia.cloud: il sito pubblico le inoltra con il client HTTP di Laravel, in una richiesta firmata. L'articolo sulla raccolta di indirizzi email senza una tabella locale descrive nel dettaglio questo scambio e la sua dipendenza dalla rete. Redis conserva il resto dello stato, cioè sessioni e cache, compresi i contatori del rate limiter.
Il codice PHP è organizzato in cinque moduli: Blog, Cloud, Locale, Seo e Waitlist. Le loro Action, le loro Query e il gateway verso aubia.cloud sono descritti nell'articolo sui cinque moduli.
Ottimizzazione delle prestazioni con Laravel Octane
Con PHP-FPM, Laravel viene inizializzato a ogni richiesta, anche quando un processo PHP ne gestisce diverse prima di essere riciclato, in base a impostazioni come pm.max_requests.
Sulla mia macchina di sviluppo, un MacBook Pro M5 Pro con PHP 8.5 e opcache attivo, l'inizializzazione di Laravel richiede 87 ms. Ogni durata che vi ho rilevato è la mediana di almeno dieci misurazioni effettuate l'8 settembre 2026: un ordine di grandezza, specifico di quella macchina.
Laravel Octane avvia Laravel una volta per worker, poi ogni worker gestisce le richieste successive con l'applicazione già caricata. I worker vengono riciclati o riavviati di tanto in tanto, per esempio durante un deploy.
In produzione, Octane serve la home page di aubia.dev con una mediana di 51 ms, secondo i log di accesso di Laravel Cloud dal 5 all'8 settembre 2026, esclusi i bot. Un quarto delle richieste impiega meno di 48 ms, tre quarti meno di 64 ms.
Su aubia.dev, Octane usa FrankenPHP, un server applicativo PHP scritto in Go che integra il server web Caddy.
Una volta installato e configurato FrankenPHP, OCTANE_SERVER lo seleziona come driver:
# .env: il driver attivo su aubia.dev
OCTANE_SERVER=frankenphp
Octane ripristina lo stato del framework tra le richieste, senza svuotare automaticamente ogni variabile globale e proprietà statica del codice applicativo. I dati di un visitatore possono essere riutilizzati per il successivo se restano in un singleton o in una proprietà statica. Dichiaro quindi con scope limitato alla richiesta il gateway del rendering lato server, che conserva uno stato specifico della richiesta in corso: il container lo ricrea a ogni richiesta.
La scelta di FrankenPHP
Il file config/octane.php usa FrankenPHP come valore predefinito quando non è impostata alcuna variabile d'ambiente:
// config/octane.php
'server' => env('OCTANE_SERVER', 'frankenphp'),
RoadRunner è un altro server compatibile con i worker di Octane. È disponibile anche Swoole, ma richiede un'estensione PHP compilata.
Ho scelto FrankenPHP rispetto a RoadRunner per Caddy, il server web che integra, e perché il suo sviluppo resta attivo nell'ecosistema Laravel. È anche più semplice da gestire. Il mio documento di decisione architetturale metteva in evidenza il supporto di Caddy per HTTP/3 e i certificati automatici. Su aubia.dev, FrankenPHP non se ne occupa: Cloudflare stabilisce le connessioni TLS e HTTP/3 con il browser, a monte del server.
Questo binario riguarda solo PHP: il rendering lato server viene eseguito in un processo Node separato.
Passare a RoadRunner richiederebbe i suoi pacchetti Composer e il suo binario, oltre a modificare OCTANE_SERVER.
Inertia e React, dalla route al bundle
Le pagine sono componenti React 19 scritti in TypeScript 7, memoizzati in fase di build dal React Compiler. Inertia 3 mostra il componente che una route o un controller designa per nome, con le sue props. Non ho scritto alcuna API propria per il sito: a parte le pagine di conferma e di disiscrizione, che interrogano aubia.cloud direttamente dal browser, i dati dell'interfaccia passano da queste props.
La home page e le pagine legali non hanno un controller: in routes/web.php, Route::inertia('/', 'welcome') associa l'URL al componente della pagina. Il blog passa da un controller, che cerca l'articolo richiesto prima di chiamare Inertia::render('blog/show', ...).
Un middleware aggiunge a ogni pagina props condivise, tra cui la lingua corrente e i testi di quella lingua. I componenti leggono quei testi con un hook useT.
Wayfinder genera funzioni TypeScript a partire dalle route di Laravel. La card di un articolo ottiene il proprio link chiamandone una con la lingua e lo slug, invece di costruire l'URL a mano. Se la route cambia nome, il controllo dei tipi fallisce.
Il rendering lato server di Inertia
Una pagina React con rendering solo nel browser dipende dal caricamento e dall'esecuzione di JavaScript prima di poter mostrare il suo contenuto. Non tutti i crawler lo eseguono, e quelli che lo fanno possono rimandare questo lavoro. Voglio che i motori di ricerca possano leggere il contenuto senza dover eseguire il JavaScript del sito.
Con l'SSR di Inertia, Laravel invia la pagina e i suoi dati a un processo Node. Node esegue il rendering dei componenti React in HTML e lo restituisce a Laravel, che lo include nella risposta tramite la vista Blade.
La risposta HTML iniziale contiene quindi il contenuto e i metadati della pagina. Laravel deve attendere il risultato di Node prima di inviarla. Il rendering di Node richiede 6 ms sulla mia macchina di sviluppo.
Nel browser, React idrata l'HTML ricevuto per rendere interattivi i componenti della pagina, form compreso.
In sviluppo, il plugin Vite di Inertia gestisce l'SSR senza che occorra avviare un server di rendering separato. In produzione, il processo Node gira accanto al server PHP: è un secondo processo da gestire.
La configurazione dell'SSR è dichiarata in config/inertia.php:
// config/inertia.php, estratto
'ssr' => [
'enabled' => (bool) env('INERTIA_SSR_ENABLED', true),
'runtime' => env('INERTIA_SSR_RUNTIME', 'node'),
'url' => env('INERTIA_SSR_URL'), // indirizzo interno predefinito omesso
'ensure_bundle_exists' => (bool) env('INERTIA_SSR_ENSURE_BUNDLE_EXISTS', true),
'throw_on_error' => (bool) env('INERTIA_SSR_THROW_ON_ERROR', false),
],
Quando il bundle SSR manca, ensure_bundle_exists evita la chiamata a Node e Laravel ripiega sul rendering lato client. L'impostazione throw_on_error a false consente lo stesso ripiego quando il rendering fallisce. La vista app.blade.php conserva un titolo e una descrizione specifici della pagina, le direttive robots e, per le pagine indicizzabili, l'URL canonico e i link hreflang disponibili.
Durante un guasto transitorio dell'SSR, preferisco questo ripiego a una pagina di errore, anche se la visualizzazione può subire ritardi. Un crawler che non esegue JavaScript non riceve allora più il contenuto dell'articolo.
Ho sostituito il gateway HTTP nativo di Inertia con ObservedSsrGateway, una classe che lo estende. Quando l'SSR è abilitato per la richiesta, questo gateway segnala errori e risposte assenti, vuote o non valide. Il log conserva solo il componente e una categoria di errore. Un HTML non vuoto ma incompleto passa invece senza segnalazione.
Il contatore dopo il rendering iniziale
Il contatore della lista d'attesa recupera il proprio totale da aubia.cloud e lo conserva in una cache lato server. Se la cache è vuota, quella chiamata di rete può ritardare la risposta.
Il middleware HandleInertiaRequests condivide il contatore come prop differita solo sulla home page, che lo mostra. Il browser ne richiede il valore dopo il rendering iniziale:
// app/Http/Middleware/HandleInertiaRequests.php
...($request->routeIs('home') ? ['waitlist' => [
'count' => Inertia::defer(fn(): ?int => app(FetchWaitlistCountQuery::class)->run(), rescue: true),
]] : []),
Se è la cache stessa a non funzionare, rescue: true omette la prop dal payload senza far fallire la richiesta differita. Il contatore resta allora assente dalla pagina.
La richiesta differita richiede 3 ms sulla mia macchina di sviluppo, con la cache del server calda.
La build con Vite 8 e Rolldown
Vite 8 integra nativamente il proprio bundler, Rolldown, scritto in Rust. Le dipendenze JavaScript sono installate da Bun, che esegue anche gli script di build; il rendering lato server resta eseguito da Node. La build di produzione aggiunge un bundle SSR dedicato, prodotto da vite build --ssr.
Il code splitting è configurato esplicitamente nella configurazione di build:
// vite.config.ts
build: {
rolldownOptions: {
output: {
codeSplitting: {
minSize: 20_000,
groups: [
{
name: 'react',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
priority: 50,
},
{
name: 'motion',
test: /[\\/]node_modules[\\/](motion|motion-dom|motion-utils)[\\/]/,
priority: 40,
},
{
name: 'inertia',
test: /[\\/]node_modules[\\/]@inertiajs[\\/]/,
priority: 20,
},
],
},
},
},
},
Ho dichiarato tre gruppi: React e il suo scheduler, le animazioni Motion e il client Inertia. Il browser può riutilizzare i loro file in cache da un deploy all'altro, finché la build non ne modifica il contenuto.
Durante la build di produzione, Sharp prepara varianti WebP e AVIF delle copertine e dell'avatar, senza ingrandire le sorgenti. Vite le scrive in /build/assets, con URL che contengono l'hash di ciascun file. Le immagini visualizzate usano srcset, sizes e dimensioni esplicite; i JPEG originali restano disponibili per la condivisione sui social. La copertina di un articolo ha priorità, mentre le card sotto il viewport vengono caricate su richiesta.
Tailwind CSS 4 configurato nel foglio di stile
Lo stile passa da Tailwind CSS 4, compilato dal suo plugin Vite. La configurazione è scritta in CSS, senza un file tailwind.config.js: resources/css/app.css importa Tailwind, poi i fogli di stile del design system, tra cui un blocco @theme che dichiara, fra l'altro, i colori e i raggi dei bordi. Questi valori diventano classi utilizzabili nei componenti.
I pulsanti, i menu e i tooltip provengono dal design system di Aubia, distribuito come registry shadcn. Il loro codice viene copiato nel repository, sotto resources/js/components, e si basa su Base UI, che gestisce il focus e la navigazione da tastiera.
Il design system definisce anche i due temi, l'effetto vetro, i font e le animazioni. Ne descrivo le impostazioni e i limiti nell'articolo che gli è dedicato.
Laravel Cloud per l'hosting, Pest per i test
Laravel Cloud ospita il sito, con dati conservati nell'Unione europea, ed esegue i suoi due processi, FrankenPHP per Laravel e Node per l'SSR.
Laravel Nightwatch segnala gli errori e le richieste lente della produzione. L'informativa sulla privacy precisa i dati trattati da Laravel Cloud, Cloudflare e Nightwatch.
La suite di test è scritta con Pest 5. I test unitari e feature coprono, fra l'altro, le Action, i controller e le risposte Inertia; i test di architettura verificano regole di codice, come declare(strict_types=1) in ogni file PHP scritto per il progetto o la coerenza delle chiavi tra i file di lingua.
Un feature test avvia il vero bundle SSR su Node e verifica che le frasi della home page compaiano nell'HTML prima che nel browser venga eseguito alcun JavaScript. I test browser passano dal plugin Browser di Pest, che pilota Chromium con Playwright.
A ogni pull request, GitHub Actions controlla il PHP con Pint, il TypeScript con Oxlint e Oxfmt, poi i tipi. In parallelo, la suite Pest viene eseguita in quattro batch e i test browser in un workflow separato.
Nei test, una richiesta HTTP verso un servizio esterno fallisce se non è stata simulata, e le risposte di aubia.cloud sono costruite con Http::fake. Se il contratto dell'API di aubia.cloud cambia, nessun test del sito pubblico lo rileva.
La beta di Aubia è gratuita e senza limiti di tempo. Per ricevere il suo invito basta iscriversi.
Si iscriva alla lista d'attesa