Live Content Collections: Un Análisis Profundo

Por
Matt Kane

Las live content collections representan la próxima evolución del contenido en Astro, trayendo capacidades de datos en tiempo real a la familiar API de content collections que ya conoces y amas.

Las content collections en Astro han pasado por varias etapas de evolución. Fueron lanzadas inicialmente como una forma increíblemente fácil y potente de gestionar contenido estructurado desde archivos en disco. Inicialmente soportando archivos Markdown, MDX y JSON, te permiten construir blogs, sitios de documentación y más con una gran experiencia de desarrollador y datos con seguridad de tipos.

Con Astro 5.0, las content collections se expandieron a un Content Layer completo que soportaba loaders conectables para todo tipo de fuentes de datos, incluyendo APIs, CMSs y más.

En Astro 5.10, las content collections dan su próximo paso, con soporte experimental para live content collections. Con estas, ahora puedes obtener contenido en tiempo de ejecución en lugar de en tiempo de build, abriendo posibilidades completamente nuevas para experiencias de contenido dinámicas, personalizadas y en tiempo real.

Ya sea que estés construyendo un sitio de e-commerce con inventario que cambia frecuentemente, un sitio de noticias con actualizaciones de última hora, o un dashboard con métricas en vivo, las nuevas live content collections de Astro proporcionan la flexibilidad que necesitas mientras mantienen la seguridad de tipos y la experiencia de desarrollador que hacen a Astro especial.

Los cimientos de las Live Content Collections

Antes de profundizar en las live content collections, vale la pena entender los cimientos sobre los que están construidas: los loaders. Las content collections de Astro usan loaders para gestionar datos estructurados y contenido en tus proyectos. Cada content collection depende de su loader para definir cómo se llenan las entradas. Durante astro build, estos loaders se ejecutan para obtener datos y llenar un almacén de datos local. Tus páginas luego consultan esta instantánea inmutable usando las funciones getCollection() y getEntry().

Las live content collections llevan este concepto un paso más allá: en lugar de obtener datos en tiempo de build, los obtienen en tiempo de solicitud, dándote acceso a los datos más frescos posibles. A veces quieres la velocidad y confiabilidad del contenido estático, pero otras veces necesitas la flexibilidad y el dinamismo de los datos en vivo. Así como puedes elegir entre páginas renderizadas estáticas y bajo demanda en Astro, ahora puedes elegir entre content collections de tiempo de build y live.

Cualquiera que sea la elección que hagas, obtienes la misma API familiar de tus content collections existentes. Si sabes cómo usar getCollection() y getEntry(), ya sabes la mayor parte de lo que necesitas para usar getLiveCollection() y getLiveEntry().

La arquitectura de las live collections

A diferencia de las content collections de tiempo de build que llenan un almacén de datos estático durante el proceso de build, las live content collections funcionan de forma fundamentalmente diferente por debajo:

Cuando se solicita una página que usa live content collections:

  1. La página llama a getLiveCollection() o getLiveEntry() para obtener datos.
  2. Los datos se obtienen de la fuente externa (API, base de datos, etc.).
  3. Los resultados se procesan y validan contra tu esquema.
  4. Los datos se devuelven a tu componente de página.

Esta arquitectura significa que siempre estás trabajando con datos frescos, pero también significa que cada solicitud involucra llamadas de red a tus fuentes de datos. Este trade-off es perfecto para casos de uso donde la frescura de los datos es más importante que el rendimiento absoluto. Puedes mitigar las preocupaciones de rendimiento con cacheo de páginas, y las live collections ayudan proporcionando cache hints que puedes usar para optimizar esto. A medida que esta característica experimental se desarrolla, Astro eventualmente manejará más de esto por ti. Por ahora, puedes usar Cache-Control y otros headers para controlar cuánto tiempo se cachean los datos en el navegador y en los CDNs.

Configurando Live Content Collections

Empezar con las live content collections requiere habilitar el flag experimental y crear una configuración de live collection:

astro.config.mjs
export default defineConfig({
experimental: {
liveContentCollections: true,
},
// Live collections require an adapter for on-demand rendering
adapter: node({
mode: 'standalone',
}),
});

Luego, crea un archivo src/live.config.ts para definir tus live collections, especificando type: 'live' y el loader de la colección.

