{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections","title":"Content Layer API, Server Islands y View Transitions en Astro 5","description":"Análisis del content layer API de Astro 5, la arquitectura de server islands y view transitions, con benchmarks de producción comparando el rendimiento con Next.js para sitios de contenido.","language":"es","datePublished":"2026-02-21","dateModified":null,"contentFormat":"text/markdown","content":"Frontend\n\n# Content Layer API, Server Islands y View Transitions en Astro 5\n\n2026-02-2110 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/astro-5-view-transitions-content-collections>)\n\n## El Content Layer API: Pipeline de Datos Basado en Loaders\n\nLas content collections de [Astro](<https://astro.build/>) 4 funcionaban adecuadamente para casos básicos. Los archivos Markdown se colocaban en ` src/content/ `, se definía un schema en ` config.ts `, y Astro se encargaba del resto. La limitación era que \"el resto\" resultaba opaco. Las fuentes personalizadas requerían esfuerzo significativo. El contenido remoto dependía de soluciones provisionales. Las fuentes mixtas no estaban bien soportadas.\n\nEl content layer API de Astro 5 repiensa esto completamente. En vez de un sistema rígido basado en directorios, se define **loaders**, funciones que pueden traer contenido de cualquier lado:\n\n```typescript\n// src/content.config.ts\nimport { defineCollection, z } from 'astro:content';\nimport { glob } from 'astro/loaders';\nimport { createNotionLoader } from './loaders/notion';\n\nconst blog = defineCollection({\n loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),\n schema: z.object({\n title: z.string(),\n date: z.coerce.date(),\n draft: z.boolean().default(false),\n tags: z.array(z.string()).default([]),\n }),\n});\n\nconst changelog = defineCollection({\n loader: createNotionLoader({\n databaseId: process.env.NOTION_DB_ID,\n filter: { property: 'Status', select: { equals: 'Published' } },\n }),\n schema: z.object({\n title: z.string(),\n version: z.string(),\n body: z.string(),\n }),\n});\n\nexport const collections = { blog, changelog };\n```\n\nEl loader ` glob ` reemplaza el viejo escaneo implícito de archivos. Pero el poder real está en escribir loaders custom. Acá va un ejemplo de loader de Notion para un changelog:\n\n```typescript\n// src/loaders/notion.ts\nimport { Client } from '@notionhq/client';\n\nexport function createNotionLoader(opts) {\n const notion = new Client({ auth: process.env.NOTION_TOKEN });\n\n return {\n name: 'notion-loader',\n async load({ store, logger }) {\n logger.info('Fetching from Notion...');\n const { results } = await notion.databases.query({\n database_id: opts.databaseId,\n filter: opts.filter,\n });\n\n store.clear();\n\n for (const page of results) {\n const blocks = await notion.blocks.children.list({\n block_id: page.id,\n });\n\n const body = blocksToMarkdown(blocks.results);\n const props = page.properties;\n\n store.set({\n id: page.id,\n data: {\n title: props.Name.title[0]?.plain_text ?? '',\n version: props.Version.rich_text[0]?.plain_text ?? '',\n body,\n },\n });\n }\n },\n };\n}\n```\n\nEl objeto ` store ` es la abstracción clave. Es un store de contenido persistente que Astro maneja entre builds. En modo dev, la función ` load() ` del loader se llama cuando hay cambios de archivos (para loaders basados en archivos) o al reiniciar el servidor (para loaders remotos). Durante el build, corre una vez y cachea. Esto significa que los builds incrementales realmente funcionan, si tu contenido de Notion no cambió, Astro puede saltear el fetch completamente usando el mecanismo de digest del store.\n\nAl profilar esto en un sitio de documentación con 2.400 páginas que tira de archivos Markdown y un headless CMS, los tiempos de build bajan de 47 segundos (Astro 4, refetcheando todo) a 12 segundos con cache hit. No es un error de tipeo.\n\n## Server Islands: El Endgame de la Hidratación Parcial\n\nAstro viene haciendo hidratación parcial desde la v1 con ` client:load `, ` client:idle `, ` client:visible `. Eso era sobre **cuándo** hidratar un componente. Los server islands son sobre **dónde** correrlo.\n\nEl concepto: tu página es un shell estático renderizado en build time (o en el edge), con agujeros recortados para contenido dinámico que se llena con HTML renderizado en el servidor al momento del request. No se necesita JavaScript del lado del cliente para esas partes dinámicas.\n\n```astro\n---\n// src/pages/product/[slug].astro\nimport ProductDetails from '../components/ProductDetails.astro';\nimport PricingPanel from '../components/PricingPanel.astro';\nimport ReviewSummary from '../components/ReviewSummary.astro';\n---\n\n<ProductDetails product={product} />\n\n<!-- Esto se renderiza en el servidor al momento del request -->\n<PricingPanel server:defer productId={product.id}>\n <div slot=\"fallback\" class=\"skeleton-pricing\">\n <div class=\"skeleton-line w-32 h-8\"></div>\n <div class=\"skeleton-line w-24 h-6\"></div>\n </div>\n</PricingPanel>\n\n<ReviewSummary server:defer productId={product.id}>\n <p slot=\"fallback\">Cargando reviews...</p>\n</ReviewSummary>\n```\n\nCuando la página carga, el shell estático aparece instantáneamente desde la CDN. Después el browser hace requests de fetch paralelos para cada island con ` server:defer `. El servidor renderiza el componente a HTML y lo manda de vuelta. El browser swapea el contenido fallback por el contenido real. Sin hidratación. Sin runtime de framework JavaScript. Solo reemplazo de HTML.\n\nConsideremos un catálogo de e-commerce con 15.000 SKUs. Las páginas de detalle de producto son completamente estáticas (imágenes, descripciones, specs) mientras que pricing e inventario son server islands que pegan contra la base de datos en vivo. La arquitectura se ve así:\n\n```\nCDN (shell estático) ──► Browser ──► Edge Function (island de pricing)\n ├──► Edge Function (island de inventario)\n └──► Edge Function (island de reviews)\n```\n\nLos resultados fueron contundentes. TTFB bajó a 28ms para el shell estático (CDN hit). Los islands dinámicos resolvían en 80-120ms. LCP total estaba debajo de 200ms para el contenido estático, con el pricing apareciendo alrededor de 300ms. Antes, con full SSR en cada request, el TTFB andaba entre 400-600ms dependiendo de la carga de la base de datos.\n\nHay una sutileza que la documentación no enfatiza lo suficiente: los server islands pueden tener **políticas de caché independientes**. El island de pricing puede ser cache-busted en cada request mientras el resumen de reviews puede cachearse por 5 minutos:\n\n```typescript\n// En el server endpoint de tu island\nexport const config = {\n cache: {\n pricing: 'no-store',\n reviews: 'public, max-age=300, stale-while-revalidate=60',\n },\n};\n```\n\nEsta granularidad es algo que no es posible lograr fácilmente con full-page SSR o ISR.\n\n## View Transitions: Persistencia de Estado y Navegación Nativa\n\nLas view transitions en Astro 5 están construidas sobre la API nativa de View Transitions (con fallback para Firefox/Safari), y cambian fundamentalmente cómo se sienten las MPAs. Un sitio estático navega como una SPA sin mandar un router del lado del cliente.\n\n```astro\n---\n// src/layouts/Base.astro\nimport { ViewTransitions } from 'astro:transitions';\n---\n<html>\n <head>\n <ViewTransitions />\n </head>\n <body>\n <nav transition:persist=\"main-nav\">\n <!-- La navegación persiste entre transiciones de página -->\n </nav>\n <main transition:animate=\"slide\">\n <slot />\n </main>\n </body>\n</html>\n```\n\nLa directiva ` transition:persist ` es donde la cosa se pone genuinamente útil. Consideremos un componente de reproductor de música en un portfolio que necesita seguir tocando entre navegaciones. En una MPA tradicional, cada navegación mata el elemento de audio. Con ` transition:persist `, el nodo DOM del componente sobrevive la transición:\n\n```astro\n<AudioPlayer\n transition:persist=\"audio-player\"\n client:load\n currentTrack={track}\n/>\n```\n\nEl reproductor sigue tocando. El contexto de audio no se interrumpe. La animación del visualizador continúa. Simplemente funciona. Esto antes solo era posible con una arquitectura SPA, lo que significaba mandar un router, un runtime de framework, y manejar estado del lado del cliente.\n\nLas mediciones de rendimiento de navegación en un sitio de documentación de 200 páginas muestran la diferencia claramente. Navegación MPA tradicional (carga completa de página): 180-350ms dependiendo del peso de la página. Con view transitions: 60-90ms de tiempo de navegación percibido, porque Astro prefetchea la siguiente página en hover y transforma el DOM.\n\n## Renderizado Híbrido: Selección de Modo por Ruta\n\nAstro 5 permite mezclar modos de renderizado a nivel de ruta. Las páginas de marketing pueden ser completamente estáticas, el dashboard puede utilizar server rendering, y los endpoints de API pueden ejecutarse en el edge:\n\n```typescript\n// astro.config.mjs\nimport { defineConfig } from 'astro/config';\nimport vercel from '@astrojs/vercel';\n\nexport default defineConfig({\n output: 'static', // default: static\n adapter: vercel({\n imageService: true,\n }),\n});\n```\n\nDespués por página:\n\n```astro\n---\n// src/pages/dashboard.astro\n// Esta página opta por server rendering\nexport const prerender = false;\n\nconst session = await getSession(Astro.cookies);\nif (!session) return Astro.redirect('/login');\n---\n```\n\nTodo es estático por defecto. Optás rutas individuales a SSR con ` export const prerender = false `. Esto es lo inverso del approach de Next.js donde se está en SSR-land por defecto y optás por generación estática.\n\n## Comparación de Rendimiento con Next.js\n\nLos siguientes datos provienen de sitios de contenido comparables en producción:\n\n| Métrica | Astro 5 (sitio docs, 800 páginas) | Next.js 15 (sitio docs, 650 páginas) |\n| --- | --- | --- |\n| Tiempo de build | 18s | 94s |\n| JS mandado (homepage) | 12KB | 87KB |\n| TTFB (CDN) | 22ms | 31ms |\n| LCP (mobile 3G) | 1.2s | 2.1s |\n| TTI (mobile 3G) | 1.4s | 3.8s |\n\nLa diferencia en tiempo de build es en gran parte porque Astro no necesita bundlear un runtime de React del lado del cliente para cada página. La diferencia de JS es la grande, 12KB vs 87KB importa enormemente en redes limitadas.\n\nPero esta comparación es injusta si tu sitio es muy interactivo. El sitio de Next.js que estoy comparando tenía búsqueda interactiva, filtrado del lado del cliente, y un widget de feedback en cada página. Astro puede hacer todo eso (con islands de React), pero una vez que teners 5+ componentes interactivos por página, la arquitectura de islands empieza a perder su ventaja porque se está mandando React de todas formas.\n\nUna regla práctica: si más del 30% de la superficie de la página es interactiva, quedarse con Next.js. Debajo de ese umbral, la arquitectura de islands de Astro te da rendimiento mediblemente mejor.\n\n## Migración desde Next.js: Problemas Comunes\n\nMigrar un blog de Next.js 14 (340 posts, MDX, componentes custom) a Astro 5 revela varias trampas comunes:\n\n**Imports de componentes MDX**: Next.js auto-importa componentes vía ` mdx-components.tsx `. Astro necesita imports explícitos en cada archivo MDX o un plugin de remark para inyectarlos. Un plugin de remark resuelve esto:\n\n```javascript\n// remark-auto-import.mjs\nexport function remarkAutoImport() {\n return (tree) => {\n const imports = [\n \"import CodeBlock from '../../components/CodeBlock.astro';\",\n \"import Callout from '../../components/Callout.astro';\",\n ];\n\n tree.children.unshift({\n type: 'mdxjsEsm',\n value: imports.join('\\n'),\n data: {\n estree: {\n type: 'Program',\n body: imports.map((imp) => ({\n type: 'ImportDeclaration',\n specifiers: [/* parseado del string de import */],\n source: { type: 'Literal', value: imp.match(/from '(.+)'/)[1] },\n })),\n sourceType: 'module',\n },\n },\n });\n };\n}\n```\n\n**Optimización de imágenes**: El componente ` <Image> ` de Next.js no traduce 1:1. El ` <Image> ` de Astro desde ` astro:assets ` maneja bien imágenes locales pero las imágenes remotas necesitan configuración explícita de ` domains ` en ` astro.config.mjs `. Sitios con 200+ posts con imágenes remotas necesitan el allowlist de domains poblado.\n\n**Feed RSS**: Next.js no tiene RSS built-in. El paquete ` @astrojs/rss ` de Astro es excelente pero la migración del content layer significó que los scripts de generación de RSS necesitan reescritura para usar la nueva API de ` getCollection() `.\n\nEl tiempo de build pasó de 94 segundos a 14 segundos. El tamaño del bundle por página bajó de ~90KB a ~8KB (solo las páginas con componentes interactivos mandan JS). Los Core Web Vitals mejoraron en todos los frentes, la mediana de LCP bajó de 1.8s a 0.9s en mobile.\n\n## Lo Que Todavía Falta\n\nAstro 5 no es perfecto. El HMR del dev server es más lento que el de Next.js para sitios grandes (500+ páginas). El ecosistema de componentes pre-armados es más chico. Si se necesita patrones pesados de middleware (auth, A/B testing, feature flags en cada request), el modelo de server islands es menos ergonómico que el middleware de Next.js.\n\nLa experiencia con TypeScript también está levemente atrás. La inferencia de tipos de Next.js para ` getStaticProps ` y server components es más ajustada. El soporte de TypeScript de Astro mejoró masivamente, pero ocasionalmente se va a encontrar tipos ` any ` en el content layer API que Next.js atraparía.\n\nAun así, para sitios de contenido, sitios genuinamente orientados al contenido donde el trabajo principal es renderizar texto, imágenes y datos estructurados, Astro 5 se destaca como la herramienta más fuerte en la categoría. El content layer API es el pipeline de datos que los sitios de contenido siempre necesitaron, los server islands resuelven el problema de contenido-dinámico-en-páginas-estáticas de forma elegante, y las view transitions hacen que todo se sienta como una SPA moderna sin el impuesto de JavaScript.\n\nConstruir una página con el nuevo content layer, medir los tiempos de build y verificar los scores de Lighthouse proporciona datos suficientes para una decisión informada.\n\n## Preguntas frecuentes\n\n### Astro 5 es un reemplazo directo de Astro 4?\n\nNo del todo. El content layer API reemplaza las viejas content collections con un sistema basado en loaders. Si se está usando defineCollection con type: 'content', se va a necesitar migrar al nuevo patrón de loaders. La herramienta de migración del CLI maneja alrededor del 80% de los casos, pero plugins custom de remark/rehype pueden necesitar ajustes manuales.\n\n### Astro 5 puede reemplazar a Next.js para todos los casos?\n\nNo. Astro se destaca en sitios orientados a contenido donde la mayoría de las páginas son estáticas o levemente dinámicas. Si se necesita interactividad pesada del lado del cliente, flujos de auth complejos, o features en tiempo real en cada página, Next.js con React Server Components sigue siendo mejor opción. El punto óptimo de Astro son blogs, docs, sitios de marketing y portfolios con interactividad selectiva.\n\n### Cómo afectan los server islands a las estrategias de caché?\n\nCada server island se fetchea independientemente, así que es posible setear diferentes políticas de caché por island. Los shells estáticos obtienen full caché de CDN mientras los islands dinámicos lo bypasean. Utilizar headers de Cache-Control en los endpoints de tus islands y considerá stale-while-revalidate para islands que toleren data levemente desactualizada.\n\n## Tips\n\n1. **Tip:** Cuando migrás de content collections de Astro 4, no intentes reescribir tu ` config.ts ` todo de una. Utilizar ` defineCollection() ` con la nueva propiedad ` loader ` en una sola colección primero, verificá el build, y después continuar con el resto. El approach viejo basado en glob sigue funcionando durante la transición.\n2. **Tip:** Los server islands con ` server:defer ` son tentadores para meter en todos lados, pero cada uno agrega un waterfall de red en el cliente. Profileá tu uso real con ` performance.mark() ` antes y después, más de 3 islands diferidos por página tiende a empezar a afectar el LCP en conexiones 3G.\n3. **Tip:** Para view transitions, siempre colocar ` transition:persist ` en tus elementos de audio/video y en cualquier componente que mantenga conexiones WebSocket. Sin esto, los badges de notificaciones en vivo se resetean en cada navegación porque el DOM se destruye en cada transición.\n\n- astro\n- view-transitions\n- content-collections\n- server-islands\n- hybrid-rendering\n- nextjs\n- frontend-architecture\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Content Layer API, Server Islands y View Transitions en Astro 5","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#title"},{"level":2,"text":"El Content Layer API: Pipeline de Datos Basado en Loaders","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#el-content-layer-api-pipeline-de-datos-basado-en-loaders"},{"level":2,"text":"Server Islands: El Endgame de la Hidratación Parcial","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#server-islands-el-endgame-de-la-hidratacin-parcial"},{"level":2,"text":"View Transitions: Persistencia de Estado y Navegación Nativa","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#view-transitions-persistencia-de-estado-y-navegacin-nativa"},{"level":2,"text":"Renderizado Híbrido: Selección de Modo por Ruta","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#renderizado-hbrido-seleccin-de-modo-por-ruta"},{"level":2,"text":"Comparación de Rendimiento con Next.js","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#comparacin-de-rendimiento-con-nextjs"},{"level":2,"text":"Migración desde Next.js: Problemas Comunes","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#migracin-desde-nextjs-problemas-comunes"},{"level":2,"text":"Lo Que Todavía Falta","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#lo-que-todava-falta"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#questions"},{"level":3,"text":"Astro 5 es un reemplazo directo de Astro 4?","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#question-1"},{"level":3,"text":"Astro 5 puede reemplazar a Next.js para todos los casos?","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#question-2"},{"level":3,"text":"Cómo afectan los server islands a las estrategias de caché?","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#article-tips"}],"sections":[{"heading":"Content Layer API, Server Islands y View Transitions en Astro 5","level":1,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections","content":"Frontend"},{"heading":"Content Layer API, Server Islands y View Transitions en Astro 5","level":1,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#title","content":"2026-02-2110 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/astro-5-view-transitions-content-collections>)"},{"heading":"El Content Layer API: Pipeline de Datos Basado en Loaders","level":2,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#el-content-layer-api-pipeline-de-datos-basado-en-loaders","content":"Las content collections de [Astro](<https://astro.build/>) 4 funcionaban adecuadamente para casos básicos. Los archivos Markdown se colocaban en ` src/content/ `, se definía un schema en ` config.ts `, y Astro se encargaba del resto. La limitación era que \"el resto\" resultaba opaco. Las fuentes personalizadas requerían esfuerzo significativo. El contenido remoto dependía de soluciones provisionales. Las fuentes mixtas no estaban bien soportadas.\n\nEl content layer API de Astro 5 repiensa esto completamente. En vez de un sistema rígido basado en directorios, se define **loaders**, funciones que pueden traer contenido de cualquier lado:\n\n```typescript\n// src/content.config.ts\nimport { defineCollection, z } from 'astro:content';\nimport { glob } from 'astro/loaders';\nimport { createNotionLoader } from './loaders/notion';\n\nconst blog = defineCollection({\n loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),\n schema: z.object({\n title: z.string(),\n date: z.coerce.date(),\n draft: z.boolean().default(false),\n tags: z.array(z.string()).default([]),\n }),\n});\n\nconst changelog = defineCollection({\n loader: createNotionLoader({\n databaseId: process.env.NOTION_DB_ID,\n filter: { property: 'Status', select: { equals: 'Published' } },\n }),\n schema: z.object({\n title: z.string(),\n version: z.string(),\n body: z.string(),\n }),\n});\n\nexport const collections = { blog, changelog };\n```\n\nEl loader ` glob ` reemplaza el viejo escaneo implícito de archivos. Pero el poder real está en escribir loaders custom. Acá va un ejemplo de loader de Notion para un changelog:\n\n```typescript\n// src/loaders/notion.ts\nimport { Client } from '@notionhq/client';\n\nexport function createNotionLoader(opts) {\n const notion = new Client({ auth: process.env.NOTION_TOKEN });\n\n return {\n name: 'notion-loader',\n async load({ store, logger }) {\n logger.info('Fetching from Notion...');\n const { results } = await notion.databases.query({\n database_id: opts.databaseId,\n filter: opts.filter,\n });\n\n store.clear();\n\n for (const page of results) {\n const blocks = await notion.blocks.children.list({\n block_id: page.id,\n });\n\n const body = blocksToMarkdown(blocks.results);\n const props = page.properties;\n\n store.set({\n id: page.id,\n data: {\n title: props.Name.title[0]?.plain_text ?? '',\n version: props.Version.rich_text[0]?.plain_text ?? '',\n body,\n },\n });\n }\n },\n };\n}\n```\n\nEl objeto ` store ` es la abstracción clave. Es un store de contenido persistente que Astro maneja entre builds. En modo dev, la función ` load() ` del loader se llama cuando hay cambios de archivos (para loaders basados en archivos) o al reiniciar el servidor (para loaders remotos). Durante el build, corre una vez y cachea. Esto significa que los builds incrementales realmente funcionan, si tu contenido de Notion no cambió, Astro puede saltear el fetch completamente usando el mecanismo de digest del store.\n\nAl profilar esto en un sitio de documentación con 2.400 páginas que tira de archivos Markdown y un headless CMS, los tiempos de build bajan de 47 segundos (Astro 4, refetcheando todo) a 12 segundos con cache hit. No es un error de tipeo."},{"heading":"Server Islands: El Endgame de la Hidratación Parcial","level":2,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#server-islands-el-endgame-de-la-hidratacin-parcial","content":"Astro viene haciendo hidratación parcial desde la v1 con ` client:load `, ` client:idle `, ` client:visible `. Eso era sobre **cuándo** hidratar un componente. Los server islands son sobre **dónde** correrlo.\n\nEl concepto: tu página es un shell estático renderizado en build time (o en el edge), con agujeros recortados para contenido dinámico que se llena con HTML renderizado en el servidor al momento del request. No se necesita JavaScript del lado del cliente para esas partes dinámicas.\n\n```astro\n---\n// src/pages/product/[slug].astro\nimport ProductDetails from '../components/ProductDetails.astro';\nimport PricingPanel from '../components/PricingPanel.astro';\nimport ReviewSummary from '../components/ReviewSummary.astro';\n---\n\n<ProductDetails product={product} />\n\n<!-- Esto se renderiza en el servidor al momento del request -->\n<PricingPanel server:defer productId={product.id}>\n <div slot=\"fallback\" class=\"skeleton-pricing\">\n <div class=\"skeleton-line w-32 h-8\"></div>\n <div class=\"skeleton-line w-24 h-6\"></div>\n </div>\n</PricingPanel>\n\n<ReviewSummary server:defer productId={product.id}>\n <p slot=\"fallback\">Cargando reviews...</p>\n</ReviewSummary>\n```\n\nCuando la página carga, el shell estático aparece instantáneamente desde la CDN. Después el browser hace requests de fetch paralelos para cada island con ` server:defer `. El servidor renderiza el componente a HTML y lo manda de vuelta. El browser swapea el contenido fallback por el contenido real. Sin hidratación. Sin runtime de framework JavaScript. Solo reemplazo de HTML.\n\nConsideremos un catálogo de e-commerce con 15.000 SKUs. Las páginas de detalle de producto son completamente estáticas (imágenes, descripciones, specs) mientras que pricing e inventario son server islands que pegan contra la base de datos en vivo. La arquitectura se ve así:\n\n```\nCDN (shell estático) ──► Browser ──► Edge Function (island de pricing)\n ├──► Edge Function (island de inventario)\n └──► Edge Function (island de reviews)\n```\n\nLos resultados fueron contundentes. TTFB bajó a 28ms para el shell estático (CDN hit). Los islands dinámicos resolvían en 80-120ms. LCP total estaba debajo de 200ms para el contenido estático, con el pricing apareciendo alrededor de 300ms. Antes, con full SSR en cada request, el TTFB andaba entre 400-600ms dependiendo de la carga de la base de datos.\n\nHay una sutileza que la documentación no enfatiza lo suficiente: los server islands pueden tener **políticas de caché independientes**. El island de pricing puede ser cache-busted en cada request mientras el resumen de reviews puede cachearse por 5 minutos:\n\n```typescript\n// En el server endpoint de tu island\nexport const config = {\n cache: {\n pricing: 'no-store',\n reviews: 'public, max-age=300, stale-while-revalidate=60',\n },\n};\n```\n\nEsta granularidad es algo que no es posible lograr fácilmente con full-page SSR o ISR."},{"heading":"View Transitions: Persistencia de Estado y Navegación Nativa","level":2,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#view-transitions-persistencia-de-estado-y-navegacin-nativa","content":"Las view transitions en Astro 5 están construidas sobre la API nativa de View Transitions (con fallback para Firefox/Safari), y cambian fundamentalmente cómo se sienten las MPAs. Un sitio estático navega como una SPA sin mandar un router del lado del cliente.\n\n```astro\n---\n// src/layouts/Base.astro\nimport { ViewTransitions } from 'astro:transitions';\n---\n<html>\n <head>\n <ViewTransitions />\n </head>\n <body>\n <nav transition:persist=\"main-nav\">\n <!-- La navegación persiste entre transiciones de página -->\n </nav>\n <main transition:animate=\"slide\">\n <slot />\n </main>\n </body>\n</html>\n```\n\nLa directiva ` transition:persist ` es donde la cosa se pone genuinamente útil. Consideremos un componente de reproductor de música en un portfolio que necesita seguir tocando entre navegaciones. En una MPA tradicional, cada navegación mata el elemento de audio. Con ` transition:persist `, el nodo DOM del componente sobrevive la transición:\n\n```astro\n<AudioPlayer\n transition:persist=\"audio-player\"\n client:load\n currentTrack={track}\n/>\n```\n\nEl reproductor sigue tocando. El contexto de audio no se interrumpe. La animación del visualizador continúa. Simplemente funciona. Esto antes solo era posible con una arquitectura SPA, lo que significaba mandar un router, un runtime de framework, y manejar estado del lado del cliente.\n\nLas mediciones de rendimiento de navegación en un sitio de documentación de 200 páginas muestran la diferencia claramente. Navegación MPA tradicional (carga completa de página): 180-350ms dependiendo del peso de la página. Con view transitions: 60-90ms de tiempo de navegación percibido, porque Astro prefetchea la siguiente página en hover y transforma el DOM."},{"heading":"Renderizado Híbrido: Selección de Modo por Ruta","level":2,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#renderizado-hbrido-seleccin-de-modo-por-ruta","content":"Astro 5 permite mezclar modos de renderizado a nivel de ruta. Las páginas de marketing pueden ser completamente estáticas, el dashboard puede utilizar server rendering, y los endpoints de API pueden ejecutarse en el edge:\n\n```typescript\n// astro.config.mjs\nimport { defineConfig } from 'astro/config';\nimport vercel from '@astrojs/vercel';\n\nexport default defineConfig({\n output: 'static', // default: static\n adapter: vercel({\n imageService: true,\n }),\n});\n```\n\nDespués por página:\n\n```astro\n---\n// src/pages/dashboard.astro\n// Esta página opta por server rendering\nexport const prerender = false;\n\nconst session = await getSession(Astro.cookies);\nif (!session) return Astro.redirect('/login');\n---\n```\n\nTodo es estático por defecto. Optás rutas individuales a SSR con ` export const prerender = false `. Esto es lo inverso del approach de Next.js donde se está en SSR-land por defecto y optás por generación estática."},{"heading":"Comparación de Rendimiento con Next.js","level":2,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#comparacin-de-rendimiento-con-nextjs","content":"Los siguientes datos provienen de sitios de contenido comparables en producción:\n\n| Métrica | Astro 5 (sitio docs, 800 páginas) | Next.js 15 (sitio docs, 650 páginas) |\n| --- | --- | --- |\n| Tiempo de build | 18s | 94s |\n| JS mandado (homepage) | 12KB | 87KB |\n| TTFB (CDN) | 22ms | 31ms |\n| LCP (mobile 3G) | 1.2s | 2.1s |\n| TTI (mobile 3G) | 1.4s | 3.8s |\n\nLa diferencia en tiempo de build es en gran parte porque Astro no necesita bundlear un runtime de React del lado del cliente para cada página. La diferencia de JS es la grande, 12KB vs 87KB importa enormemente en redes limitadas.\n\nPero esta comparación es injusta si tu sitio es muy interactivo. El sitio de Next.js que estoy comparando tenía búsqueda interactiva, filtrado del lado del cliente, y un widget de feedback en cada página. Astro puede hacer todo eso (con islands de React), pero una vez que teners 5+ componentes interactivos por página, la arquitectura de islands empieza a perder su ventaja porque se está mandando React de todas formas.\n\nUna regla práctica: si más del 30% de la superficie de la página es interactiva, quedarse con Next.js. Debajo de ese umbral, la arquitectura de islands de Astro te da rendimiento mediblemente mejor."},{"heading":"Migración desde Next.js: Problemas Comunes","level":2,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#migracin-desde-nextjs-problemas-comunes","content":"Migrar un blog de Next.js 14 (340 posts, MDX, componentes custom) a Astro 5 revela varias trampas comunes:\n\n**Imports de componentes MDX**: Next.js auto-importa componentes vía ` mdx-components.tsx `. Astro necesita imports explícitos en cada archivo MDX o un plugin de remark para inyectarlos. Un plugin de remark resuelve esto:\n\n```javascript\n// remark-auto-import.mjs\nexport function remarkAutoImport() {\n return (tree) => {\n const imports = [\n \"import CodeBlock from '../../components/CodeBlock.astro';\",\n \"import Callout from '../../components/Callout.astro';\",\n ];\n\n tree.children.unshift({\n type: 'mdxjsEsm',\n value: imports.join('\\n'),\n data: {\n estree: {\n type: 'Program',\n body: imports.map((imp) => ({\n type: 'ImportDeclaration',\n specifiers: [/* parseado del string de import */],\n source: { type: 'Literal', value: imp.match(/from '(.+)'/)[1] },\n })),\n sourceType: 'module',\n },\n },\n });\n };\n}\n```\n\n**Optimización de imágenes**: El componente ` <Image> ` de Next.js no traduce 1:1. El ` <Image> ` de Astro desde ` astro:assets ` maneja bien imágenes locales pero las imágenes remotas necesitan configuración explícita de ` domains ` en ` astro.config.mjs `. Sitios con 200+ posts con imágenes remotas necesitan el allowlist de domains poblado.\n\n**Feed RSS**: Next.js no tiene RSS built-in. El paquete ` @astrojs/rss ` de Astro es excelente pero la migración del content layer significó que los scripts de generación de RSS necesitan reescritura para usar la nueva API de ` getCollection() `.\n\nEl tiempo de build pasó de 94 segundos a 14 segundos. El tamaño del bundle por página bajó de ~90KB a ~8KB (solo las páginas con componentes interactivos mandan JS). Los Core Web Vitals mejoraron en todos los frentes, la mediana de LCP bajó de 1.8s a 0.9s en mobile."},{"heading":"Lo Que Todavía Falta","level":2,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#lo-que-todava-falta","content":"Astro 5 no es perfecto. El HMR del dev server es más lento que el de Next.js para sitios grandes (500+ páginas). El ecosistema de componentes pre-armados es más chico. Si se necesita patrones pesados de middleware (auth, A/B testing, feature flags en cada request), el modelo de server islands es menos ergonómico que el middleware de Next.js.\n\nLa experiencia con TypeScript también está levemente atrás. La inferencia de tipos de Next.js para ` getStaticProps ` y server components es más ajustada. El soporte de TypeScript de Astro mejoró masivamente, pero ocasionalmente se va a encontrar tipos ` any ` en el content layer API que Next.js atraparía.\n\nAun así, para sitios de contenido, sitios genuinamente orientados al contenido donde el trabajo principal es renderizar texto, imágenes y datos estructurados, Astro 5 se destaca como la herramienta más fuerte en la categoría. El content layer API es el pipeline de datos que los sitios de contenido siempre necesitaron, los server islands resuelven el problema de contenido-dinámico-en-páginas-estáticas de forma elegante, y las view transitions hacen que todo se sienta como una SPA moderna sin el impuesto de JavaScript.\n\nConstruir una página con el nuevo content layer, medir los tiempos de build y verificar los scores de Lighthouse proporciona datos suficientes para una decisión informada."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#questions","content":""},{"heading":"Astro 5 es un reemplazo directo de Astro 4?","level":3,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#question-1","content":"No del todo. El content layer API reemplaza las viejas content collections con un sistema basado en loaders. Si se está usando defineCollection con type: 'content', se va a necesitar migrar al nuevo patrón de loaders. La herramienta de migración del CLI maneja alrededor del 80% de los casos, pero plugins custom de remark/rehype pueden necesitar ajustes manuales."},{"heading":"Astro 5 puede reemplazar a Next.js para todos los casos?","level":3,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#question-2","content":"No. Astro se destaca en sitios orientados a contenido donde la mayoría de las páginas son estáticas o levemente dinámicas. Si se necesita interactividad pesada del lado del cliente, flujos de auth complejos, o features en tiempo real en cada página, Next.js con React Server Components sigue siendo mejor opción. El punto óptimo de Astro son blogs, docs, sitios de marketing y portfolios con interactividad selectiva."},{"heading":"Cómo afectan los server islands a las estrategias de caché?","level":3,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#question-3","content":"Cada server island se fetchea independientemente, así que es posible setear diferentes políticas de caché por island. Los shells estáticos obtienen full caché de CDN mientras los islands dinámicos lo bypasean. Utilizar headers de Cache-Control en los endpoints de tus islands y considerá stale-while-revalidate para islands que toleren data levemente desactualizada."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#article-tips","content":"1. **Tip:** Cuando migrás de content collections de Astro 4, no intentes reescribir tu ` config.ts ` todo de una. Utilizar ` defineCollection() ` con la nueva propiedad ` loader ` en una sola colección primero, verificá el build, y después continuar con el resto. El approach viejo basado en glob sigue funcionando durante la transición.\n2. **Tip:** Los server islands con ` server:defer ` son tentadores para meter en todos lados, pero cada uno agrega un waterfall de red en el cliente. Profileá tu uso real con ` performance.mark() ` antes y después, más de 3 islands diferidos por página tiende a empezar a afectar el LCP en conexiones 3G.\n3. **Tip:** Para view transitions, siempre colocar ` transition:persist ` en tus elementos de audio/video y en cualquier componente que mantenga conexiones WebSocket. Sin esto, los badges de notificaciones en vivo se resetean en cada navegación porque el DOM se destruye en cada transición.\n\n- astro\n- view-transitions\n- content-collections\n- server-islands\n- hybrid-rendering\n- nextjs\n- frontend-architecture\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/astro-5-view-transitions-content-collections"},{"text":"Astro","url":"https://astro.build/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections"},"headline":"Content Layer API, Server Islands y View Transitions en Astro 5","description":"Análisis del content layer API de Astro 5, la arquitectura de server islands y view transitions, con benchmarks de producción comparando el rendimiento con Next.js para sitios de contenido.","datePublished":"2026-02-21","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://statics.lucioduran.com/images/lucioduran-social-card.jpg","url":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Frontend","keywords":"astro, view-transitions, content-collections, server-islands, hybrid-rendering, nextjs, frontend-architecture","wordCount":1843,"timeRequired":"PT10M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/astro-5-view-transitions-content-collections"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Content Layer API, Server Islands y View Transitions en Astro 5","item":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Astro 5 es un reemplazo directo de Astro 4?","acceptedAnswer":{"@type":"Answer","text":"No del todo. El content layer API reemplaza las viejas content collections con un sistema basado en loaders. Si se está usando defineCollection con type: 'content', se va a necesitar migrar al nuevo patrón de loaders. La herramienta de migración del CLI maneja alrededor del 80% de los casos, pero plugins custom de remark/rehype pueden necesitar ajustes manuales."}},{"@type":"Question","name":"Astro 5 puede reemplazar a Next.js para todos los casos?","acceptedAnswer":{"@type":"Answer","text":"No. Astro se destaca en sitios orientados a contenido donde la mayoría de las páginas son estáticas o levemente dinámicas. Si se necesita interactividad pesada del lado del cliente, flujos de auth complejos, o features en tiempo real en cada página, Next.js con React Server Components sigue siendo mejor opción. El punto óptimo de Astro son blogs, docs, sitios de marketing y portfolios con interactividad selectiva."}},{"@type":"Question","name":"Cómo afectan los server islands a las estrategias de caché?","acceptedAnswer":{"@type":"Answer","text":"Cada server island se fetchea independientemente, así que es posible setear diferentes políticas de caché por island. Los shells estáticos obtienen full caché de CDN mientras los islands dinámicos lo bypasean. Utilizar headers de Cache-Control en los endpoints de tus islands y considerá stale-while-revalidate para islands que toleren data levemente desactualizada."}}]}]}],"contentHash":"368ba7277e709e003fd218c82a6c6d31846d4bbab583fa0747d3a90c82050bcd","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/astro-5-view-transitions-content-collections","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":74863,"contentBytes":16208}}
