Skip to content
SoyGarfield
Research #03

Cómo construí una tienda con catálogo en tiempo real y rebuild automático: el caso Los Luises

¿Se puede tener datos en tiempo real (precio, stock) en un sitio generado estáticamente, sin perder la velocidad del export estático ni depender de una persona para republicar?

Por Pietro Fiorillo ·

Índice del artículo
Diagrama de dos caminos desde una edición en Firestore. Lectura en vivo: listados ya abiertos actualizan precio y stock al instante vía onSnapshot, sin rebuild. Rebuild automático: una Cloud Function detecta el cambio, dispara Cloud Build, que corre next build y firebase deploy, dejando páginas y sitemap al día en minutos.
De una edición en Firestore a dos resultados distintos: datos instantáneos para quien ya tiene el sitio abierto, y un sitio republicado solo, sin que nadie corra el build. Diagrama propio.

El problema: un catálogo que cambia todos los días, en un sitio que se genera una sola vez

Los Luises es un taller de sublimación y estampado con más de 26 años de trayectoria. Su catálogo cambia seguido: precios, ofertas, stock y productos nuevos. El sitio, en cambio, es un export estático de Next.js: HTML generado una vez en build time, sin servidor detrás. Si cada cambio de precio necesitara un build completo, el catálogo nunca estaría al día.

Por qué separé la lectura en tiempo real del rebuild

En vez de forzar todo por un solo camino, separé el problema en dos. Para datos que cambian seguido (precio, stock, ofertas), el navegador escucha Firestore directamente. Para cambios que necesitan una página nueva o afectan el sitemap (un producto nuevo, por ejemplo), el sitio se vuelve a generar completo.

El hook que hace la lectura en tiempo real es simple: se suscribe a la colección products con onSnapshot y actualiza el estado cada vez que algo cambia en Firestore, sin que el usuario recargue la página.

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 };
}
Código real de lib/useProducts.ts en el repositorio del proyecto, octubre de 2026.

Cómo se dispara el rebuild solo

La otra mitad del sistema es una Cloud Function que escucha cualquier escritura en products/{productId}. Cuando detecta un cambio, llama directamente a la API de Cloud Build para arrancar un build nuevo, sin que una persona tenga que ejecutar nada.

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.
Código real (trimmed) de functions/index.js. El comentario sobre cloudbuild.yaml está en el archivo original, no lo agregué yo.

Ese comentario en el código es, literalmente, una nota que me dejé a mí mismo: los pasos del build (buildSteps()) tienen que coincidir con los de cloudbuild.yaml en la raíz del repo, y si cambio uno tengo que acordarme de cambiar el otro a mano. Vuelvo sobre esto en la sección de deuda técnica.

Cómo validé esta arquitectura

Corrí npm run build de verdad contra el repositorio (Next.js 16.2.12, Turbopack, con el .env.local real del proyecto) y medí el resultado en vez de describirlo de memoria. El build terminó en 18,8 segundos y generó 74 rutas estáticas, incluidas 54 páginas de producto (38 personalizados y 16 de stock) y un sitemap.xml con 62 URLs, que coincide con lo que devuelve el sitio en producción.

const nextConfig: NextConfig = {
  output: "export",
  images: {
    unoptimized: true,
  },
};
Código real de next.config.ts. Igual que en otros proyectos, images.unoptimized es obligatorio junto al export estático.

Resultados medibles

Medí lo siguiente el 18 de octubre de 2026, con Lighthouse (lab data, throttling simulado) contra el sitio en producción, y con una build real local.

Métricas reales medidas el 18 de octubre de 2026.
MétricaResultadoEntorno / evidencia
Lighthouse Performance (móvil, lab)61 / 100Lighthouse 13.5.0, throttling simulado
LCP (lab)18,3 sMisma medición Lighthouse, no es un dato de campo (CrUX)
Tiempo de build local18,8 snext build, Next.js 16.2.12, Turbopack
Tamaño del export estático24 MB en 781 archivosCarpeta /out generada por ese build
Rutas estáticas generadas74 (54 páginas de producto)Log del build
URLs en el sitemap62sitemap.xml generado por el build y confirmado en producción

El LCP de 18,3 segundos es el más alto de todos los proyectos que medí para esta serie. Mi hipótesis, a falta de perfilar el navegador a fondo, es que se suman dos costos: images.unoptimized: true (igual que en otros proyectos con export estático) y que la lista de productos depende de que onSnapshot reciba el primer snapshot de Firestore antes de poder pintar nada. Es una hipótesis razonable a partir del código, no una causa confirmada con profiling.