En este ejemplo estoy usando dos paquetes de live loader que creé, pero probablemente necesitarás crear tus propios loaders para tus propias fuentes de datos en vivo siguiendo nuestra documentación. ¡Esperamos que más loaders de la comunidad estén disponibles, así que asegúrate de compartir lo que construyas! (Me tomó unas pocas horas agregar soporte de live loader a mis paquetes existentes de feed loader y Bluesky loader para colecciones de tiempo de build, así que esperemos que no sea demasiado difícil empezar.)

src/live.config.ts
import { defineLiveCollection } from 'astro:content';
import { liveFeedLoader } from '@ascorbic/feed-loader';
import { liveBlueskyLoader } from '@ascorbic/bluesky-loader';
export const astroNews = defineLiveCollection({
type: 'live',
loader: liveFeedLoader({
url: 'https://astro.build/rss.xml',
}),
});
export const socialPosts = defineLiveCollection({
type: 'live',
loader: liveBlueskyLoader({
identifier: 'astro.build',
limit: 10,
}),
});

Obteniendo datos en vivo en tus páginas

Una vez que hayas definido tus live collections, usarlas en tus páginas es muy similar a las content collections de tiempo de build existentes, con algunas diferencias clave. En particular, querrás agregar algo de manejo de errores ya que tus datos se están cargando en vivo desde una fuente externa:

src/pages/news.astro
---
export const prerender = false;
import { getLiveCollection } from 'astro:content';
// Fetch the latest Astro blog posts
const { entries: blogPosts, error } = await getLiveCollection('astroNews');
if (error) {
console.error('Failed to load news:', error);
}
---
<h1>Latest Astro News</h1>
{
error ? (
<p>Unable to load news at this time. Please try again later.</p>
) : (
<div class="news-grid">
{blogPosts.map((post) => (
<article class="news-card">
<h2>
<a href={post.data.url}>{post.data.title}</a>
</h2>
{post.data.description && (
<p class="summary">{post.data.description}</p>
)}
</article>
))}
</div>
)
}

También puedes usar getLiveEntry() para obtener una sola entrada por su ID, o usando parámetros de filtro:

src/pages/social/[id].astro
---
export const prerender = false;
import { getLiveEntry, render } from 'astro:content';
const postId = Astro.params.id;
const { entry: post, error } = await getLiveEntry('socialPosts', postId);
if (error) {
console.error('Failed to load post:', error);
return Astro.rewrite('/404');
}
const { Content } = await render(post);
---
<div class="post">
<Content />
<div class="engagement-stats">
<span>❤️ {post.data.likeCount}</span>
<span>🔄 {post.data.repostCount}</span>
<span>💬 {post.data.replyCount}</span>
{post.data.quoteCount > 0 && <span>📝 {post.data.quoteCount}</span>}
</div>
</div>

Construyendo un live content loader

Crear live loaders personalizados te permite conectarte a cualquier API o fuente de datos, dándote control completo sobre cómo se obtienen y procesan los datos. La API está diseñada para ser simple, flexible y con seguridad de tipos, para que puedas construir loaders que se adapten a tus necesidades específicas, manteniendo la facilidad de uso y seguridad de tipos por las que Astro es conocido. Nos encantaría ver a la gente probando la API experimental y dando comentarios sobre ella, para que podamos mejorarla antes de que sea estable.

Creando un Custom API Loader

Aquí hay un ejemplo de un live loader para una API de e-commerce:

src/loaders/store-loader.ts
import type { LiveLoader } from 'astro:content';
interface Product {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
description?: string;
}
interface ProductFilter {
category?: string;
inStock?: boolean;
}
export function createStoreLoader(
baseUrl: string,
): LiveLoader<Product, ProductFilter> {
return {
loadCollection: async (filter) => {
try {
const url = new URL(`${baseUrl}/products`);
if (filter?.category) {
url.searchParams.set('category', filter.category);
}
if (filter?.inStock !== undefined) {
url.searchParams.set('inStock', filter.inStock.toString());
}
const response = await fetch(url);
if (!response.ok) {
return {
error: new Error(
`Failed to fetch products: ${response.statusText}`,
),
};
}
const data = await response.json();
return {
entries: data.map((product: Product) => ({
id: product.id,
data: product,
})),
};
} catch (error) {
return {
error: error instanceof Error ? error : new Error('Unknown error'),
};
}
},
loadEntry: async (id) => {
try {
const response = await fetch(`${baseUrl}/products/${id}`);
if (response.status === 404) {
return { entry: null };
}
if (!response.ok) {
return {
error: new Error(`Failed to fetch product: ${response.statusText}`),
};
}
const product = await response.json();
return {
entry: {
id: product.id,
data: product,
},
};
} catch (error) {
return {
error: error instanceof Error ? error : new Error('Unknown error'),
};
}
},
};
}

