Abrí mi propio blog un sábado y encontré una entrada que parecía de 1996: texto plano, sin fuente, sin márgenes, sin cabecera. El HTML seguía ahí, la URL respondía 200, pero el CSS y el layout habían desaparecido del render. No fue un problema de estilos: fue un archivo .astro que estaba en el formato equivocado y Astro decidió tratarlo como otra cosa. Cómo lo detecté y los dos validadores que implementé para que nunca más un build de CI/CD se despliegue con ese defecto.
El síntoma: post en vivo, estilo muerto
El post afectado era patrones-resiliencia-pipelines-ia-produccion, escrito en Markdown con frontmatter estilo --- YAML arriba. Eso es normal en un .md, pero mi sitio no publica Markdown en src/pages/blog/: publica componentes Astro. Cada post es un .astro que importa un layout compartido:
import BlogLayout from '../../layouts/BlogLayout.astro';
const seo = { title: '...', canonical: '...', ogImage: '...' };
---
<BlogLayout title={seo.title} ...>
<h2>Sección</h2>
<p>Contenido</p>
</BlogLayout> El build no falló. Astro compila todo lo que hay en src/pages/, y un .astro sin import de layout tampoco es un error de compilación: se convierte en una página válida que simplemente no envuelve su contenido en el layout. Resultado: página servida por Cloudflare Pages con HTTP 200, contenido completo y cero CSS. El defecto más difícil de atrapar en un pipeline es el que compila bien.
Cómo lo diagnosticé
Fui del síntoma a la causa en tres pasos:
- Verifiqué el HTML servido: la página tenía el
<p>y el<h2>, pero no traía las clases ni el<head>del layout. Contenido sin wrap = página que no pasó porBlogLayout. - Comparé con un post sano: los posts correctos empiezan con
---seguido de unimportde JavaScript. El roto empezaba con YAML de frontmatter classic. - Rastreé el origen: el post lo había generado un cron nocturno. El cron sabía escribir Markdown, no componentes Astro. El error no fue de tipeo: fue de plantilla.
Corrí el HTML a mano en ambos archivos y avisé, pero el punto importante fue el siguiente: si un cron lo escribió mal una vez, otro cron lo escribirá mal de nuevo. El fix individual no sirve sin control en el pipeline.
El fix estructural: validadores prebuild
Implementé dos scripts en scripts/ del repo. El primero valida un post individual — lo uso desde los crons antes de escribir el archivo final:
#!/bin/bash
# validate-astro-post.sh <path> — exit 1 si el formato es incorrecto
FILE="$1"
HAS_IMPORT=$(grep -c "import BlogLayout" "$FILE")
HAS_FM=$(head -1 "$FILE" | grep -c "^---$")
if [ "$HAS_FM" -gt 0 ] && [ "$HAS_IMPORT" -eq 0 ]; then
echo "ERROR: frontmatter Markdown en lugar de Astro Component"
exit 1
fi
[ "$HAS_IMPORT" -eq 0 ] && { echo "ERROR: falta import BlogLayout"; exit 1; }
echo "OK"; exit 0 El segundo recorre src/pages/blog/ y src/pages/en/blog/, ignora los index.astro y acumula errores. La pieza clave no es el script: es el hook que lo hace obligatorio. Lo conecté al package.json como paso prebuild:
{
"scripts": {
"prebuild": "npm run validate && mkdir -p public/blog && cp -n src/assets/blog/*.webp public/blog/ 2>/dev/null; true",
"build": "npm run prebuild && astro build"
}
} Con esto, npm run build deja de ser un comando de compilación pasivo y se convierte en una compuerta: si algún .astro tiene frontmatter Markdown sin import BlogLayout, el build muere antes de generar dist/, y el deploy a Cloudflare Pages nunca llega a salir. El mismo patrón sirve para cualquier check estructural que quieras imponer a contenido generado automáticamente — planes de Terraform linted, esquemas de PostgreSQL validados, specs de API revisados — siempre en el punto de entrada del pipeline, no en una revisión manual posterior.
La regla que me llevé
La lección de fondo es una que aprendí antes en IaC y reconfirmé aquí: un defecto que compila bien solo se atrapa con una regla explícita. Astro no sabe que mi blog exige BlogLayout; es convención mía, y lo convencional no se enforcement solo. La única defensa real contra contenido generado por automatización es un código de salida distinto de cero en el pipeline. Un cron que escribe mal un archivo es un incidente; el mismo cron con un validador que lo bloquea es solo un log.
Resultados
- Detección: 0 defectos desde el fix. Los crons de blog que corren desde entonces (agosto a octubre 2026) tienen 100% de posts con formato correcto; el validador individual rechaza la mala plantilla antes de que llegue a
src/pages/. - 2 archivos corregidos a mano durante el incidente (versión ES y EN del mismo post), y 3 mecanismos de prevención instalados: validador individual, validador global y hook
prebuild. - Costo de la compuerta: ~1 segundo. El script somete todo el blog a un
greppor archivo y sale con código 0 o 1; no hay parsing de Astro ni overhead de build. - Pipeline blindado: hoy
npm run buildno produce artefacto si cualquier post está en formato incorrecto — en local o en CI/CD. Un build fallido es señal de defecto en minutos, no un post feo en producción durante días.
Si trabajas con Astro, CI/CD o pipelines de contenido y te interesa ver más de este trabajo de infraestructura, pasa por danieltini.dev o mira el código en github.com/daniel-alejandro-t.