Organiza tu Contenido con Colecciones Anidadas — Cómo usar colecciones anidadas en un proyecto real con Astro
Cómo usar colecciones anidadas en un proyecto real con Astro
Ya sabemos cómo trabajar con colecciones tanto a nivel de Content Island como a nivel de código,así que vamos a ver como quedaría en un proyecto de ejemplo con Astro.
Este proyecto lo podemos encontrar en el repositorio:
https://github.com/content-island/collection-example-astro
Setup proyecto
Vamos a clonarnos el repo y ver como funciona:
git clone https://github.com/content-island/collection-example-astro.gitVamos a instalar las dependencias:
npm installY antes de arrancarlo vamos a crear un archivo .env con el siguiente contenido:
CONTENT_ISLAND_SECRET_TOKEN=a2bb1def2090a842ab3e331e33693e2cAhora ya podemos arrancar el proyecto:
npm run devAquí podemos ver una página con el listaado de los cursos.

Si pinchamos en uno de ellos, navegamos a la página del curso, donde podemos ver el video de le lección actual, un listado de las lecciones del curso y una descripción del curso.

Estructura
Sobre la estructura del proyecto, debajo de la carpeta src tenemos código fuente de nuestra aplicacion, y:
📁 collection-example-astro/
└── 📁 src/
├── 📁 api/
├── 📁 components/
├── 📁 layouts/
├── 📁 lib/
├── 📁 pages/
└── 📁 styles/Debajo de la carpeta api hemos definido el modelo de datos que consumimos desde Content Island, así como los métodos para cargar cursos y lecciones.
En la carpeta components tenemos los componentes que usamos en las páginas, como el componente de listado de lecciones.
En la carpeta layouts, tenemos un layout que usamos en todas las páginas, y que nos permite definir un header y un footer.
En lib, la inicializacíon del cliente de Content Island, que es el que nos permite cargar los cursos y lecciones.
Debajo de la carpeta pages tenemos las páginas de nuestra aplicación:
Si te fijas sólo tenemos los ficheros astro de nuestros páginas, no es buena idea tener ahí subcomponentes porque Astro se puede liar pensando que son páginas.
Tenemos la página index.astro que es la página de listado de cursos.
Y la página de trainings, fijate que definimos una ruta dinámica con [trainingSlug].astro que se corresponde con el campo slug de cada cursos, y [lessonSlug] que se corresponde con el campo slug de cada lección.
Página listado de cursos
Vamos a ver como está esto implementado página por página, empezamos por la página que muestra el listado de cursos.
Abrimos src/pages/index.astro ¿Qué tenemos aquí?
Lo primero el código entre "rejas", en Astro nos permite definir código que se ejecuta en el servidor, y que nos permite cargar los cursos desde Content Island.
---
import Layout from "@/layouts/layout.astro";
import TrainingCard from "@/components/trainings/training-card.astro";
import { getTrainings } from "@/api";
const trainings = await getTrainings();
---- Importamos el layout de la página.
- Importamos el componente de cards de cursos.
- Importamos el método de la api getTrainigs.
- Invocamos al método getTrainigs de la api para cargar los cursos.
api
Vamos a bucear en ese método getTrainings
¿Qué tenemos aquí?
Por un lado leemos la lista de trainings.
Por otro iteramos y cargamos las lecciones de cada training.
export async function getTrainings(): Promise<TrainingWithLessons[]> {
const trainings = await client.getContentList<Training>({
contentType: "training",
});
// For each training, fetch its lessons and return the enriched object
const trainingsWithLessons = await Promise.all(
trainings.map(async (training) => {
// Fetch the lesson objects using the stored lesson IDs
const lessons = await getLessons(training.lessons ?? []);
// Return a new object that includes full lesson data
return {
...training,
lessons,
};
})
);
// Return the final list of trainings with full lessons
return trainingsWithLessons;
}¿Por qué en la lista de trainings nos hacen falta las lecciones? Porque cuando pinchemos en un curso tenemos que navegar a la página del curso e indicar la primera lección que queremos mostrar.
Esto se podría optimizar, por ejemplo, cargando sólo los datos de la primera lección, o indicando que si no hay slug en la página de lección cargar la primera lección del curso.
Si te fijas, en getLessons cargamos las lecciones de cada curso, y devolvemos un objeto que incluye los datos completos de las lecciones.
export async function getLessons(ids: string[]): Promise<Lesson[]> {
return await client.getContentList<Lesson>({
contentType: "Lesson",
id: { in: ids },
});
}Sobre el modelo de datos, lo hemos copiado de Content Island, lo puedes encontrar en:
./src/api/models.ts
UI
Volvamos a index.astro, y ahora fijate que ya tenemos el markup.
Hay un componente layout que se encarga de renderizar el header y el footer, vamos a ver que pinta tiene:
- Por un lado imports globals:
- ClientRouter de Astro, que nos permite navegar entre páginas sin recargar.
- Estilos globales de Tailwind y de Highlight.js (para tener syntax highlighting en código).
- Y nos traemos un componente de cabecera.
- Fijate que aceptamos Props esto es parecido a los props de React, y nos permite pasarle un título a la página.
- Y por último, el slot, que es donde se renderizará el contenido de la página, que lo ponemos entre el header y, si tuvieramos el footer,
./src/layouts/layout.astro
---
import { ClientRouter } from "astro:transitions";
// Nos traemos también los estilos globales (tailwind)
import "../styles/global.css";
import 'highlight.js/styles/github-dark.css';
import Header from "@/components/header.astro";
interface Props {
title: string;
}
const { title } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<ClientRouter />
</head>
<body>
<Header />
<slot />
</body>
</html>Y volvemos a la página de listado y lo siguiente que tenemos es que iteramos sobre la lista de cursos y renderizamos un componente de card para cada uno de ellos.
Si nos vamos al componente card fijate que le pasamos por props los datos del curso, y que renderizamos el título, la imagen y un enlace a la página del curso.
src/components/trainings/training-card.astro
Y aquí lo mismo, maquetamos la tarjeta del curso y lo enlazamos a la página del curso.
Página de detalle de curso
Si te fijas en el nombre de carpeta y fichero de esta página tenemos dos parametros dinámicos, - [trainingSlug]: Se correspone con el campo slug de cada curso, es decir el fragmento de url amistosa que identifica a cada curso.
- _[lessonSlug]: que nos permiten cargar el curso y la lección que queremos mostrar.
📁 pages/
└── 📁 training/
└── 📁 [trainingSlug]/
└── 📄 [lessonSlug].astro¿Cómo se gestiona esto en Astro? Pues en la propia página hay un método que se llama getStaticPaths que, si eres fan del universo Marvel, hace las veces de Dr. Strange en infinity Wars, calcula todas las posibles rutas que puede haber en la aplicación para cada curso y lección.
export async function getStaticPaths() {
const trainings = await getTrainings();
return trainings.flatMap((training) =>
training.lessons.map((lesson) => ({
params: {
trainingSlug: training.slug,
lessonSlug: lesson.slug,
},
props: {
training,
},
})),
);
}Aquí si te fijas tiras de la misma api que hemos usado para cargar los cursos, y para cada curso y lección generamos una ruta:
En params se devuelve cada slug (el de curso y lección).
En props le pasamos ya a cada página los datos para que se pueda renderizar, en este caso cada curso con sus lecciones.
Ya seguimos con el código que aplica a lo que es la página en sí, y vemos que leemos la información de training y del slug sacamos la lección actual para saber cual queremos mostrar.
Y en el markup, usamos el layout como en la páginas de cursos, y armamos el contenido de la página, tirando de subcomponentes:
- El video de la lección.
- La lista de lecciones.
- Y la descripción del curso.
Puedes ver el código de cada componente pinchando sobre el mismo con el botón derecho y eligiendo "Go to Definition".
Referenciar colecciones de objetos facilita la gestión del contenido. Astro y Content Island permiten integrarlo de forma dinámica y sencilla.