Saltar al contenido

El loader como objeto: load, store y contexto

El loader con name, load y schema que recibe el contexto y escribe directamente en el store. Manejar set, get, has, delete y clear; validar con parseData, calcular la huella con generateDigest y hablar con el logger para convertir una carga en un loader reutilizable en Astro 7.

De la función al objeto

Un loader como objeto es una estructura con tres piezas: un name que lo identifica en los logs y en la caché, una función load donde ocurre todo el trabajo, y un schema opcional que puede depender de la propia carga. La diferencia crucial con el loader inline es que load no devuelve nada: en lugar de entregar un array, recibe un contexto con el store y escribe en él directamente. El loader pasa de “producir datos” a “gestionar un almacén”.

TypeScript
import type { Loader } from 'astro/loaders';export function apiLoader(url: string): Loader {  return {    name: 'api-loader',    load: async ({ store, parseData, generateDigest, logger }) => {      logger.info(`Cargando desde ${url}`);      const res = await fetch(url);      const items = await res.json();      store.clear();      for (const item of items) {        const data = await parseData({ id: item.id, data: item });        const digest = generateDigest(data);        store.set({ id: item.id, data, digest });      }    },  };}

Este patrón —una función que recibe opciones y devuelve un objeto Loader— es el idiomático, porque convierte al loader en una pieza configurable y reutilizable. Lo declaras una vez y lo usas en varias colecciones con distintas url, igual que glob y file son fábricas que devuelven loaders según sus opciones. El loader como objeto no es solo más potente que el inline: es la forma de empaquetar una integración y compartirla.

TypeScript
const noticias = defineCollection({  loader: apiLoader('https://api.ejemplo.com/noticias'),  schema: z.object({ id: z.string(), titular: z.string() }),});

El schema, además, puede vivir dentro del loader en lugar de en defineCollection. Un loader que conoce la forma de su fuente —porque habla con una API concreta— puede exponer su propio esquema, incluso uno asíncrono que consulte la API para derivarlo. Cuando el loader trae su esquema, quien lo usa no tiene que escribir uno: la integración llega completa, con su forma incluida.

El store por dentro

Dentro de load, el store es tu interfaz con el almacén de la colección, y ofrece justo los métodos de un mapa persistente. store.set añade o reemplaza una entrada; recibe un objeto con al menos un id y un data, y admite body, rendered y digest cuando aplican. store.get recupera una entrada por su id. store.has comprueba si existe. store.delete la elimina. Y store.clear vacía la colección entera de un golpe.

TypeScript
store.set({ id: 'post-1', data });     // anade o reemplazaconst entrada = store.get('post-1');   // recupera por idif (store.has('post-2')) { /* ... */ } // comprueba existenciastore.delete('post-3');                // elimina unastore.clear();                          // vacia la coleccion entera

Para recorrer lo que hay, el store ofrece store.keys, store.values y store.entries, que devuelven los id, los datos y los pares completos. Con ellos, un loader puede razonar sobre el estado previo: comparar las claves que ya tenía con las que la fuente ofrece ahora, y borrar las que desaparecieron. Esa capacidad de mirar atrás —de saber qué había antes— es justo lo que el loader inline no tenía y lo que abre la puerta a las actualizaciones incrementales.

El gesto de store.clear seguido de un bucle de store.set es el patrón de “reemplazo total”: olvida todo y vuelve a poblar. Es correcto y simple, y para muchas fuentes basta. Pero fíjate en que ahora eliges hacerlo, en lugar de que te lo impongan: podrías, en su lugar, actualizar solo las entradas cambiadas y conservar el resto. El loader como objeto te da esa elección; qué hagas con ella es la diferencia entre un loader que rehace todo y uno que solo toca lo justo.

parseData, generateDigest y el logger

El contexto de load trae, junto al store, tres herramientas que elevan un bucle de carga a un loader robusto. parseData es la validación: recibe un id y un data crudo, lo comprueba contra el esquema de la colección y devuelve el dato ya tipado, o lanza si no cumple. Llamarlo antes de store.set es lo que garantiza que al almacén solo llegue contenido válido; saltárselo es meter datos sin verificar y perder la red de seguridad que justifica todo el sistema.

TypeScript
const data = await parseData({ id: item.id, data: item });// data esta validado contra el schema; si no cumpliera, parseData habria lanzadostore.set({ id: item.id, data, digest: generateDigest(data) });

generateDigest calcula una huella —un hash— de un dato: la misma entrada produce siempre el mismo digest, y una entrada distinta produce otro. Su utilidad es detectar cambios sin comparar campo a campo. Si guardas el digest de cada entrada en el store, en la siguiente carga puedes calcular el digest nuevo y compararlo con el viejo: si coinciden, la entrada no cambió y puedes ahorrarte reprocesarla. store.set incluso usa el digest para saltarse la escritura si nada cambió. Es la pieza técnica sobre la que se construye el caché incremental de la próxima lección.

El logger, por fin, es la voz del loader. En vez de un console.log anónimo, emite mensajes con el nombre del loader por delante, integrados en la salida de Astro, con niveles —logger.info, logger.warn, logger.error, logger.debug—. Un loader que habla con una fuente remota debería contar lo que hace: cuántas entradas trajo, si la fuente respondió con un cambio o sin él, cuánto tardó. Ese rastro es lo que convierte un fallo de integración de un misterio en un diagnóstico.

Entradas con cuerpo, no solo datos

Hasta aquí el loader guardaba entradas de puros datos, pero puede también aportar contenido renderizable, como hace glob con el Markdown. Para eso, store.set acepta dos campos más: body, el texto original sin procesar, y rendered, un objeto con el HTML ya generado y sus metadatos. Una entrada con rendered es la que luego responde a render() en la plantilla; una sin él es un registro de datos y nada más.

Generar ese HTML a mano sería tedioso, así que el contexto de load ofrece una ayuda: renderMarkdown, una función que toma una cadena de Markdown y devuelve el rendered listo para guardar. Con ella, un loader que trae artículos de un CMS escritos en Markdown puede entregarlos tan renderizables como si fueran ficheros locales, sin salir del sistema de collections.

TypeScript
load: async ({ store, parseData, renderMarkdown }) => {  const posts = await traerPostsDelCms();  for (const post of posts) {    const data = await parseData({ id: post.slug, data: post.meta });    const rendered = await renderMarkdown(post.cuerpo);    store.set({ id: post.slug, data, body: post.cuerpo, rendered });  }}

La distinción entre entrada de datos y entrada con cuerpo, que en glob venía dada por la extensión del fichero, aquí la decides tú al construir la entrada. Es el mismo eje de siempre —si esto es un registro o un documento— pero ahora bajo tu control total: un loader puede producir colecciones de datos, de documentos o de ambas cosas, según lo que su fuente ofrezca y lo que tú decidas depositar.