Skip to content
SoyGarfield
Research #03

Come ho costruito un negozio con catalogo in tempo reale e rebuild automatico: il caso Los Luises

Si possono avere dati in tempo reale (prezzo, stock) su un sito generato staticamente, senza perdere la velocità dell'export statico né dipendere da una persona per ripubblicare?

Di Pietro Fiorillo ·

Indice dell'articolo
Diagramma di due percorsi da una modifica in Firestore. Lettura live: elenchi già aperti aggiornano prezzo e stock all'istante via onSnapshot, senza rebuild. Rebuild automatico: una Cloud Function rileva la modifica, avvia Cloud Build, che esegue next build e firebase deploy, lasciando pagine e sitemap aggiornate in pochi minuti.
Da una modifica in Firestore a due risultati diversi: dati istantanei per chi ha già il sito aperto, e un sito ripubblicato da solo, senza che nessuno esegua la build. Diagramma originale.

Il problema: un catalogo che cambia ogni giorno, in un sito generato una volta sola

Los Luises è un laboratorio di sublimazione e stampa con oltre 26 anni di attività. Il suo catalogo cambia spesso: prezzi, offerte, stock e nuovi prodotti. Il sito, invece, è un export statico di Next.js: HTML generato una volta in build time, senza server dietro. Se ogni modifica di prezzo richiedesse una build completa, il catalogo non sarebbe mai aggiornato.

Perché ho separato la lettura in tempo reale dal rebuild

Invece di forzare tutto su un unico percorso, ho diviso il problema in due. Per i dati che cambiano spesso (prezzo, stock, offerte), il browser ascolta Firestore direttamente. Per le modifiche che richiedono una pagina nuova o toccano la sitemap (un prodotto nuovo, per esempio), il sito viene rigenerato per intero.

L'hook che fa la lettura in tempo reale è semplice: si iscrive alla collezione products con onSnapshot e aggiorna lo stato ogni volta che qualcosa cambia in Firestore, senza che l'utente ricarichi la pagina.

export function useProducts() {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const unsubscribe = onSnapshot(
      collection(db, "products"),
      (snapshot) => {
        setProducts(snapshot.docs.map((doc) => doc.data() as Product));
        setLoading(false);
      },
      (err) => {
        setError(err);
        setLoading(false);
      }
    );
    return unsubscribe;
  }, []);

  return { products, loading, error };
}
Codice reale di lib/useProducts.ts nel repository del progetto, ottobre 2026.

Come si avvia il rebuild da solo

L'altra metà del sistema è una Cloud Function che ascolta qualsiasi scrittura su products/{productId}. Quando rileva una modifica, chiama direttamente l'API di Cloud Build per avviare una build nuova, senza che una persona debba eseguire nulla.

exports.rebuildOnProductChange = onDocumentWritten(
  { document: "products/{productId}", region: "us-central1", memory: "256MiB" },
  async (event) => {
    const auth = new GoogleAuth({
      scopes: ["https://www.googleapis.com/auth/cloud-platform"],
    });
    const client = await auth.getClient();
    const projectId = await auth.getProjectId();

    const body = {
      source: { storageSource: { bucket, object } },
      steps: buildSteps(),
      timeout: "1200s",
    };

    await client.request({
      url: `https://cloudbuild.googleapis.com/v1/projects/${projectId}/builds`,
      method: "POST",
      data: body,
    });
  }
);

// Debe reflejar los mismos steps que cloudbuild.yaml en la raíz del repo.
// Si cambiás cloudbuild.yaml, actualizá esto también.
Codice reale (abbreviato) di functions/index.js. Il commento su cloudbuild.yaml è nel file originale, non l'ho aggiunto io.

Quel commento nel codice è, letteralmente, una nota che ho lasciato a me stesso: i passi della build (buildSteps()) devono coincidere con quelli di cloudbuild.yaml nella root del repo, e se ne cambio uno devo ricordarmi di cambiare anche l'altro a mano. Ci torno nella sezione sul debito tecnico.

Come ho validato questa architettura

Ho eseguito davvero npm run build contro il repository (Next.js 16.2.12, Turbopack, con il .env.local reale del progetto) e ho misurato il risultato invece di descriverlo a memoria. La build è terminata in 18,8 secondi e ha generato 74 rotte statiche, incluse 54 pagine prodotto (38 personalizzati e 16 di stock) e un sitemap.xml con 62 URL, che coincide con quanto restituisce il sito in produzione.

const nextConfig: NextConfig = {
  output: "export",
  images: {
    unoptimized: true,
  },
};
Codice reale di next.config.ts. Come in altri progetti, images.unoptimized è obbligatorio insieme all'export statico.

Risultati misurabili

Ho misurato quanto segue il 18 ottobre 2026, con Lighthouse (dati di laboratorio, throttling simulato) contro il sito in produzione, e con una build reale in locale.

Metriche reali misurate il 18 ottobre 2026.
MetricaRisultatoAmbiente / evidenza
Lighthouse Performance (mobile, lab)61 / 100Lighthouse 13.5.0, throttling simulato
LCP (lab)18,3 sStessa misurazione Lighthouse, non è un dato di campo (CrUX)
Tempo di build locale18,8 snext build, Next.js 16.2.12, Turbopack
Dimensione dell'export statico24 MB in 781 fileCartella /out generata da quella build
Rotte statiche generate74 (54 pagine prodotto)Log della build
URL nella sitemap62sitemap.xml generato dalla build e confermato in produzione