Luego úsalo en tu configuración de live collections:

src/live.config.ts
import { defineLiveCollection, z } from 'astro:content';
import { createStoreLoader } from './loaders/store-loader';
export const products = defineLiveCollection({
type: 'live',
loader: createStoreLoader('https://store.example.com'),
schema: z.object({
id: z.string(),
name: z.string(),
price: z.number(),
category: z.string(),
inStock: z.boolean(),
description: z.string().optional(),
}),
});

Loader con contenido renderizado

Los live content loaders pueden soportar contenido renderizado, facilitando a los usuarios mostrar contenido HTML obtenido desde una API. Aquí hay un ejemplo de un loader de publicaciones de blog que obtiene posts desde un CMS y renderiza el contenido como HTML:

src/loaders/blog-loader.ts
import type { LiveLoader } from 'astro:content';
interface BlogPost {
title: string;
author: string;
publishDate: Date;
content: string;
excerpt: string;
tags: string[];
}
interface BlogFilter {
status?: 'published' | 'draft';
author?: string;
}
export function createBlogLoader(
baseUrl: string,
): LiveLoader<BlogPost, BlogFilter> {
return {
loadCollection: async (filter) => {
try {
const url = new URL(`${baseUrl}/posts`);
if (filter?.status) {
url.searchParams.set('status', filter.status);
}
if (filter?.author) {
url.searchParams.set('author', filter.author);
}
const response = await fetch(url);
if (!response.ok) {
return {
error: new Error(`Failed to fetch posts: ${response.statusText}`),
};
}
const posts = await response.json();
return {
entries: posts.map((post: any) => ({
id: post.slug,
data: {
title: post.title,
author: post.author,
publishDate: new Date(post.publishDate),
content: post.content,
excerpt: post.excerpt,
tags: post.tags || [],
},
rendered: post.html ? { html: post.html } : undefined,
})),
};
} catch (error) {
return {
error: error instanceof Error ? error : new Error('Unknown error'),
};
}
},
loadEntry: async (slug) => {
try {
const response = await fetch(`${baseUrl}/posts/${slug}`);
if (response.status === 404) {
return { entry: null };
}
if (!response.ok) {
return {
error: new Error(`Failed to fetch post: ${response.statusText}`),
};
}
const post = await response.json();
return {
entry: {
id: post.slug,
data: {
title: post.title,
author: post.author,
publishDate: new Date(post.publishDate),
content: post.content,
excerpt: post.excerpt,
tags: post.tags || [],
},
rendered: post.html ? { html: post.html } : undefined,
},
};
} catch (error) {
return {
error: error instanceof Error ? error : new Error('Unknown error'),
};
}
},
};
}

Luego úsalo en tu configuración de live collections:

src/live.config.ts
import { defineLiveCollection, z } from 'astro:content';
import { createBlogLoader } from './loaders/blog-loader';
export const blogPosts = defineLiveCollection({
type: 'live',
loader: createBlogLoader('https://cms.example.com'),
schema: z.object({
title: z.string(),
author: z.string(),
publishDate: z.date(),
content: z.string(),
excerpt: z.string(),
tags: z.array(z.string()),
}),
});

Live collections vs colecciones de tiempo de build

Entender cuándo usar live collections versus colecciones tradicionales de tiempo de build es crucial para construir aplicaciones performantes:

Usa live collections cuando:

  • Los datos cambian frecuentemente: Niveles de inventario, contenido generado por usuarios, métricas en vivo
  • Se requiere personalización: Recomendaciones específicas para el usuario, datos de dashboard
  • La precisión en tiempo real es crítica: Feeds de noticias, contenido de redes sociales, resultados en vivo
  • Se necesita filtrado dinámico: Resultados de búsqueda, catálogos de productos filtrados