Seguridad por capas: lo que permiten estas reglas y lo que no

La colección products se puede leer sin autenticación (son datos de catálogo, no sensibles) pero solo se puede escribir con sesión iniciada. Son las reglas reales del proyecto, sin resumir ni suavizar.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /products/{productId} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}
Código real de firestore.rules en el repositorio del proyecto.

Esto protege contra escrituras anónimas a la base de datos. No dice nada sobre la seguridad del panel /admin en sí (login, gestión de sesión) ni sobre otras partes del sistema: es una pieza de la seguridad, no toda la seguridad.

Separar lo que cambia todo el tiempo de lo que necesita una página nueva no es una optimización, es la pregunta correcta antes de elegir la arquitectura.

Pietro Fiorillo

Mi participación en este proyecto

Mi participación en Los Luises Sublimación incluyó el diseño completo de la arquitectura, el desarrollo full-stack (frontend, panel admin y Cloud Function), la infraestructura en Firebase y Google Cloud, y el SEO técnico, como parte de mi trabajo en MANYA Digital.

Lo que haría distinto: una advertencia que encontré en mi propio código

Revisando el repositorio para este artículo encontré dos cosas que no recordaba con precisión. La primera: los pasos del pipeline de build están escritos dos veces, una vez en la Cloud Function y otra en cloudbuild.yaml, y el propio código tiene un comentario pidiendo que se actualicen juntos. Es exactamente el tipo de duplicación que se desincroniza con el tiempo si nadie la nota.

La segunda: el runtime de la Cloud Function está fijado en Node 20, mientras que los pasos de Cloud Build corren sobre imágenes de Node 22. No rompe nada hoy, pero son dos versiones de Node conviviendo en el mismo pipeline sin que eso esté documentado en ningún lado hasta este artículo.

Ninguno de los dos puntos afecta al sitio en producción ahora mismo. Los dejo documentados porque son el tipo de detalle que vale la pena conocer antes de tocar el pipeline, no después de que algo se rompa por una actualización hecha en un solo lugar.

Qué probaría después

Unificaría la definición del pipeline en un solo archivo (que la Cloud Function lea cloudbuild.yaml en vez de duplicar sus pasos en JavaScript), alinearía la versión de Node entre la función y las imágenes de Cloud Build, y optimizaría las imágenes del catálogo para bajar el LCP medido de 18,3 segundos, probablemente el cambio con más impacto real de los tres.

Hasta entonces, el sistema cumple lo que promete: precio y stock al instante, páginas nuevas sin que nadie corra un deploy a mano, y una advertencia en el código que, ahora sí, queda escrita en un lugar donde no se va a perder.

Fuentes

Preguntas frecuentes

¿Por qué no usar simplemente ISR de Next.js en vez de Firestore en tiempo real?

Porque el sitio está exportado como estático (output: export), y ese modo no soporta ISR: ningún valor de revalidate tiene efecto real en producción. La lectura en tiempo real con onSnapshot resuelve el problema de otra forma, en el navegador, sin depender de un servidor Next.

¿Qué pasa si Cloud Build falla al reconstruir el sitio?

El sitio público sigue funcionando con la última versión desplegada; no se cae. Lo que no ocurre es que las páginas nuevas, el sitemap o los datos estructurados se actualicen hasta que una build posterior tenga éxito. No hay una alerta automática de build fallido verificada en esta entrega.

¿Es seguro que cualquiera pueda leer la colección de productos?

Las reglas de Firestore permiten lectura pública solo de la colección products y exigen autenticación para escribir. Es el mismo modelo que usaría una API pública de catálogo: los datos de producto no son sensibles, pero nadie puede modificarlos sin iniciar sesión.

¿Por qué dos sistemas (lectura en vivo y rebuild) en vez de uno solo?

Resuelven problemas distintos. onSnapshot actualiza precio y stock al instante en una página ya cargada, pero no crea páginas nuevas ni actualiza el sitemap. El rebuild hace eso, pero tarda minutos. Juntos cubren tanto la actualización inmediata como el SEO estático.

¿Cuánto tarda en verse un cambio real?

Depende de qué cambie. Un precio o el estado de stock se ve al instante en cualquier pestaña abierta. Un producto nuevo necesita que termine el build (medí 18,8 segundos en local, sin contar el deploy) antes de tener su propia página y aparecer en el sitemap.

¿Cuánto cuesta mantener una arquitectura así?

El costo depende del uso de Firestore, Cloud Functions, Cloud Build y Firebase Hosting, que tienen niveles gratuitos amplios para un catálogo de este tamaño. No hay un servidor Node propio que pagar ni mantener.