L'LCP di 18,3 secondi è il più alto tra tutti i progetti che ho misurato per questa serie. La mia ipotesi, senza aver profilato a fondo il browser, è che si sommino due costi: images.unoptimized: true (come in altri progetti con export statico) e il fatto che la lista prodotti dipenda dal primo snapshot di Firestore ricevuto da onSnapshot prima di poter disegnare qualcosa. È un'ipotesi ragionevole a partire dal codice, non una causa confermata con il profiling.

Sicurezza a livelli: cosa permettono queste regole e cosa no

La collezione products si può leggere senza autenticazione (sono dati di catalogo, non sensibili) ma si può scrivere solo con una sessione autenticata. Sono le regole reali del progetto, senza riassumere né addolcire.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /products/{productId} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}
Codice reale di firestore.rules nel repository del progetto.

Questo protegge da scritture anonime sul database. Non dice nulla sulla sicurezza del pannello /admin in sé (login, gestione della sessione) né su altre parti del sistema: è un pezzo della sicurezza, non tutta la sicurezza.

Separare ciò che cambia continuamente da ciò che richiede una pagina nuova non è un'ottimizzazione, è la domanda giusta prima di scegliere l'architettura.

Pietro Fiorillo

Il mio ruolo in questo progetto

Il mio ruolo in Los Luises Sublimación ha incluso il design completo dell'architettura, lo sviluppo full-stack (frontend, pannello admin e Cloud Function), l'infrastruttura su Firebase e Google Cloud, e la SEO tecnica, come parte del mio lavoro in MANYA Digital.

Cosa farei diversamente: un avvertimento che ho trovato nel mio stesso codice

Rivedendo il repository per questo articolo ho trovato due cose che non ricordavo con precisione. La prima: i passi della pipeline di build sono scritti due volte, una nella Cloud Function e una in cloudbuild.yaml, e il codice stesso ha un commento che chiede di aggiornarli insieme. È esattamente il tipo di duplicazione che si disallinea nel tempo se nessuno se ne accorge.

La seconda: il runtime della Cloud Function è fissato a Node 20, mentre i passi di Cloud Build girano su immagini Node 22. Non rompe nulla oggi, ma sono due versioni di Node che convivono nella stessa pipeline senza che questo sia documentato da nessuna parte fino a questo articolo.

Nessuno dei due punti influisce sul sito in produzione in questo momento. Li lascio documentati perché sono il tipo di dettaglio che vale la pena conoscere prima di toccare la pipeline, non dopo che qualcosa si rompe per un aggiornamento fatto in un solo posto.

Cosa proverei dopo

Unificherei la definizione della pipeline in un solo file (facendo leggere alla Cloud Function cloudbuild.yaml invece di duplicarne i passi in JavaScript), allineerei la versione di Node tra la funzione e le immagini di Cloud Build, e ottimizzerei le immagini del catalogo per abbassare l'LCP misurato di 18,3 secondi, probabilmente la modifica con l'impatto reale maggiore delle tre.

Fino ad allora, il sistema mantiene quello che promette: prezzo e stock all'istante, pagine nuove senza che nessuno esegua un deploy a mano, e un avvertimento nel codice che, ora sì, resta scritto in un posto dove non andrà perso.

Fonti

Domande frequenti

Perché non usare semplicemente l'ISR di Next.js invece di Firestore in tempo reale?

Perché il sito è esportato come statico (output: export), e quella modalità non supporta l'ISR: nessun valore di revalidate ha un effetto reale in produzione. La lettura in tempo reale con onSnapshot risolve il problema in un altro modo, nel browser, senza dipendere da un server Next.

Cosa succede se Cloud Build fallisce nel ricostruire il sito?

Il sito pubblico continua a funzionare con l'ultima versione pubblicata; non si ferma. Quello che non succede è che le pagine nuove, la sitemap o i dati strutturati si aggiornino finché una build successiva non va a buon fine. Non ho verificato un avviso automatico di build fallita in questa uscita.

È sicuro che chiunque possa leggere la collezione di prodotti?

Le regole di Firestore permettono la lettura pubblica solo della collezione products e richiedono l'autenticazione per scrivere. È lo stesso modello di una API pubblica di catalogo: i dati prodotto non sono sensibili, ma nessuno può modificarli senza aver fatto login.

Perché due sistemi (lettura live e rebuild) invece di uno solo?

Risolvono problemi diversi. onSnapshot aggiorna prezzo e stock all'istante su una pagina già caricata, ma non crea pagine nuove né aggiorna la sitemap. Il rebuild fa questo, ma richiede minuti. Insieme coprono sia l'aggiornamento immediato sia la SEO statica.

Quanto tempo ci vuole per vedere una modifica reale?

Dipende da cosa cambia. Un prezzo o lo stato dello stock si vede all'istante in qualsiasi scheda aperta. Un prodotto nuovo richiede che la build finisca (ho misurato 18,8 secondi in locale, senza contare il deploy) prima di avere una propria pagina e comparire nella sitemap.

Quanto costa mantenere un'architettura così?

Il costo dipende dall'uso di Firestore, Cloud Functions, Cloud Build e Firebase Hosting, che hanno livelli gratuiti ampi per un catalogo di queste dimensioni. Non c'è un server Node proprio da pagare né da mantenere.