4 de agosto de 2026

Actualizar a Astro 7: el compilador en Rust ya no te perdona el HTML sucio

Foto de Marco Orta Marco Orta | 14 min de lectura
Compartir
Paneles de cristal con degradado violeta y naranja alineándose en una rejilla perfecta mientras algunos fragmentos irregulares son rechazados
Tabla de Contenidos

    Astro 7 salió el 22 de junio de 2026 y es la actualización más profunda del framework desde que existe: el compilador de .astro está reescrito en Rust, el pipeline de Markdown ya no usa remark/rehype, y el motor de render pasa de recursivo a encolado.

    Para la mayoría de proyectos el upgrade es de unos minutos. Pero hay tres cambios que rompen en silencio —y uno de ellos, el de espacios en blanco, puede pegarte palabras en producción sin que ningún error te avise.

    Esta guía la escribo desde este mismo sitio: ortamarco.me corre Astro 7.0.7 en producción, con Vite 8, MDX, plugins rehype propios y adaptador de Node. Los números y las trampas que verás abajo salen de aquí, no de las notas de la versión.

    Si lo que buscas es una introducción a Astro —qué es, islas, Actions, Content Layer—, eso está en la guía completa de Astro.js. Aquí vamos a la migración.

    Qué cambió, en una pantalla

    ÁreaAntes (Astro 6)Ahora (Astro 7)
    Compilador .astroGoRust, y es el único
    Markdown / MDXunified (remark + rehype)Sätteri (Rust, sobre pulldown-cmark y Oxc)
    RenderRecursivoEncolado, ~2.4× más rápido
    BundlerVite 7Vite 8 (Rolldown)
    compressHTMLtrue'jsx'
    RoutingConvencionalAdvanced Routing con src/fetch.ts
    CachéPor adaptadorAstro.cache + routeRules
    @astrojs/dbExistíaEliminado

    Requisito mínimo: Node 22.12.0. Si vienes de Node 20, ese salto es obligatorio y además Node 20 murió en abril de 2026, así que toca de todas formas.

    Los números, y qué se puede creer de ellos

    El anuncio oficial habla de builds entre un 15 % y un 61 % más rápidos, con algunos sitios construyendo más del doble de rápido. Los benchmarks publicados son concretos:

    SitioAstro 6Astro 7
    astro.build62.70 s24.24 s
    docs.astro.build114.54 s73.53 s
    tauri.app86.12 s55.33 s
    developers.cloudflare.com386.89 s261.94 s
    biomejs.dev176.39 s149.90 s
    aspire.dev385.84 s326.11 s

    Fíjate en el rango: astro.build baja un 61 %, pero biomejs.dev solo un 15 %. La mejora depende muchísimo de cuánto Markdown tengas, porque el grueso viene de Sätteri, no del compilador. El propio equipo mide que el compilador Rust por sí solo aporta «aproximadamente un 6 %» en docs.astro.build.

    Mis números medidos en este sitio, ahora mismo:

    Astro 7.0.7 · Node 24.16.0 · WSL2
    102 posts · 163 páginas HTML · 125 MB de salida
    
    Build en frío (sin dist ni .astro):  17.69 s   (servidor: 14.06 s)
    Build en caliente:                   17.36 s   (servidor: 13.79 s)
    Índice de Pagefind:                   0.23 s   (102 páginas, 15 368 palabras)
    

    Aviso honesto: no puedo darte el delta. Este sitio migró a 7 sin dejar una medición limpia de la 6, así que compararlo sería inventármelo. Lo que sí te sirve del dato de arriba es la escala: un sitio de cien y pico páginas con MDX, Preact, Tailwind 4 y dos plugins rehype se construye entero en menos de 18 segundos. Si el tuyo está en ese orden de magnitud y tarda mucho más, el problema no es Astro.


    Trampa 1: el compilador en Rust ya no arregla tu HTML

    El compilador de Go corregía en silencio el HTML mal formado: cerraba etiquetas que dejabas abiertas y reordenaba anidamientos inválidos. El de Rust no. Trata el marcado tal cual.

    Esto se traduce en tres cosas:

    Las etiquetas sin cerrar ahora son error de compilación. No un warning: el build falla.

    <!-- Astro 6: se cerraba solo, y tú ni te enterabas -->
    <div class="card">
      <p>Hola
    
    <!-- Astro 7: error de compilación -->
    

    Los elementos void (<br>, <img>, <input>, <hr>) siguen sin necesitar cierre. Y los atributos sin terminar también revientan ahora.

    El HTML inválido ya no se reordena. Si tienes un <div> dentro de un <p>, antes el compilador lo sacaba fuera silenciosamente para producir HTML válido. Ahora lo deja donde lo pusiste, y el navegador aplicará sus reglas de recuperación — que no son las mismas. El resultado es que una página puede renderizarse distinto sin que nada falle.

    Este es, con diferencia, el caso más traicionero de todo el upgrade, porque no hay error que perseguir. Si tienes componentes antiguos con anidamiento dudoso, compáralos renderizados antes y después.

    La serialización de CSS cambia. Los valores de color y el formato de url() pueden salir escritos de otra forma. Es puramente cosmético y no afecta al comportamiento, pero si tienes tests de snapshot sobre CSS, van a fallar en masa por nada.

    La buena noticia: los dos primeros casos se cazan compilando. El tercero se caza leyendo el diff.

    Trampa 2: compressHTML pasa de true a 'jsx'

    Esta es la que muerde de verdad, y la que quiero que te lleves aunque no leas nada más.

    El nuevo valor por defecto aplica las reglas de espacios en blanco de JSX: los saltos de línea entre elementos inline dejan de producir un espacio visible.

    <span>hola</span>
    <em>mundo</em>
    
    • Astro 6 renderizaba hola mundo
    • Astro 7 renderiza holamundo

    En prosa y en enlaces esto es un desastre silencioso. Ningún error, ningún warning: simplemente tu texto empieza a salir con palabras pegadas, y lo descubres cuando alguien te lo dice.

    Tienes dos salidas. La correcta a largo plazo es adoptar las reglas de JSX y poner los espacios explícitos:

    <span>hola</span>{" "}
    <em>mundo</em>
    

    La pragmática, y la que yo elegí en este sitio, es fijar el valor antiguo y migrar después de forma deliberada:

    // astro.config.mjs
    export default defineConfig({
      // Astro 7 cambió el default de `compressHTML` de `true` a `'jsx'` (elimina
      // espacios en blanco entre elementos inline como React). Lo fijamos en `true`
      // para conservar exactamente el output de v6 y evitar que se peguen palabras
      // en prosa/enlaces. Se puede migrar a `'jsx'` más adelante de forma deliberada.
      compressHTML: true,
    });
    

    Ese comentario está copiado literalmente de la configuración de este blog. Fijarlo convierte un problema de contenido —que se descubre tarde y mal— en una tarea de refactor que haces cuando quieras.

    Si decides adoptar 'jsx', la forma seria de hacerlo es construir dos veces y comparar el HTML servido, no ir página por página a ojo:

    # Construye con el valor antiguo, guarda una página representativa
    npm run build && cp dist/client/blog/tu-post/index.html /tmp/antes.html
    
    # Cambia a compressHTML: 'jsx', reconstruye
    npm run build && cp dist/client/blog/tu-post/index.html /tmp/despues.html
    

    Y ahí pegas los dos y miras qué se movió:

    Trampa 3: Sätteri sustituye a remark y rehype

    Astro 7 reemplaza el pipeline de unified por Sätteri, un procesador escrito en Rust sobre pulldown-cmark y Oxc. En builds de documentación grandes esto quita más de un minuto, y trae soporte nativo para GFM, puntuación tipográfica, IDs de encabezado, contenedores, matemáticas y frontmatter — cosas que antes pedían un plugin cada una.

    El precio es que @astrojs/markdown-remark ya no se instala por defecto. Si dependes de plugins remark o rehype, la documentación te pide instalarlo y declarar markdown.processor: unified(), o portar tus plugins al formato MDAST/HAST de Sätteri.

    Aquí va un matiz que verifiqué en este repo y que no he visto documentado en ningún sitio. Este blog usa dos plugins rehype propios:

    // astro.config.mjs
    markdown: {
      syntaxHighlight: {
        type: 'prism',
        excludeLangs: ['mermaid'],
      },
      rehypePlugins: [rehypeResponsiveTables, rehypeMermaid],
    },
    

    No declara markdown.processor: unified() y funciona igual: los diagramas mermaid se renderizan y las tablas salen con su contenedor responsive. El motivo es que @astrojs/markdown-remark entra igualmente como dependencia transitiva:

    $ npm ls @astrojs/markdown-remark
    [email protected]
    ├─┬ @astrojs/[email protected]
    │ └── @astrojs/[email protected]
    └─┬ [email protected]
      └── @astrojs/[email protected] deduped
    

    Traducido: si usas @astrojs/mdx, el paquete llega solo y tus plugins rehype siguen corriendo. Aun así, yo lo declararía explícitamente en package.json en vez de depender de una transitiva, porque el día que Astro deje de arrastrarlo tu build se rompe sin que hayas tocado nada.

    Verifica el tuyo antes de asumir nada:

    npm ls @astrojs/markdown-remark
    

    Y comprueba que los plugins realmente se ejecutan mirando el HTML construido, no el dev server:

    grep -c "lo-que-tu-plugin-inyecta" dist/client/ruta/index.html
    

    Flags experimentales que hay que quitar

    Cinco funciones salieron de experimental. Si las tenías activadas, hay que sacarlas del bloque experimental o el build se queja:

    Flag experimentalQué hacer ahora
    rustCompilerQuitar. Es el compilador por defecto y único
    queuedRenderingQuitar. Activo por defecto
    advancedRoutingQuitar. Activo por defecto
    loggerMover al campo logger de nivel superior
    cache y routeRulesMover fuera de experimental, a nivel superior

    src/fetch.ts es ahora un nombre reservado

    Con Advanced Routing, src/fetch.ts pasa a ser un archivo reservado para configurar el pipeline de peticiones. Si ya tenías un archivo con ese nombre para otra cosa, tienes tres opciones:

    // Apuntar a otro archivo
    export default defineConfig({ fetchFile: './src/router.ts' });
    
    // O desactivar Advanced Routing por completo
    export default defineConfig({ fetchFile: null });
    

    O simplemente renombrar tu archivo. Es un choque poco probable, pero cuando ocurre el síntoma es desconcertante.

    Lo que se eliminó

    @astrojs/db desaparece por completo y queda sin mantenimiento. Las alternativas son node:sqlite (integrado en Node, sin compilación nativa), Drizzle ORM, o lo que uses ya. Este sitio usa node:sqlite directamente para reseñas, contadores y leads, y la experiencia es buena: cero dependencias nativas y WAL activado a mano.

    Las APIs internas de astro:transitions ya no se exportan. Fuera TRANSITION_BEFORE_PREPARATION, TRANSITION_AFTER_PREPARATION, TRANSITION_BEFORE_SWAP, TRANSITION_AFTER_SWAP, TRANSITION_PAGE_LOAD, y las funciones isTransitionBeforePreparationEvent(), isTransitionBeforeSwapEvent() y createAnimationScope().

    Se sustituyen por los nombres de evento directos, que además siempre fueron más legibles:

    document.addEventListener('astro:before-preparation', (e) => { /* ... */ });
    document.addEventListener('astro:after-swap', (e) => { /* ... */ });
    

    getContainerRenderer() cambia de ruta de import. Ya no sale de la raíz del paquete, sino de un entrypoint dedicado:

    // Antes
    import { getContainerRenderer } from '@astrojs/react';
    
    // Ahora
    import { getContainerRenderer } from '@astrojs/react/container-renderer';
    

    Aplica igual a @astrojs/preact, @astrojs/solid-js, @astrojs/svelte, @astrojs/vue y @astrojs/mdx.

    Lo nuevo que sí vas a querer

    Superadas las trampas, hay tres cosas que justifican el upgrade por sí solas.

    Advanced Routing con src/fetch.ts. Control total del pipeline de peticiones usando el patrón estándar de handler fetch que ya usan Cloudflare Workers, Deno y Bun. Sirve para interceptar peticiones, reenviarlas a un backend, o componer Astro como middleware junto a algo como Hono.

    Caché de rutas con Astro.cache y routeRules. Una API agnóstica de plataforma con semántica HTTP e invalidación por etiquetas:

    // Declarativo, por grupo de rutas, fuera del código de la ruta
    export default defineConfig({
      routeRules: {
        '/blog/**': { cache: { maxAge: 3600, tags: ['blog'] } },
      },
    });
    

    Los proveedores de CDN para Netlify, Vercel y Cloudflare están todavía en experimental.

    Render encolado, ~2.4× más rápido. Sustituye el enfoque recursivo anterior. No hay nada que hacer: viene activado y se nota en sitios con muchos componentes anidados.

    El upgrade, paso a paso

    # 1. Rama nueva
    git checkout -b upgrade/astro-7
    
    # 2. Node 22.12 o superior (24 LTS es la apuesta sensata)
    node -v
    
    # 3. La actualización asistida de Astro
    npx @astrojs/upgrade
    
    # 4. Quita los flags experimentales que ya no existen
    #    rustCompiler, queuedRendering, advancedRouting
    #    y mueve logger, cache y routeRules a nivel superior
    
    # 5. Fija compressHTML antes de construir, para aislar problemas
    #    compressHTML: true  en astro.config.mjs
    
    # 6. Construye. Aquí salen las etiquetas sin cerrar
    npm run build
    
    # 7. Comprueba que tus plugins de markdown siguen vivos
    npm ls @astrojs/markdown-remark
    

    El paso 5 es deliberado: si actualizas y cambias el tratamiento de espacios a la vez, cuando algo se vea raro no vas a saber si fue el compilador o el whitespace. Fija true, deja el build en verde, y después decides si migras a 'jsx'.

    Un apunte sobre la salida final: si ya venías comprimiendo el HTML por tu cuenta con alguna herramienta externa, revísalo — ahora hay dos capas haciendo lo mismo con reglas distintas.

    Checklist antes de desplegar

    • Node 22.12 o superior en local, CI y producción
    • experimental sin rustCompiler, queuedRendering ni advancedRouting
    • logger, cache y routeRules movidos a nivel superior
    • compressHTML fijado explícitamente, no heredado
    • Build en verde: no quedan etiquetas ni atributos sin cerrar
    • npm ls @astrojs/markdown-remark confirma que está, si usas plugins
    • Plugins remark/rehype verificados sobre el HTML construido
    • Ningún src/fetch.ts propio con otro propósito
    • Imports de getContainerRenderer apuntando a /container-renderer
    • Sin restos de @astrojs/db ni de las constantes internas de astro:transitions
    • Páginas con anidamiento HTML dudoso comparadas antes y después

    Conclusión

    Astro 7 es un upgrade que merece la pena y que en la mayoría de proyectos se resuelve en una tarde. Lo nuevo —Vite 8, Sätteri, render encolado, Advanced Routing, caché de rutas— llega sin pedir una reescritura.

    Pero conviene entender de dónde vienen los problemas. Los dos cambios que rompen fuerte —el compilador estricto y compressHTMLno son bugs: son el framework dejando de tapar cosas que antes tapaba. El HTML mal cerrado siempre estuvo mal; simplemente ahora te enteras. Y los espacios entre elementos inline siempre fueron ambiguos; ahora hay una regla explícita.

    La única que puede escaparse a producción sin avisar es la de los espacios. Fija compressHTML: true el primer día, y ya migrarás cuando tengas tiempo de leer el diff con calma.

    Compartir

    Buscar

    Etiquetas

    Laravel PHP Tutorial Desarrollo Web IA JavaScript Buenas Prácticas Laravel 13 Seguridad Herramientas Claude SEO Expresiones Regulares Manipulación de Texto Frontend