Astro: Genera rutas o páginas con archivos JSON

Entrada: Astro: Genera rutas o páginas con archivos JSON

En Astro puedes crear páginas automáticamente añadiendo un archivo .astro o .md dentro de la carpeta pages/. Pero, ¿qué hay de los archivos JSON?

Imagina que quieres guardar datos de un portafolio y/o una lista de elementos, no se siente bien guardarlos en archivos Markdown. Para nuestra suerte, Astro también nos da la posibilidad de convertir nuestros archivos JSON en páginas, y no solo ello, también nos permite «tipar» estos datos con Typescript.

Crear proyecto Astro

Para este ejemplo crearemos un portafolio. Iniciemos un proyecto de Astro:

npm create astro

Agrégale el nombre que desees, yo le pondré «astro-portafolio». Más adelante, cuando te pregunta si usarás Typescript, le dices que sí, y eliges el modo «Strict» (como todo un pro).

Uso de Content Collections

En la documentación nos explican que la mejor forma de administrar y crear contenido en un proyecto Astro es a través de las colecciones de contenido o content collections. Además, que nos da la posibilidad de tipar nuestros contenidos de Markdown, MDX, YAML o JSON. Si si, JSON.

Las Content Collections, no tienen mucha ciencia. Solo hay que crear una carpeta content/ dentro de src/ (en esta carpeta, Astro buscará las colecciones de contenido) y cada carpeta que agregues allí será una colección de contenido.

Vamos a crear 2 colecciones de contenido: proyectos/ y clientes/. Nuestro proyecto tomará una estructura similar a esta:

public/
src/
    content/
        clientes/
        proyectos/
    pages/
        index.astro
Code language: Bash (bash)

En una colección guardaremos nuestros proyectos para el portafolio, y en el otro, guardaremos a nuestros clientes. Más adelante las relacionaremos.

En la documentación también se recomienda pasar a Content Collections si tienes un blog o un proyecto donde tus archivos Markdown viven dentro de la carpeta pages/

Crea el archivo openai-website.json dentro de proyectos/ con la siguiente estructura:

{
  "title": "Sitio web de OpenAI",
  "slug": "sitio-web-de-openai",
  "url": "https://openai.com",
  "customer": "Sam Altman",
  "year": 2024
}Code language: JSON / JSON with Comments (json)

El «slug» nos servirá más adelante para crear una ruta donde se accederá a este contenido.

Crea un par de archivos más, por ejemplo, tesla-website.json y apple-website.json. Recuerda cambiar los datos y que todos compartan la misma estructura. En unas secciones después, le diremos a Astro como debe lucir la estructura de nuestros archivos y que nos avise si alguno no cumple los requisitos.

Luego, para en la colección clientes/ crea un JSON llamado sam-altman.json con la siguiente estructura:

{
  "name": "Sam Altman",
  "image": "sam-altman.jpg"
}Code language: JSON / JSON with Comments (json)

Para esta carpeta también crea otros 2 más, como elon-musk.json y steve-jobs.json. Descarga las imágenes correspondientes y agrégalas dentro de la carpeta public/.

public/
    elon-musk.jpg
    sam-altman.jpg
    steve-jobs.jpg
src/Code language: Bash (bash)

Por el momento, solo hemos estructurado el proyecto. Ahora aprenderemos a generar rutas dinámicas que son la clave para autogenerar las páginas a través de archivos JSON.

Uso de Dynamic Routes

Sabemos que al crear un archivo .astro dentro de pages/ se crea una ruta. La misión es lograr el mismo efecto pero con archivos .json, y para solucionar este problema, Astro nos ofrece las rutas dinámicas o dynamic routes. Aquí te explicaré lo básico de su funcionamiento, pero puedes ahondar mucho más en su documentación.

Básicamente, las rutas dinámicas se crean asignando parámatros entre corchetes en las carpetas y/o archivos dentro de pages/. Por ejemplo la ruta /pages/portafolio/[proyecto].astro recibe el parámetro proyecto y la ruta /pages/[idioma]/[manual].astro recibe los parámetros idioma y manual. Los valores que tomen estos parámetros le dicen a Astro que páginas debe crear.

¿Y como asignamos estos parámetros? En los archivos [proyecto].astro o [manual].astro debemos exportar una función getStaticPaths() que devuelve un arreglo de objetos con una propiedad params.

Veámoslo en la práctica. Crea la estructura /pages/portafolio/[proyecto].astro y agrega lo siguiente:

---
export function getStaticPaths() {
  return [
    {params: {proyecto: 'tesla'}},
    {params: {proyecto: 'openai'}},
    {params: {proyecto: 'apple'}},
  ];
}

// Accede al parámetro a través de Astro.params
const { proyecto } = Astro.params;
---

<h1>Proyecto: {proyecto}!</h1>
Code language: JavaScript (javascript)

Puedes ver que la propiedad params recibe un objeto donde van los parámetros que va a recibir la ruta dinámica. En nuestro caso, solo definimos el parámetro proyecto.

