Esta semana hemos lanzado el primer beta de Astro 5, que incluye una forma completamente nueva de manejar contenido en Astro. Este post hace un deep dive en la Content Layer API, mostrando cómo funciona y cómo puedes usarla para construir tus sitios.
Astro nació para crear sitios web content-driven. Aunque ahora también se puede usar para construir todo tipo de dynamic apps, sigue siendo el mejor para sitios construidos alrededor de mucho contenido. Desde sitios de docs full-featured construidos con Starlight como Cloudflare y StackBlitz, hasta hermosos marketing sites para marcas como Porsche y Netlify, millones de usuarios cada día ya están disfrutando de los sitios rápidos y accesibles que puedes construir con Astro, y miles de ingenieros aman la mejor experiencia de desarrollador de la industria.
En Astro 2 introdujimos las Content Collections como una nueva y poderosa forma de organizar tu contenido local, construir con type safety, y escalar a miles de páginas. Las Content Collections proporcionan una experiencia de desarrollador best-in-class para archivos locales como Markdown y MDX, pero escuchamos de vosotros que queríais los mismos beneficios para todo vuestro contenido incluyendo remote APIs. También estaba claro que mientras muchas personas estaban construyendo sitios con miles de páginas, nuestra Content Collections API tenía problemas para escalar a decenas de miles de páginas, con builds más lentos y un uso excesivo de memoria.
En junio compartimos un early preview de nuestro plan para resolver estos problemas y más con un tipo completamente nuevo de content collection respaldado por la Content Layer API que te da la flexibilidad que pediste. Imaginamos llevar las collections más allá de solo archivos en src/content y dejar que cargues tu contenido desde cualquier lugar. Hemos estado probando con collections que escalan a un tamaño nunca antes posible. Desde nuestro primer release experimental en Astro 4.14, hemos estado trabajando duro en estabilizar esta nueva API para su lanzamiento en Astro 5.
Qué es el Content Layer
La Content Layer API es el futuro de las content collections que conoces y amas. Te permite cargar data de cualquier fuente cuando construyes tu sitio, y luego acceder a ella en tu página con una API simple y type-safe.
A la Content Layer API no le importa dónde está almacenado tu data. Una collection podría seguir siendo archivos locales de Markdown, otra podría llamar a una API, y otra podría vivir en otro lugar de tu filesystem. Usando las mismas funciones getEntry() y getCollection() que antes, puedes cargar data de muchas fuentes en la misma página. No hay impacto en el rendimiento: Astro cachea la data localmente entre builds, lo que significa que las updates pueden ser rápidas y pueden minimizar el número de API calls que necesitan hacerse. Y por supuesto, todo sigue siendo type-safe, con TypeScript types auto-generados desde tu schema.
Si has usado content collections antes, reconocerás muchos de los siguientes conceptos y términos. De hecho, ¡no hemos cambiado mucho sobre cómo usar las collections en tu proyecto! Una collection sigue siendo nuestro término para un conjunto de entries que comparten un schema común. Cada entry tiene un ID único. Es análogo a una tabla en una base de datos relacional.
Pero ahora, cada collection usa un loader, que define cómo se cargan los entries para poblar esa collection. Un loader puede ser una inline function básica que devuelve un array de entries, o puede ser un object más avanzado que maneja su propio caching y data store. Los primeros loaders ya están distribuidos como módulos en npm.
Siempre que se construye tu sitio, el loader de cada collection es invocado lo cual actualiza el data store local. Puedes consultar ese data store usando las familiares funciones getCollection() y getEntry(). Esto se hace en build time para tus páginas prerendered, o durante server rendering si usas un on-demand adapter. En cada uno de estos casos, la misma data está disponible, que es el snapshot del momento del build.
Creando collections
Define tus content collections en src/content/config.ts, que ya tienes si has usado collections antes. La nueva propiedad loader define la fuente de los datos, y puede ser tan simple como una async function que devuelve un array de items:
import { defineCollection, z } from 'astro:content';
const countries = defineCollection({ loader: async () => { const response = await fetch('https://restcountries.com/v3.1/all'); const data = await response.json(); // Must return an array of entries with an id property, or an object with IDs as keys and entries as values return data.map((country) => ({ id: country.cca3, ...country, })); }, // optionally define a schema using Zod schema: z.object({ id: z.string(), name: z.string(), capital: z.array(z.string()), population: z.number(), // ... }),});
export const collections = { countries };Esta data está entonces disponible en tus componentes .astro, igual que antes:
---import type { GetStaticPaths } from 'astro';import { getCollection } from 'astro:content';
export const getStaticPaths: GetStaticPaths = async () => { const collection = await getCollection('countries'); if (!collection) return []; return collection.map((country) => ({ params: { id: country.id, }, props: { country, }, }));};
const { country } = Astro.props;---
<h1>{craft.data.name}</h1><p>Capital: {craft.data.capital}</p>Ya estábamos bastante contentos con esta experiencia de consultar y renderizar tu data en la página, así que volvimos nuestra atención a la mecánica subyacente del proceso. ¡Hagamos ese deep dive en cómo la Content Layer API organiza, gestiona y usa tu contenido!
Ciclo de vida del content layer
Cuando se ejecuta astro build o astro dev, el loader de cada collection es invocado en paralelo. Estos loaders actualizan su propio scoped data store, que se preserva entre builds.
Los componentes y páginas de Astro pueden entonces usar getCollection o getEntry para consultar la data. La data es immutable en ese punto, así que todas las páginas consultan el mismo snapshot, compilado en build time. Esto aplica tanto si la página está siendo prerendered en build time, como si es server rendered on-demand. Lo importante a notar aquí es que el data store solo se actualiza en build time: un sitio desplegado no puede cambiar el data store. Si una fuente de datos necesita actualizar una collection debe hacerlo triggersando un nuevo build.
Aunque es immutable en producción, cuando se ejecuta astro dev el data store puede ser actualizado on demand por los usuarios con el hotkey s+enter, o por integraciones. Pueden hacerlo de muchas maneras, como registrar un development refresh endpoint o abrir un socket a un CMS para escuchar updates.
Cómo funciona un loader
El primer ejemplo mostró cómo construir un simple inline loader, pero no necesitas quedarte ahí. Con la object loader API, puedes hacer loaders potentes con capacidades más avanzadas. Un object loader interactúa con el content layer vía un data store object. Este es un key-value store que está scoped a una collection individual. Una collection solo puede acceder a sus propios entries pero tiene control completo sobre estos. Si un loader sabe que su fuente de datos no ha cambiado entonces puede saltar la actualización completamente, o puede simplemente actualizar los entries que han cambiado.
La Content Layer API proporciona algunas herramientas para hacer esto más fácil. Primero está el metadata store, que puede usarse para almacenar valores arbitrarios como tiempos de Última modificación, o sync tokens. Comparar estos permitirá al loader hacer cosas como conditional API requests, o usar delta sync APIs.
Este ejemplo muestra cómo hacer esto con un RSS feed loader. Almacena el last modified header en el metadata store y luego lo usa para hacer conditional requests cuando carga el feed la próxima vez:
export function feedLoader({ url }: FeedLoaderOptions): Loader { const feedUrl = new URL(url); // Return a loader object return { // The name of the loader. This is used in logs and error messages. name: 'feed-loader', // The load method is called to load data load: async ({ store, logger, meta }) => { // Check if there's a last-modified time already stored const lastModified = meta.get('last-modified');
// If so, make a conditional request for the feed const headers = lastModified ? { 'If-Modified-Since': lastModified } : {};
const res = await fetch(feedUrl, { headers });
// If the feed hasn't changed, you do not need to update the store if (res.status === 304) { logger.info('Feed not modified, skipping'); return; } if (!res.ok || !res.body) { throw new Error(`Failed to fetch feed: ${res.statusText}`); }
// Store the last-modified header in the meta store so we can // send it with the next request meta.set('last-modified', res.headers.get('last-modified'));
// ... now store the data }, };}Si el contenido ha cambiado, podemos tanto limpiar el store y reemplazarlo todo, como actualizar incrementalmente entries individuales si la fuente de datos proporciona este nivel de detalle.
export function feedLoader({ url }: FeedLoaderOptions): Loader { const feedUrl = new URL(url); // Return a loader object return { // The name of the loader. This is used in logs and error messages. name: 'feed-loader', // The load method is called to load data load: async ({ store, logger, meta }) => { // Check if there's a last-modified time already stored const lastModified = meta.get('last-modified');
// If so, make a conditional request for the feed const headers = lastModified ? { 'If-Modified-Since': lastModified } : {};
const res = await fetch(feedUrl, { headers });
// If the feed hasn't changed, you do not need to update the store if (res.status === 304) { logger.info('Feed not modified, skipping'); return; } if (!res.ok || !res.body) { throw new Error(`Failed to fetch feed: ${res.statusText}`); }
// Store the last-modified header in the meta store so we can // send it with the next request meta.set('last-modified', res.headers.get('last-modified'));
const feed = parseFeed(res.body);
// If the loader doesn't handle incremental updates, clear the store before inserting new entries // In some cases the API might send a stream of updates, in which case you would not want to clear the store // and instead add, delete, or update entries as needed. store.clear();
for (const item of feed.items) { // The parseData helper uses the schema to validate and transform data const data = await parseData({ id: item.guid, data: item, });
// The generateDigest helper lets you generate a digest based on the content. This is an optional // optimization. When inserting data into the store, if the digest is provided then the store will // check if the content has changed before updating the entry. This will avoid triggering a rebuild // in development if the content has not changed. const digest = generateDigest(data);
store.set({ id, data, // If the data source provides HTML, it can be set in the `rendered` property // This will allow users to use the `<Content />` component in their pages to render the HTML. rendered: { html: data.description ?? '', }, digest, }); } }, };}Cuándo no usar content collections
Anteriormente estaba claro cuándo las content collections eran una buena idea: ¡siempre que estuvieras usando contenido local en tus páginas! La Content Layer API te da mucho más poder y flexibilidad porque puedes usarla para cualquier fuente de contenido incluyendo live APIs. Sin embargo, es importante recordar que la data solo se actualiza cuando se construye el sitio, así que no servirá para todos los use cases.
Esto significa que las collections son perfectas cuando tu data cambia relativamente poco frecuente, como un blog. Si estás escribiendo un blog que está hosteado en un CMS, puedes triggerar un build con un webhook siempre que publiques un nuevo post. Las incremental updates del content layer deberían hacer este build rápido.
Lo mismo aplica a la mayoría de sitios e-commerce, donde un build puede ser triggerado cuando se edita un producto. Si estás ok con esperar el tiempo que toma desplegar el sitio para publicar updates, entonces las content collections siguen siendo tu opción obvia. Obtendrás el mejor rendimiento y una gran experiencia de desarrollador.
Si necesitas que tus páginas se actualicen en near real-time o con contenido personalized, entonces es mejor usar un on-demand rendering adapter, idealmente con CDN cache headers para asegurar que las cargas de páginas sean super rápidas. Incluso puedes combinar ambos usando server islands y obtener lo mejor de ambos mundos - renderizar el contenido principal usando content collections, y usar un server island para contenido real-time o personalized.
Qué viene a continuación
La Content Layer API es un gran paso adelante para Astro, pero apenas estamos empezando. Actualmente, el data store es solo un key-value store, con filtering limitado. Esto es rápido pero no memory efficient, y el querying no es muy flexible. Nuestro objetivo es introducir un backend basado en Astro DB en una futura versión para ayudar a escalar a cientos de miles de páginas. También nos permitirá soportar queries más complejas, y quizás incluso real-time updates.
Mientras tanto, ¡nos encantaría escuchar tu feedback sobre la Content Layer API! Prueba migrar tus sitios existentes para usarla. (¡Es fácil, lo prometemos!) y cuéntanos cómo te va. Prueba algunos de los nuevos loaders construidos por la comunidad, o construye el tuyo propio para tu API favorita. ¡Estamos emocionados de ver qué construyes con ello!
