Actualicé este sitio de Astro 5 a Astro 6 por la razón más aburrida posible: cinco CVEs de Dependabot, todos resueltos con astro@6.4.6 o más nuevo. Un parche de seguridad. Esperaba un bun add astro@latest de diez minutos, un build en verde, y de regreso a escribir.
En vez de eso me llevé tres builds rotos seguidos. Y aquí está la parte que vale la pena anotar: ni uno solo era en código que yo escribí. Cada falla cayó en el theme vendorizado — el config de contenido, las utilidades del blog, el layout — archivos que vinieron con el template AstroWind y que nunca había tenido razón de tocar. Si corres un portafolio o blog sobre un theme como este y has estado posponiendo el salto a v6, este es el post que hubiera querido leer primero: los cambios exactos que rompen algo, los errores tal cual salieron, y los fixes de una línea.
Por qué un “parche de seguridad” en realidad es una migración mayor
Astro 6 es una versión mayor. El semver de versiones mayores permite romper cosas, y Astro 6 eliminó un montón de APIs que estuvieron deprecadas durante todo el ciclo 5.x. En una app desde cero eres dueño de cada archivo, así que sientes cada eliminación exactamente una vez, en tu propio código. En un sitio con theme la cuenta es distinta: la mayoría del código no es tuyo. La ruptura aparece en la plomería del theme, con mensajes de error que apuntan a archivos que no escribiste y que no conoces del todo.
Esa es toda la historia de esta migración. Tres cambios que rompen algo me pegaron, todos en código vendorizado. Aquí están en el orden en que el build los encontró.
Cambio que rompe #1: las content collections legacy desaparecieron
El primer bun run build después del bump se negó incluso a arrancar. Astro 5 introdujo la Content Layer API (el loader glob() en un archivo src/content.config.ts) pero mantuvo funcionando las “legacy” content collections en paralelo durante todo un ciclo mayor. Astro 6 eliminó la API legacy por completo — sin flag de compatibilidad, sin periodo de gracia. Si tus collections todavía dependen de la convención vieja src/content/config.ts, el build se detiene.
El fix es mecánico pero son dos movimientos, no uno.
Movimiento 1 — reubicar y modernizar el config. Renombra src/content/config.ts a src/content.config.ts (nota: fuera de la carpeta content/, un nivel arriba) y dale a cada collection un loader explícito:
// src/content.config.ts
import { z, defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
const postCollection = defineCollection({
// Astro 6 Content Layer API — reemplaza las legacy content collections eliminadas
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/post' }),
schema: z.object({
title: z.string(),
publishDate: z.date().optional(),
// ...el resto de tu schema
}),
});
export const collections = { post: postCollection };Movimiento 2 — arreglar el efecto dominó en cada archivo que lee la collection. Esta es la parte que duele en un sitio con theme, porque las utilidades del blog del theme están llenas de la API vieja. Dos renombres importan:
entry.render()→render(entry)—renderahora es un import nombrado desdeastro:content, no un método del entry.entry.slug→entry.id— los entries de la Content Layer exponenid, noslug.
En el helper de blog de AstroWind eso significó que esto:
// Astro 5 (antes)
const { Content, remarkPluginFrontmatter } = await post.render();
const slug = cleanSlug(post.slug);se convirtiera en esto:
// Astro 6 (después)
import { getCollection, render } from 'astro:content';
// ...
const { Content, remarkPluginFrontmatter } = await render(post);
const slug = cleanSlug(post.id); // Content Layer: entry.id es el slugNada de eso es difícil. Pero no lo encuentras leyendo una guía de migración — lo encuentras porque el build lanza LegacyContentConfigError, arreglas el config, y el siguiente build lanza un render is not a function más adentro, en un archivo que nunca habías abierto.
Cambio que rompe #2: la matriz de versiones de integraciones (el error que más tiempo me comió)
Con el config de contenido arreglado, el siguiente build murió con algo genuinamente críptico — y este es el que más tiempo me costó, porque el error culpa a un archivo dentro de node_modules y no dice nada sobre por qué:
[ERROR] [vite] ✗ Build failed
node_modules/@astrojs/mdx/dist/server.js (3:9): "chunkToString" is not exported by
"node_modules/astro/dist/runtime/server/index.js", imported by
"node_modules/@astrojs/mdx/dist/server.js".Esto es lo que en realidad pasó. Las integraciones que se conectan al runtime de Astro publican una versión mayor nueva por cada mayor de Astro. @astrojs/mdx@6 apunta a Astro 6; @astrojs/mdx@7 apunta a Astro 7. Cuando actualicé Astro, mi package manager resolvió felizmente @astrojs/mdx a su versión mayor más nueva — v7 — porque el rango con caret lo permitía. Pero mdx v7 mete la mano al runtime interno de Astro por un export (chunkToString) que solo existe en Astro 7. En Astro 6, ese import no resuelve a nada, y Rollup falla el build con el mensaje de arriba.
Si lo instalas manualmente sí te llevas un aviso, pero es fácil que se te pierda entre un montón de output del upgrade:
warn: incorrect peer dependency "astro@6.4.8"El fix es fijar la integración a la versión mayor que coincide con tu versión de Astro:
// package.json
"devDependencies": {
"@astrojs/mdx": "^6.0.1" // coincide con Astro 6, NO la más reciente (v7 = Astro 7)
}La regla general que te salva la próxima vez: no todas las integraciones suben de versión mayor al mismo ritmo que Astro. En este sitio con Astro 6, @astrojs/mdx es v6 — pero @astrojs/sitemap sigue en v3, @astrojs/rss en v4, y @astrojs/partytown en v2, y todas están bien. Las que se rompen con un desajuste son las integraciones acopladas al runtime — mdx, y los renderizadores de frameworks como @astrojs/react / vue / svelte — porque importan directamente de los internos de Astro. Entonces, después de un bump mayor, no actualices todo a ciegas: revisa las peerDependencies de cada integración contra tu versión de Astro (ese warning de incorrect peer dependency es la pista) y fija las acopladas al runtime a la versión mayor correcta para que un rango ^ no te arrastre a la siguiente. El error del build no te va a decir “versión de integración incorrecta”; te va a mostrar un export interno que no existe y dejarte averiguar el resto.
Aquí está el desglose en este sitio exacto, para que veas que no es todo o nada:
| Integración | Versión en este sitio Astro 6 | ¿Sigue la versión mayor de Astro? |
|---|---|---|
@astrojs/mdx | v6 | Sí — súbela junto con Astro (esta fue la culpable) |
@astrojs/react / vue / svelte | coincide con Astro | Sí — renderizadores acoplados al runtime |
@astrojs/sitemap | v3 | No — versiona de forma independiente |
@astrojs/rss | v4 | No — versiona de forma independiente |
@astrojs/partytown | v2 | No — versiona de forma independiente |
Cambio que rompe #3: ViewTransitions ahora es ClientRouter
La tercera falla fue un import que no se pudo resolver. Astro renombró el componente de view transitions de <ViewTransitions /> a <ClientRouter /> desde Astro 5 (el nombre ahora describe lo que hace — un router del lado del cliente — en vez de la característica de CSS que dispara), y deprecó el nombre viejo durante el ciclo 5.x. Astro 6 eliminó el export ViewTransitions. Si tu layout todavía lo importa, el build no lo puede resolver.
Un import y una etiqueta:
---
// src/layouts/Layout.astro
import { ClientRouter } from 'astro:transitions'; // antes: ViewTransitions
---
<head>
<!-- ... -->
<ClientRouter fallback="swap" />
<!-- antes: <ViewTransitions /> -->
</head>Trivial de arreglar — pero la documentación marca una trampa más sutil que un buscar-y-reemplazar no va a atrapar: el timing de los eventos del ciclo de vida cambió. Si tienes JavaScript enganchado a astro:page-load o astro:after-swap, no cambies el componente y asumas que ya quedó — navega un par de veces y confirma que tus scripts siguen disparando cuando esperas. Los míos estaban bien; los tuyos podrían no estarlo.
Los que no me pegaron a mí — pero podrían pegarte a ti
Tres cambios que rompen algo golpearon mi setup. Astro 6 eliminó más que eso, y cuáles te pegan depende de lo que haga tu theme. Antes de actualizar, revisa esta lista:
Astro.glob()fue eliminado. Usaimport.meta.glob()en su lugar. Yo no lo usaba — pero bastantes themes sí, para construir navegación o páginas de listado, así que busca esa referencia primero.- Se requiere Node 22+ (Node 18 y 20 quedan fuera). Aquí hay uno traicionero: mi
package.jsontodavía declaraba"engines": { "node": "^18.17.1 || ^20.3.0 || >= 21.0.0" }. Ese campo es solo informativo, así que mi build local no se quejó — pero un runner de CI o un proyecto en Vercel/Netlify todavía fijado a Node 18 fallaría en el build o, peor, en runtime. Actualizaenginesy la versión de Node de tu plataforma de deploy juntos. - Zod 4. Astro 6 trae Zod 4. Si tus schemas de contenido dependen de comportamiento exclusivo de Zod 3, se van a romper. Schemas simples tipo
z.object({...})como el mío pasaron sin cambios. emitESMImage()fue eliminado. Este es para autores de integraciones, no para código de app — pero si una integración de imágenes poco común en tu stack no se ha actualizado, ahí es donde va a aparecer.
La lección real: en un sitio con theme, el build es tu guía de migración — y apunta a código que no escribiste
La guía oficial de upgrade a v6 de Astro es genuinamente buena, y deberías leerla primero. Pero está escrita para alguien que es dueño de su código base — “actualiza tu config de contenido”, “actualiza tu layout”, “actualiza tus imports”. En un sitio basado en theme, tú no escribiste el config de contenido, ni el layout, ni la mitad de los imports. La guía de upgrade describe cambios en código que nunca has leído.
Así que el playbook práctico que de verdad funcionó:
- Lee la guía oficial de upgrade para saber qué viene — quieres tener la forma de los cambios en la cabeza antes de que empiecen los errores.
- Fija cada integración
@astrojs/*a tu mayor de Astro antes de reconstruir, para que el cambio #2 nunca te pase a ti. - Corre el build y deja que te guíe. Cada falla nombra un archivo y un síntoma; arréglalo, reconstruye, repite. En un theme maduro son de tres a cinco fixes mecánicos, no una reescritura. Y una vez que esté en verde, no te quedes en “el build pasó” — valida el output del post antes de publicarlo, porque una migración puede compilar limpio y aun así cambiar cómo se renderiza una página.
- Presupuesta tiempo para el código vendorizado. Los fixes son pequeños, pero están en archivos que no te sabes de memoria, así que cada uno te cuesta un minuto de “espera, ¿dónde está esto y quién lo llama?” que una migración desde cero no tendría.
De principio a fin esto tomó como treinta minutos, y más de la mitad se me fue persiguiendo la pista falsa de chunkToString en el cambio #2 antes de entender que era solo un desajuste de versiones. Los CVEs de seguridad eran reales y valía la pena cerrarlos por su cuenta — así que si estás sentado sobre Astro 5, haz el bump. Solo no lo agendes como un trámite de diez minutos, y no te sorprendas cuando los stack traces apunten a un lugar en el que nunca habías estado.
Si te gustó la forma de esto — una costura pequeña y filosa escondida dentro de un theme de Astro — es exactamente la misma clase de bug que la trampa de Partytown que mató en silencio mi Google Analytics: un default en código vendorizado haciendo algo que nunca pediste. Y si quieres el recorrido completo de cómo encajan las capas de config de un theme AstroWind (la misma capa que convierte un bump de versión en una búsqueda del tesoro), ese es mi análisis a fondo del template AstroWind.