Saltar al contenido
Todos los artículos

Cómo está hecha esta web: estática donde se puede, en streaming donde hace falta

Esta web también es un proyecto de referencia. Tiene tres idiomas, decenas de páginas, más de una docena de herramientas interactivas y una guía de IA que responde mientras escribe. Saca 100 en las cuatro categorías de Lighthouse en móvil y funciona con los niveles gratuitos de Azure Static Web Apps y de la API de Gemini. Nada de eso necesita un stack exótico: React, Vite, un paso de prerender y un puñado de comprobaciones que se niegan a desplegar algo roto. Así encajan las piezas, incluidas dos cosas que solo descubrí midiendo.

10 min de lectura

1. Una fuente de verdad, muchas salidas

Los artículos, las páginas de servicios, los proyectos y las herramientas viven como datos tipados en el repositorio, un archivo por artículo y por servicio. La compilación convierte esos datos en todo lo que necesita un lector o una máquina: las páginas, un índice para los listados, un sitemap con las tres versiones de idioma de cada URL, un feed RSS por idioma, una imagen para compartir por artículo e idioma, el resumen llms.txt para asistentes de IA y el archivo de conocimiento con el que responde la guía.

Los dos últimos se mantenían a mano y se desincronizaron la primera vez que cambió el nombre de un producto. Ahora un script los regenera con los mismos datos en cada compilación, y una comprobación falla si la versión guardada no coincide. Nada se escribe dos veces, así que nada puede contradecirse.

json
// package.json
"build": "node scripts/gen-posts-meta.mjs && node scripts/gen-knowledge.mjs
          && tsc --noEmit -p tsconfig.app.json && vite build
          && node scripts/sitemap.mjs && node scripts/prerender.mjs"

2. Prerenderizar cada página, en cada idioma

La web es una app de React, pero nadie descarga una carcasa vacía. Tras la compilación con Vite, un script arranca la web compilada en local, abre cada ruta en cada idioma en un Chrome sin interfaz y guarda el HTML terminado en su propio archivo: cada página por tres idiomas. Cada uno tiene su título, su descripción, su URL canónica, enlaces hreflang a los otros dos idiomas y datos estructurados para artículos, FAQ y migas de pan. La misma pasada genera una imagen para compartir por artículo.

Los idiomas van en la ruta: inglés en la raíz, alemán en /de, español en /es. Azure Static Web Apps sirve cada archivo prerenderizado directamente mediante reglas de reescritura que genera la compilación, y una salvaguarda hace fallar la compilación si dos reglas chocan, cosa que pasó una vez con /de y /de/. Para los rastreadores es HTML estático normal; para las personas, la app de React toma el control y cada clic posterior es instantáneo.

Un repositorio, un pipeline: HTML estático para cada página e idioma, una función con streaming para la guía de IA y comprobaciones antes y después de cada despliegue.
Arquitectura de esta web: los datos de contenido del repositorio de GitHub se compilan con GitHub Actions (generar datos, comprobar tipos, compilación Vite, sitemap, prerenderizar cada página con Chrome sin interfaz, tests de extremo a extremo) y se despliegan en Azure Static Web Apps; los visitantes reciben HTML estático por idioma con redirección de idioma en línea, y la guía de IA llama a una Azure Function gestionada que transmite respuestas de Gemini 3.5 Flash con Flash-Lite de respaldo; tras cada despliegue se avisa a IndexNow, y tareas programadas revisan las páginas cada hora y los enlaces cada semana.

3. No desperdiciar el primer pintado

Tres mediciones cambiaron el código. Primero, los visitantes en alemán y español de la portada se redirigían a /de o /es solo después de cargar el bundle principal de 150 KB, lo que costaba un segundo en móvil. Ahora la redirección va como un script en línea de diez líneas al principio del HTML. La Content Security Policy permite exactamente ese script por su hash SHA-256, y la compilación falla si script y hash se separan.

Segundo, React sustituía la página prerenderizada por un marcador vacío hasta que llegaba el fragmento de código de la página, así que el contenido parpadeaba y Lighthouse contaba el segundo pintado. Ahora el navegador carga ese fragmento, y en los artículos también el artículo, antes de que React arranque, y el HTML prerenderizado se queda en pantalla hasta que React muestra lo mismo. Tercero, la fuente web entraba después del primer pintado y movía el texto. Con la fuente precargada y font-display en optional, o llega a tiempo o no se usa en esa visita. Todo junto bajó el mayor pintado en móvil de la portada de unos 2,5 a 1,1 segundos.