Usa colecciones de tiempo de build cuando:

  • El contenido es relativamente estático: Publicaciones de blog, documentación, páginas de marketing
  • El rendimiento es primordial: Sitios de alto tráfico donde cada milisegundo cuenta
  • Necesitas transformaciones de imágenes o renderizado de MDX: Las live collections no soportan transformaciones de imágenes o renderizado de MDX

Enfoques híbridos

Puedes combinar ambos enfoques en el mismo proyecto, e incluso dentro de la misma página. Por ejemplo, podrías usar colecciones de tiempo de build para contenido estático como publicaciones de blog, mientras usas live collections para características dinámicas como comentarios o perfiles de usuario.

src/pages/blog/[slug].astro
---
export const prerender = false;
import { getEntry, getLiveCollection } from 'astro:content';
// Blog post content is fetched at build time and cached in the data store. The site is rebuilt when new posts are added
const post = await getEntry('blog', Astro.params.slug);
// Live comments are fetched at request time, so they always show the latest comments
const { entries: comments } = await getLiveCollection('comments', {
postId: Astro.params.slug,
});
---
<!-- Static blog post content -->
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
<!-- Live comments section -->
<section class="comments">
<h2>Comments ({comments.length})</h2>
{
comments.map((comment) => (
<div class="comment">
<strong>{comment.data.author}</strong>
<p>{comment.data.content}</p>
</div>
))
}
</section>

Patrones híbridos como estos combinan bien con server islands, permitiéndote crear páginas verdaderamente híbridas donde el contenido principal es estático pero componentes específicos obtienen datos en vivo. Esto te da lo mejor de ambos mundos: entrega de contenido estático rápido con secciones dinámicas y en tiempo real.

src/pages/blog/[slug].astro
---
// This page can be prerendered because the main content is static
import { getEntry, getCollection } from 'astro:content';
import Comments from '../components/Comments.astro';
export const getStaticPaths = async () => {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { slug: post.id },
}));
};
const post = await getEntry('blog', Astro.params.slug);
---
<!-- Static blog post content -->
<article>
<h1>{post.data.title}</h1>
<div class="content">
<Content />
</div>
</article>
<!-- Dynamic comments loaded via server island -->
<Comments server:defer postId={Astro.params.slug} />
src/components/Comments.astro
---
export const prerender = false;
import { getLiveCollection } from 'astro:content';
interface Props {
postId: string;
}
const { postId } = Astro.props;
// This component runs as a server island, fetching live data
const { entries: comments, error } = await getLiveCollection('comments', {
postId,
status: 'approved',
});
// Cache in the CDN for 10 minutes
Astro.response.headers.set('Cache-Control', 'public, s-maxage=600');
---
<section>
<h2>Comments</h2>
{error ? (
<p>Unable to load comments at this time.</p>
) : comments.length === 0 ? (
<p>No comments yet. Be the first to comment!</p>
) : (
<div class="comments-list">
{comments.map((comment) => (
<div class="comment">
<div class="comment-header">
<strong>{comment.data.author}</strong>
<time>{comment.data.createdAt.toLocaleDateString()}</time>
</div>
<p>{comment.data.content}</p>
</div>
))}
</div>
)}
</section>

Este enfoque proporciona varios beneficios. Te da una carga de página inicial rápida con contenido estático, mientras aún permite que componentes específicos obtengan datos en vivo según sea necesario. También te permite cachear los datos en vivo de manera efectiva, mejorando el rendimiento y reduciendo la carga en tus APIs.

Manejo de errores y resiliencia

Las live content collections proporcionan manejo de errores explícito que hace tu aplicación más resiliente:

