Migrando al Content Layer API de Astro 5: Lo que Aprendí
Astro 5 trajo un cambio importante: el Content Layer API reemplaza la vieja forma de manejar Content Collections. Acabo de migrar este mismo portafolio y quiero compartir lo que aprendí — incluyendo los errores que me encontré.
🤔 ¿Por qué migrar?
El viejo sistema (type: 'content') sigue funcionando, pero está en modo legacy. El nuevo Content Layer API ofrece:
- Más flexibilidad — puedes cargar contenido desde cualquier fuente, no solo archivos locales
- Mejor rendimiento — build incremental más eficiente
- API unificada —
glob(),file(), o loaders personalizados - Futuro-proof — las nuevas funciones de Astro se construyen sobre esta API
Si tu proyecto usa Astro 5 y todavía tienes type: 'content' en tu config, es momento de migrar.
📋 Paso 1: Mover el archivo de configuración
El primer cambio es la ubicación del archivo. Ya no vive dentro de src/content/:
❌ src/content/config.ts (legacy)
✅ src/content.config.ts (Content Layer API)Sí, sale de la carpeta content/ y se coloca directamente en src/.
📋 Paso 2: Usar el glob() loader
El cambio más visible es reemplazar type: 'content' con un loader:
import { defineCollection, z } from 'astro:content';
+import { glob } from 'astro/loaders';
const blogCollection = defineCollection({
- type: 'content',
+ loader: glob({
+ pattern: '**/[^_]*.{md,mdx}',
+ base: './src/content/blog',
+ }),
schema: z.object({
title: z.string(),
description: z.string(),
date: z.coerce.date(),
}),
});El pattern define qué archivos cargar (archivos .md y .mdx que no empiecen con _). El base es la carpeta donde están tus archivos.
Tip: Tus archivos Markdown no necesitan moverse. Solo cambia cómo Astro los encuentra.
⚠️ Paso 3: Arreglar los IDs (el error que nadie te avisa)
Este fue mi primer problema real. Después de migrar, mis URLs se veían así:
❌ /proyectos/mi-sitio-web.md
✅ /proyectos/mi-sitio-web¿Por qué? El glob() loader genera IDs que incluyen la extensión del archivo. En la API legacy, el ID era solo el nombre sin extensión.
La solución es usar generateId para limpiar los IDs:
const stripExtension = ({ entry }: { entry: string }) =>
entry.replace(/\.mdx?$/, '');
const blogCollection = defineCollection({
loader: glob({
pattern: '**/[^_]*.{md,mdx}',
base: './src/content/blog',
generateId: stripExtension, // ← esto arregla las URLs
}),
schema: z.object({ ... }),
});Sin esta línea, tus rutas dinámicas van a generar URLs con .md al final. No rompe el build, pero sí rompe los enlaces internos del sitio.
📋 Paso 4: Cambiar slug por id
En la API legacy, las entries tenían una propiedad slug. En el Content Layer API, eso ya no existe. Se usa id:
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
- params: { slug: post.slug },
+ params: { slug: post.id },
props: { post },
}));
}Esto aplica en todos los lugares donde uses .slug — páginas de listado, detail pages, links internos, etc.
📋 Paso 5: Nuevo render()
Las entries ya no tienen un método .render() propio. Ahora se importa la función render() desde astro:content:
-import { getCollection } from 'astro:content';
+import { getCollection, render } from 'astro:content';
const post = Astro.props.post;
-const { Content } = await post.render();
+const { Content } = await render(post);Es un cambio pequeño pero necesario en todas las páginas que renderizan contenido Markdown.
🧹 Paso 6: Limpiar el caché
Después de todos los cambios, limpia la carpeta .astro/ para que Astro regenere el content store:
rm -rf .astro
pnpm buildSi no limpias el caché, puedes ver errores confusos donde los IDs viejos colisionan con los nuevos.
✅ Checklist de migración
| # | Tarea | Archivo(s) |
|---|---|---|
| 1 | Mover config | src/content/config.ts → src/content.config.ts |
| 2 | Agregar glob() loader | src/content.config.ts |
| 3 | Agregar generateId | src/content.config.ts |
| 4 | Cambiar slug → id | Todas las páginas con getStaticPaths |
| 5 | Importar render() | Páginas de detalle (blog, proyectos) |
| 6 | Limpiar .astro/ | Terminal |
| 7 | Verificar | pnpm run check && pnpm build |
💡 Lo que me sorprendió
El build pasa aunque las URLs estén mal. El
.mden las URLs no genera error de build — solo produce rutas que nadie va a visitar. Es silencioso.La API de schemas no cambia. El
schema: ({ image }) => z.object({ ... })funciona exactamente igual. No necesitas tocar tus schemas.getCollection()funciona igual. La forma de consultar colecciones no cambia. Solo cambian las propiedades de los objetos que devuelve.Los archivos de contenido no se mueven. Tus
.mdse quedan donde están. Solo cambia la config que le dice a Astro dónde encontrarlos.
🔗 Referencias
¿Tienes problemas con la migración? Escríbeme y te ayudo.