Esto debería generar las rutas /portafolio/tesla, /portafolio/openai y /portafolio/apple. Verifícalo en tu navegador.

Espera un momento. Diego, estas rutas no se generan con los JSON’s que me hiciste crear anteriormente. Eso lo arreglaremos en la siguiente sección.

Crear rutas con los Content Collections

Ahora que aprendiste a usar «content collections» y «dynamic routes» es momento de combinarlas y crear nuestro portafolio.

La misión es obtener los datos de la colección de contenido proyectos/ y generar rutas para cada JSON de su interior. Actualiza tu archivo [proyecto].astro:

---
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const proyectos = await getCollection('proyectos');
  console.log(proyectos);

  const arregloObjetos = proyectos.map((proyecto) => {
    return {
      params: {proyecto: proyecto.data.slug},
      props: {data: proyecto.data},
    }
  });

  return arregloObjetos;
}

// Accede a los props a través de Astro.props
const { data } = Astro.props;
---

<h1>Proyecto: {data.title}</h1>
<h3>Customer: {data.customer}</h3>
<h3>Fecha: {data.year}</h3>
<a href={data.url}>Link al proyecto</a>
Code language: JavaScript (javascript)

Antes de que te eches a llorar, déjame explicarte el código.

Línea 5: Usé la función getCollection() para obtener los datos de una determinada colección de contenidos, en este caso los contenidos de proyectos/ y como devuelve una promesa entonces agregamos async/await.

Línea 6: Agregué un console.log() para que veas en la consola de Node el retorno de getCollection(). Fíjate que nos devuelve una lista de objetos con id, collection y data. El collection nos dice a que colección pertenece el objeto, data nos ofrece todo el contenido del archivo JSON, y el id nos devuelve la RUTA relativa a la colección donde pertenece el archivo (el id lo usaremos al final para mejorar las rutas).

Línea 8-15: Recuerda que getStaticPaths() debe devolver una lista de objetos. Recorrimos todos los proyectos con la función map(), y esto retorna una lista de objetos con sus params (obligatorio) y props (opcional).

  • En params defines los parámetros que recibe la ruta dinámica. En nuestro caso es el parámetro proyecto, y le asigné el slug definido en los archivos JSON.
  • En props asigné los datos extraídos de los JSON’s que se usarán en cada ruta.

Genial. Ahora visita las rutas /portafolio/ agregando el «slug» que añadiste en cada archivo JSON. En mi caso uno de ellos sería /portafolio/sitio-web-de-openai.

Falta un detalle. Si te fijas, no estamos obteniendo los datos de la colección customers/, solo estamos usando los datos de portfolio/. En la siguiente sección vincularemos ambas colecciones.

Definir estructuras con Zod y vincular Content Collections

Astro nos da la posibilidad de definir la estructura de nuestras colecciones. Lo único que debemos hacer es crear un archivo config.ts dentro de /src/content/.

El archivo config.ts tiene la siguiente estructura:

import { defineCollection } from 'astro:content';

// Definimos las colecciones con "defineCollection"
const proyectoCollection = defineCollection({ /* ... */ });

// Exporta el objeto "collections" que vincula
// el nombre de la carpeta colección con su definición
export const collections = {
  'proyectos': proyectoCollection,
};
Code language: JavaScript (javascript)

Si ejecutas el proyecto, debería generarse un error porque aún no hemos definido el defineCollection(). Debemos enviarle un «esquema», que básicamente es la estructura que tomarán nuestros datos. Para definir esquemas de validación, existen muchas bibliotecas, una de ellas es Zod, y Astro ya lo tiene integrado.

Definamos los esquemas para proyectos/ y clientes/ en config.ts:

import { defineCollection, reference, z } from 'astro:content';

// Definir colección para proyectos/
const proyectosCollection = defineCollection({
  // 'data' es para definir contenidos JSON o YAML y 'content' es para contenidos Markdown
  type: 'data',
  // definición de esquema con Zod
  schema: z.object({
    title: z.string(),
    slug: z.string(),
    url: z.string().url(),
    customer: reference('clientes'),
    year: z.number(),
  })
});

// Definir colección para clientes/
const clientesCollection = defineCollection({
  type: 'data',
  schema: z.object({
    name: z.string(),
    image: z.string(),
  })
});

export const collections = {
  'proyectos': proyectosCollection,
  'clientes': clientesCollection,
};
Code language: JavaScript (javascript)

En la línea 12, estamos diciéndole a Astro que el campo customer: de un determinado proyecto debe hacer referencia a un elemento de la colección clientes/. Esto provocará un error, porque nuestros archivos JSON dentro de proyectos/ no hacen esta referencia. Pero no te preocupes, Astro nos explicará que estamos fallando.

En mi caso, me pide que agregue una referencia a un elemento de customers/. Yo le pase «Sam Altman» cuando debió ser «sam-altman».