src/pages/dashboard.astro
---
export const prerender = false;
import { getLiveCollection } from 'astro:content';
// Fetch multiple live collections with individual error handling
const [metricsResult, alertsResult, reportsResult] = await Promise.all([
getLiveCollection('metrics'),
getLiveCollection('alerts', { severity: 'high' }),
getLiveCollection('reports', { recent: true }),
]);
// Handle errors gracefully
const metrics = metricsResult.error ? [] : metricsResult.entries;
const alerts = alertsResult.error ? [] : alertsResult.entries;
const reports = reportsResult.error ? [] : reportsResult.entries;
const hasErrors =
metricsResult.error || alertsResult.error || reportsResult.error;
---
{
hasErrors && (
<div class="error-banner">
Some dashboard data may be outdated. Please refresh to try again.
</div>
)
}
<div class="dashboard">
<section class="metrics">
<h2>Metrics</h2>
{
metrics.length === 0 ? (
<p>No metrics available</p>
) : (
<div class="metrics-grid">
{metrics.map((metric) => (
<div class="metric-card">
<h3>{metric.data.name}</h3>
<p class="value">{metric.data.value}</p>
</div>
))}
</div>
)
}
</section>
<section class="alerts">
<h2>High Priority Alerts</h2>
{
alerts.length === 0 ? (
<p>No alerts - all systems operational</p>
) : (
<ul class="alerts-list">
{alerts.map((alert) => (
<li class="alert">
<strong>{alert.data.title}</strong>
<p>{alert.data.description}</p>
</li>
))}
</ul>
)
}
</section>
</div>

Consideraciones de rendimiento y mejores prácticas

Al usar live content collections, es importante considerar las implicaciones de rendimiento, especialmente cuando se usan APIs más lentas o complejas. Aquí hay algunas mejores prácticas a tener en cuenta:

Cacheo con cache hints

Las live content collections soportan cache hints que te permiten proporcionar metadatos de cacheo para tus respuestas. Esto ayuda a optimizar el rendimiento habilitando headers de cache apropiados y estrategias de invalidación de cache. En futuras versiones de Astro, estos cache hints serán usados para cachear páginas automáticamente, pero por ahora puedes usarlos para establecer headers HTTP apropiados en tus páginas.

src/loaders/cached-store-loader.ts
import type { LiveLoader } from 'astro:content';
interface Product {
id: string;
name: string;
price: number;
lastModified: string;
category: string;
}
export function createStoreLoader(baseUrl: string): LiveLoader<Product> {
return {
loadCollection: async (filter) => {
try {
const response = await fetch(`${baseUrl}/products`);
if (!response.ok) {
return {
error: new Error(
`Failed to fetch products: ${response.statusText}`,
),
};
}
const products = await response.json();
return {
entries: products.map((product: Product) => ({
id: product.id,
data: product,
cacheHint: {
tags: [`product-${product.id}`],
lastModified: new Date(product.lastModified),
},
})),
cacheHint: {
tags: ['products'],
},
};
} catch (error) {
return {
error: error instanceof Error ? error : new Error('Unknown error'),
};
}
},
loadEntry: async (id) => {
try {
const response = await fetch(`${baseUrl}/products/${id}`);
if (response.status === 404) {
return { entry: null };
}
if (!response.ok) {
return {
error: new Error(`Failed to fetch product: ${response.statusText}`),
};
}
const product = await response.json();
return {
entry: {
id: product.id,
data: product,
},
cacheHint: {
tags: [`product-${id}`],
lastModified: new Date(product.lastModified),
},
};
} catch (error) {
return {
error: error instanceof Error ? error : new Error('Unknown error'),
};
}
},
};
}

Luego usa los cache hints en tus páginas para establecer headers HTTP apropiados:

src/pages/products/[id].astro
---
export const prerender = false;
import { getLiveEntry } from 'astro:content';
const {
entry: product,
error,
cacheHint,
} = await getLiveEntry('products', Astro.params.id);
if (error || !product) {
return Astro.redirect('/products');
}
// Set cache headers based on the cache hint
if (cacheHint?.lastModified) {
Astro.response.headers.set(
'Last-Modified',
cacheHint.lastModified.toUTCString(),
);
}
if (cacheHint?.tags) {
Astro.response.headers.set('Cache-Tag', cacheHint.tags.join(','));
}
// Set your own cache control headers
Astro.response.headers.set('Cache-Control', 'public, max-age=600'); // 10 minutes
---
<h1>{product.data.name}</h1>
<p>Price: ${product.data.price}</p>

