Saltar al contenido

Estrategia de contenido a escala

Cuando el contenido pasa de un puñado de ficheros a decenas de colecciones alimentadas por un CMS, hace falta estrategia: organizar content.config.ts con esquemas que se referencian, escribir loaders propios que traigan datos remotos con caché incremental, servir el sitio en varios idiomas con el i18n de Astro, y generar miles de páginas sin que el build se ahogue mezclando estático, bajo demanda y server islands.

Muchas colecciones que se referencian

A escala, el contenido deja de ser una sola colección de posts y se vuelve un pequeño modelo de datos: posts, autores, categorías, series, páginas de aterrizaje. Todo eso se declara en src/content.config.ts, y la tentación de dejarlo crecer como un único fichero de mil líneas se combate con el mismo criterio del nivel: cada colección define su esquema, y las relaciones entre ellas se expresan con reference, que enlaza una entrada con otra de otra colección sin duplicar datos.

TypeScript
// src/content.config.tsimport { defineCollection, reference, z } from 'astro:content';import { glob } from 'astro/loaders';const posts = defineCollection({  loader: glob({ pattern: '**/*.mdx', base: './src/content/posts' }),  schema: z.object({    title: z.string(),    lang: z.enum(['es', 'en', 'pt']),    autor: reference('autores'),    relacionados: z.array(reference('posts')).default([]),  }),});const autores = defineCollection({  loader: glob({ pattern: '**/*.json', base: './src/content/autores' }),  schema: z.object({ nombre: z.string(), bio: z.string() }),});export const collections = { posts, autores };

Un reference('autores') no copia el autor dentro del post: guarda una referencia que luego resuelves con getEntry, de modo que el nombre del autor vive en un solo sitio y cambiarlo actualiza todos sus posts. Ese modelado relacional —normalizar, referenciar, resolver— es lo que distingue un contenido que escala de uno que se repite a sí mismo hasta volverse incoherente. Cuando la referencia falla porque el autor no existe, el esquema lo detecta en el build, no en producción.

Loaders: del glob local al CMS remoto

El glob que has visto es un loader, y ahí está la idea que abre la puerta a la escala: una colección no está atada a ficheros locales. Un loader es cualquier cosa que sepa poblar el almacén de contenido, así que el mismo posts que hoy lee .mdx de tu disco puede mañana leer de un CMS remoto sin que las páginas que lo consumen cambien una línea. Escribir un loader propio es implementar un objeto con un name y una función load que recibe el store y lo llena.

TypeScript
// src/loaders/cms.tsimport type { Loader } from 'astro/loaders';export function cmsLoader(config): Loader {  return {    name: 'cms-loader',    load: async ({ store, meta, parseData, generateDigest, logger }) => {      const desde = meta.get('ultima-sync');      const items = await fetch(config.endpoint + '?desde=' + desde)        .then((r) => r.json());      logger.info('recibidos ' + items.length + ' items del cms');      for (const item of items) {        const data = await parseData({ id: item.slug, data: item });        store.set({ id: item.slug, data, digest: generateDigest(data) });      }      meta.set('ultima-sync', String(Date.now()));    },  };}

Dos piezas hacen a este loader apto para la escala. El digest es una huella del contenido de cada entrada: si Astro ve que el digest no cambió, se ahorra reprocesar esa entrada, así que un sitio con miles de páginas solo recalcula las que de verdad cambiaron. Y el meta, un pequeño almacén que persiste entre builds, permite guardar una marca de tiempo y pedir al CMS solo lo modificado desde la última sincronización. Juntos convierten un build ingenuo que se lo trae todo cada vez en uno incremental que toca lo mínimo, que es la única forma de que un sitio grande siga construyéndose en segundos.

i18n: el sitio en varios idiomas

Un sitio de contenido serio suele hablar más de un idioma, y Astro trae el enrutado internacional de fábrica. Se declara en la configuración: un defaultLocale, la lista de locales, y una estrategia de rutas que decide si el idioma por defecto lleva prefijo en la URL o no.

TypeScript
// astro.config.mjsimport { defineConfig } from 'astro/config';export default defineConfig({  i18n: {    defaultLocale: 'es',    locales: ['es', 'en', 'pt'],    routing: { prefixDefaultLocale: false },  },});

Con esto, /blog/hola sirve el español y /en/blog/hello el inglés, y Astro te da utilidades para saber en qué idioma estás y construir enlaces al equivalente en otros. La estrategia de contenido que mejor escala combina esto con el campo lang del esquema: cada entrada declara su idioma, y una ruta dinámica bajo [lang] filtra la colección por idioma para generar solo las páginas de cada locale. Así el modelo de datos y el enrutado hablan el mismo lenguaje, y añadir un idioma es traducir entradas, no reescribir plantillas.

Generar miles de páginas sin ahogar el build

La ruta dinámica es la máquina que convierte contenido en páginas. Un solo fichero con getStaticPaths recorre la colección entera y emite una página por entrada: es como esta misma guía fabrica cientos de lecciones desde una plantilla.

ASTRO
---// src/pages/[lang]/blog/[...slug].astroimport { getCollection, getEntry, render } from 'astro:content';export async function getStaticPaths() {  const posts = await getCollection('posts');  return posts.map((post) => ({    params: { lang: post.data.lang, slug: post.id },    props: { post },  }));}const { post } = Astro.props;const autor = await getEntry(post.data.autor);const { Content } = await render(post);---<h1>{post.data.title}</h1><p>por {autor.data.nombre}</p><Content />

A escala de decenas de miles de páginas, generarlas todas en el build tiene un límite: el tiempo. La estrategia madura no es horneral todo por sistema, sino repartir. Las páginas que importan por SEO y cambian poco —el grueso del contenido— se generan estáticas. La cola larga que casi nadie visita puede servirse bajo demanda con un adaptador, generándose en la primera visita y cacheándose después, de modo que el build no paga por millones de páginas que quizá nadie pida. Y dentro de una página mayormente estática, el trozo que sí es dinámico —un contador de vistas, una recomendación personalizada— se aísla en una server island que se renderiza aparte sin volver dinámica la página entera.