Yo lo arreglaré mis archivos JSON para cumplir las demandas del esquema. Tu también has lo mismo.

Mi openai-website.json quedo así:

{
  "title": "Sitio web de OpenAI",
  "slug": "sitio-web-de-openai",
  "url": "https://openai.com",
  "customer": "sam-altman",
  "year": 2024
}
Code language: JSON / JSON with Comments (json)

De esta forma vinculamos nuestras colecciones y definimos su estructura.

Nota: En los esquemas puedes eliminar o agregar más campos, añadir valores por defecto, crear campos opcionales y más, gracias a Zod. Puedes visitar su documentación para más detalles.

Es momento de recuperar los datos de la colección customers/. Actualiza tu [proyecto].astro a lo siguiente:

---
import { getCollection, getEntry } from 'astro:content';

export async function getStaticPaths() {
  const proyectos = await getCollection('portfolio');

  const arregloObjetos = proyectos.map((proyecto) => {
    return {
      params: {proyecto: proyecto.data.slug},
      props: {data: proyecto.data},
    }
  });

  return arregloObjetos;
}

const { data } = Astro.props;

const customer = await getEntry(data.customer);
---

<h1>Proyecto: {data.title}</h1>
<h3>Customer: {customer.data.name}</h3>
<img src=`/${customer.data.image}` width="100">
<h3>Fecha: {data.year}</h3>
<a href={data.url}>Link al proyecto</a>
Code language: JavaScript (javascript)

Línea 19: Para obtener datos de una colección que hace referencia a otra colección debes usar getEntry().

Mira tus páginas con el path /portafolio/ mas el slug que agregaste a cada uno, todas se generaron a través de archivos JSON y puedes agregar más archivos y automáticamente tendrás nuevas páginas.

Puedes felicitarte, ¡lo lograste! Y si deseas mejorar las rutas para que no escribas manualmente un slug en cada JSON puedes leer la siguiente sección.

Mejorar las rutas

Cuando agregas un archivo .astro en pages/ se genera la ruta nueva usando el nombre del archivo. Hagamos lo mismo con nuestros archivos JSON, para no tener que escribir un campo «slug» en cada uno de ellos.

Elimina el campo slug: de los JSON que se encuentran en proyectos/:

{
  "title": "Sitio web de OpenAI",
  "slug": "sitio-web-de-openai", // <-- Elimina esto
  "url": "https://openai.com",
  "customer": "sam-altman",
  "year": 2024
}
Code language: JSON / JSON with Comments (json)

Recuerda que Astro se quejará porque nuestros archivos no cumplen con la esquema que definimos en content/config.ts. Elimina la definición de «slug» del esquema:

const proyectosCollection = defineCollection({
  type: 'data',
  schema: z.object({
    title: z.string(),
    slug: z.string(), // <-- Elimina esto
    url: z.string().url(),
    customer: reference('clientes'),
    year: z.number(),
  })
});
Code language: JavaScript (javascript)

También usábamos el slug en [proyecto].astro, por ello seguiremos teniendo error. Vamos a asignarle el valor proyecto.id al parámetro proyecto:

export async function getStaticPaths() {
  const proyectos = await getCollection('portfolio');

  const arregloObjetos = proyectos.map((proyecto) => {
    return {
      params: {proyecto: proyecto.id},
      props: {data: proyecto.data},
    }
  });

  return arregloObjetos;
}
Code language: JavaScript (javascript)

Recuerda que el id es una ruta, en este caso solo devuelve el nombre de los archivos: apple-website, openai-website y tesla-website porque son rutas relativas a la carpeta content/. Como resultado, esto generará las siguientes rutas en tu navegador /portafolio/apple-website, /portafolio/openai-website y /portafolio/tesla-website.

Como te decía, el id nos devuelve una ruta. ¿Qué pasaría si creas un subdirectorio dentro de la colección proyectos/?

content/
    clientes/
        ...
    proyectos/
        apps/
            apple-music.json
        openai-website.json
        spacex-website.json
        tesla-website.json

¿Cuál sería el id de apple-music.json?

Bueno, el ID sería la ruta app/apple-music.

El problema es que los params: definidos en getStaticPaths() no saben procesar rutas. Esto generará un error, ya que hasta el momento solo estaba recibiendo el nombre de los archivos. Para arreglarlo, debemos convertir nuestro parámetro proyecto a un «parametro rest«. No es complicado, solo debemos cambiar el nombre del archivo [proyecto].astro a [...proyecto].astro

Ahora puedes acceder a /portafolio/apps/apple-music.

¡Y con esto terminamos! Espero esta guía haya sido de tu agrado.

Referencias

Compartir:
Suscríbete para recibir artículos de desarrollo web

En este boletín envío correos relacionados al área del diseño y desarrollo web. Escribo sobre WordPress y AstroJS, a veces un poco de UX/UI.

2 Responses

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Astro: Crea un Slider de imágenes con Lightbox