html
<!-- index.html: runs before CSS, fonts and the app bundle -->
<script>
  (function () {
    var l = location, p = l.pathname, s = new URLSearchParams(l.search), q = s.get("lang"), w;
    if (/^\/(de|es)(\/|$)/.test(p)) return;
    var ok = function (x) { return x === "en" || x === "de" || x === "es"; };
    try { w = ok(q) ? q : p === "/" ? [localStorage.getItem("dominik-lang"), (navigator.language || "").slice(0, 2)].filter(ok)[0] : ""; } catch (e) {}
    if (w !== "de" && w !== "es") return;
    s.delete("lang");
    var r = s.toString();
    l.replace("/" + w + (p === "/" ? "" : p) + (r ? "?" + r : "") + l.hash);
  })();
</script>
<!-- staticwebapp.config.json: script-src 'self' 'sha256-…' -->

4. Una guía de IA que hace streaming en un host estático

La guía de abajo a la derecha es mi propio motor de chat, el que hay detrás del producto Helptempo: un componente web en un Shadow DOM, para que el CSS de la web no pueda romperlo, y un único controlador de servidor que funciona en cualquier host. En esta web, el controlador corre como una Azure Function gestionada. Responde con el archivo de conocimiento generado más los títulos de la página en la que está el visitante, con Gemini 3.5 Flash y Gemini 3.5 Flash-Lite como respaldo, que solo entra antes de que se haya enviado la primera palabra. Las respuestas preparadas hacen instantáneas las preguntas sugeridas, y un límite por visitante mantiene viva la clave gratuita.

El streaming necesitó el modelo de programación v4 de Azure Functions con los streams HTTP activados. La sorpresa fue una sola cabecera: con Content-Type 'text/event-stream; charset=utf-8', Azure Static Web Apps retenía toda la respuesta hasta que estaba completa; con exactamente 'text/event-stream', cada trozo llegaba según se escribía. Lo aislé con ocho variantes en un entorno de preview antes de tocar producción. Ahora las primeras palabras aparecen tras unos 1,2 a 1,5 segundos en vez de la respuesta completa tras 2,5 a 3.

js
// api/chat/index.js — Azure Functions v4 on Static Web Apps
const { app } = require("@azure/functions");
app.setup({ enableHttpStream: true });
app.http("chat", { methods: ["GET", "POST"], authLevel: "anonymous", route: "chat",
  handler: async (req) => (await chat).azureFunctions(req) });

// in the engine: exactly this content type, no charset suffix
// "Content-Type": "text/event-stream"

5. Comprobaciones que se niegan a desplegar

Cada push a main compila la web, comprueba los tipos y ejecuta tests de extremo a extremo contra los archivos compilados antes de desplegar nada: cada ruta en cada idioma, en ancho de móvil y de escritorio, debe tener exactamente un h1, ningún scroll horizontal, ninguna imagen rota y ningún error de consola. El cuestionario, el cambio de idioma, los enlaces compartidos de las herramientas y la secuencia de arranque tienen sus propias comprobaciones, entre ellas una que ralentiza el JavaScript y falla si el título prerenderizado desaparece. La API del chat tiene sus propias pruebas, y el conocimiento de la guía de IA se prueba con 30 preguntas de visitantes con datos que debe decir y que no debe decir.

Tras el despliegue, la web avisa a IndexNow de qué URL han cambiado. Fuera de los despliegues, una tarea programada comprueba cada hora que responden todas las páginas, artículos, PDF de guías, feeds y el chat, y una tarea semanal revisa cada enlace a fuentes y cada vídeo de los artículos. Un enlace roto o una página caída pone en rojo un workflow y llega a mi bandeja antes de que lo encuentre un visitante.

6. Lo que cuesta, y lo que reutilizaría

El alojamiento es el plan gratuito de Azure Static Web Apps con sus funciones gestionadas, y la guía funciona con una clave gratuita de Gemini y un modelo de respaldo, así que el coste de funcionamiento es casi cero. Las concesiones son honestas: la cuota gratuita del modelo a veces se agota durante el día y responde el respaldo, y un host estático implica que el formulario de contacto envía a un servicio de formularios en vez de a un backend propio.

Lo que reutilizaría en cualquier web de empresa: prerenderizar en vez de renderizar en servidor cuando el contenido cambia unas pocas veces al día, generar cada archivo legible por máquinas desde una sola fuente de datos, medir antes de optimizar y dejar que el pipeline se niegue a desplegar en vez de confiar en que la gente recuerde las comprobaciones. El motor de chat es reutilizable por diseño; por eso se convirtió en un producto.

Conclusión

Una web rápida y multilingüe con una guía de IA en directo no necesita una granja de servidores: prerenderiza todo, protege el primer pintado, haz streaming solo de lo que lo necesita y deja que el pipeline diga que no.

Más artículos

    Hablemos

    Cuéntame qué estás construyendo; te respondo en 1 o 2 días laborables.

    Dominik Heinrich
    Empresa
    MADO Consulting

    Disponible en todo el mundo en remoto. Presencial bajo petición, según el país.

    Te respondo en 1 o 2 días laborables. Huecos libres: mira el calendario

    Abre aquí el calendario de reservas de Calendly. Al cargarlo se envían datos a Calendly (ver aviso legal).