Ten en cuenta que los cache hints proporcionan metadatos sobre tu contenido, pero aún necesitarás establecer tus propios headers de cache para controlar el comportamiento real del cacheo. Plataformas como Netlify te permiten invalidar caches basándose en estos tags, para que puedas asegurar que tu contenido en vivo se mantenga fresco sin llamadas innecesarias a la API.

Casos de Uso del Mundo Real

Catálogo de Productos de E-commerce

src/pages/products/[...slug].astro
---
export const prerender = false;
import { getLiveCollection, getLiveEntry } from 'astro:content';
const slug = Astro.params.slug;
const { entry: product, error } = await getLiveEntry('products', slug);
if (error || !product) {
return Astro.redirect('/products');
}
// Also fetch related products
const { entries: related } = await getLiveCollection('products', {
category: product.data.category,
exclude: product.id,
limit: 4,
});
// Render product details and related items...
---

Agregación de Noticias y Redes Sociales

Aquí hay un ejemplo usando loaders de la comunidad para crear un dashboard de noticias y redes sociales en vivo:

src/pages/dashboard.astro
---
export const prerender = false;
import { getLiveCollection } from 'astro:content';
// Fetch live RSS feed using community loader
const { entries: astroNews, error: newsError } =
await getLiveCollection('astroNews');
// Fetch live Bluesky posts using community loader
const { entries: socialPosts, error: socialError } =
await getLiveCollection('socialPosts');
const hasErrors = newsError || socialError;
---
<div class="dashboard">
{
hasErrors && (
<div class="error-banner">
Some content may be unavailable. Please refresh to try again.
</div>
)
}
<section class="news-section">
<h2>Latest Tech News</h2>
{
newsError ? (
<p>Unable to load news at this time.</p>
) : (
<div class="news-grid">
{astroNews.map((article) => (
<article class="news-card">
<h3>
<a href={article.data.link} target="_blank">
{article.data.title}
</a>
</h3>
<p class="meta">
{article.data.pubDate?.toLocaleDateString()} |{' '}
{article.data.creator}
</p>
{article.data.summary && (
<p class="summary">{article.data.summary}</p>
)}
</article>
))}
</div>
)
}
</section>
<section class="social-section">
<h2>Latest from Bluesky</h2>
{
socialError ? (
<p>Unable to load social posts at this time.</p>
) : (
<div class="posts-feed">
{socialPosts.map((post) => (
<div class="post-card">
<div class="post-header">
<strong>{post.data.author.displayName}</strong>
<span class="handle">@{post.data.author.handle}</span>
<time>{post.data.createdAt.toLocaleString()}</time>
</div>
<div class="post-content">
{post.rendered && <Fragment set:html={post.rendered.html} />}
</div>
</div>
))}
</div>
)
}
</section>
</div>

Usando Custom Loaders en Páginas

Una vez que hayas creado custom loaders, puedes usarlos en tus páginas:

src/pages/products/[id].astro
---
export const prerender = false;
import { getLiveEntry } from 'astro:content';
// Fetch a single product with error handling
const { entry: product, error } = await getLiveEntry(
'products',
Astro.params.id,
);
if (error) {
console.error('Failed to load product:', error);
return Astro.redirect('/products');
}
if (!product) {
return Astro.redirect('/products');
}
---
<h1>{product.data.name}</h1>
<p class="price">
{
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(product.data.price)
}
</p>
<p class="stock-status">
{product.data.inStock ? 'In Stock' : 'Out of Stock'}
</p>
{
product.data.description && (
<div class="description">
<p>{product.data.description}</p>
</div>
)
}

El Futuro de las Live Content Collections

Las live content collections son actualmente experimentales, pero representan un paso importante en la evolución de Astro, abriendo más casos de uso para construir sitios con contenido dinámico y en tiempo real en Astro mientras se mantiene la experiencia de desarrollador que amas.

Próximos pasos

Las live content collections son experimentales en Astro 5.10 y necesitamos tus comentarios. Para involucrarte:

Las live content collections abren nuevas posibilidades emocionantes para construir experiencias web dinámicas y en tiempo real mientras se mantiene la experiencia de desarrollador que amas. Ya sea que estés construyendo un sitio de e-commerce, una plataforma de noticias o un dashboard de datos, las live collections proporcionan la flexibilidad y el poder que necesitas para crear aplicaciones web verdaderamente dinámicas.