Migré mi portfolio de Astro 4 a Astro 5 y no fue un paseo
Cuando empecé mi portfolio en febrero de 2026, lo hice con Astro 4. Era estable, documentado, y funcionaba. La migración a Astro 5 no estaba en mi roadmap — llegó cuando quise añadir un par de features y me encontré con que ciertos paquetes ya no tenían soporte para la versión 4.
Así que migré. Y no fue tan sencillo como npx @astrojs/upgrade.
Lo que cambió de verdad
Astro 5 trajo cambios en el sistema de contenido (Content Collections pasó a ser el estándar, config.ts se movió a src/content.config.ts), cambios en el sistema de tipos, y algunos componentes dejaron de funcionar como antes. Para un portfolio pequeño como el mío, el impacto fue manejable, pero no trivial.
La documentación oficial cubre los cambios, pero no el orden en que debes hacerlos. Eso es lo que voy a contar aquí.
Paso 1: No corras npx @astrojs/upgrade primero
Si corres el upgrade sin preparar el terreno, te quedas con un proyecto que no builda y no sabes por dónde empezar. Haz esto antes:
# 1. Crear una rama (o snapshot del contenedor)
git checkout -b migrate-astro-5
# 2. Leer el changelog de Astro 5
# No todo aplica a tu proyecto. Filtra por lo que realmente usas.
Mi setup usaba: Content Collections con Zod, integración con Tailwind, y cero islands de framework (React/Svelte). La migración se redujo a tres problemas concretos.
Paso 2: Content Collections — el cambio gordo
En Astro 4, la configuración de colecciones vivía en src/content/config.ts. En Astro 5, se movió a src/content.config.ts en la raíz del proyecto.
// Antes (Astro 4) — src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({ /* ... */ });
export const collections = { blog };
// Después (Astro 5) — src/content.config.ts (raíz del proyecto)
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({ /* ... */ });
export const collections = { blog };
Lo que no dice la documentación: si dejas el config.ts antiguo en src/content/, Astro 5 lo ignora y no te da error — pero tampoco carga tus esquemas. Si tienes validación Zod en producción, te quedas sin ella sin saberlo. Me pasó. La solución:
rm src/content/config.ts
Paso 3: El schema de Zod cambió
En Astro 4, los campos opcionales se definían con z.string().optional(). En Astro 5, algunos tipos internos cambiaron. Mi schema para proyectos tenía link: z.string().optional() que funcionaba bien, pero al migrar, los proyectos sin link dejaban de aparecer.
La solución real: asegurarme de que todos mis frontmatter tuvieran link: null explícito para los proyectos sin enlace, y featured: false para los que no están destacados, en lugar de confiar en defaults implícitos.
# En cada proyecto .md
link: null
featured: false
Paso 4: El build y el contenedor Docker
Mi portfolio builda dentro de un Dockerfile multi-stage. El cambio de versión de Astro no afectó al Dockerfile en sí, pero sí tuve que limpiar la caché de node_modules:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN rm -rf src/content/config.ts 2>/dev/null || true
RUN npm run build
Ese rm evita que el config.ts antiguo quede huérfano dentro del contenedor si olvidé eliminarlo en el repo.
El resultado
Build exitoso sin warnings. Los posts y proyectos se renderizan correctamente. El sistema de tipos es más estricto (Zod valida todo en build time), y eso está bien — prefiero un build fallido a un error en producción.
Si tu portfolio es como el mío (Astro + Tailwind, sin frameworks JS, Content Collections con Zod), la migración te llevará una tarde contando tiempo de build y pruebas. No es difícil, solo hay que hacerlo en orden: leer changelog, migrar config, actualizar frontmatter, buildear, desplegar.
Y sobre todo: no dejes el config.ts antiguo.