Páginas Dinámicas Astro v7.1+
Antes de comenzar la explicación es importantísimo recordar que en Astro Content Layer (v7.1+), cada archivo de una Colección de Datos mantiene un id que incluye la extensión del archivo (ejemplo: mi-post.md). Por ello, limpiamos la extensión mediante Javascript al definir las rutas dinámicas para generar URLs limpias sin .md.
En lugar de dedicarnos a enlazar archivo a archivo de nuestra colección, creamos un archivo de página dinámica al
cual llamaremos [id].astro y que nos permitirá renderizar cada archivo de una colección. Atendiendo a
esta filosofía, para poder acceder a cada uno de los post, se crea un archivo con el nombre [page].astro para hacer la portada del blog/tienda.
La estructura de las páginas con dos colecciones (posts y productos) para Blog y tienda, quedaría así:
src/
└── pages/
├── blog/
│ ├── [page].astro <-- Portada de Blog/Índice de páginas (blog/1, blog/2...)
│ └── [id].astro <-- Post individual
├── about.astro <-- ¿Quiénes somos?
├── contacto.astro <-- Página de contacto
├── index.astro <-- La página de inicio de la web
├── tienda/
│ ├── [page].astro <-- Portada de la tienda (Galería de productos)
│ └── [id].astro <-- Productos individuales
Archivo de página dinámica para renderizar elementos de la colección
Para renderizar el archivo sólo se necesita un archivo de página dinámica como éste:
---
import { getCollection, render } from 'astro:content';
import { Image } from 'astro:assets';
import DefaultLayout from '../../layouts/DefaultLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map(post => ({
// En Astro Content Layer, post.id incluye la extensión (ej. "mi-post.md").
// Sanitizamos el id eliminando la extensión para generar la URL limpia /blog/mi-post:
params: { id: post.id.replace(/\.[^/.]+$/, "") },
props: { post },
}));
}
const { post } = Astro.props;
// API oficial de Astro 5/6/7 para obtener el componente
const { Content } = await render(post);
---
<DefaultLayout title={post.data.title}>
<h1 class="text-3xl font-bold my-4">{post.data.title}</h1>
<p>{post.data.description}</p>
{post.data.image && (
<figure>
<Image
src={post.data.image.url}
alt={post.data.image.alt}
class="postImg"
width={800}
height={450}
/>
</figure>
)}
<hr />
<article class="prose">
<Content />
</article>
<p>
<a href="/blog"
class="inline-flex items-center text-sm text-gray-600 hover:text-black my-4">
Entradas del blog
</a>
</p>
</DefaultLayout>
Donde:
post.dataguarda todas las propiedades del Frontmatter de cada archivo (en el caso del ejemplo, los posts) validadas porZod.<Content/>en lugar de<slot />: En Astro v7.1.4, el método heredado post.render() está obsoleto/eliminado a favor de la función exportada render(post) desde astro:content, para obtener el componente especial<Content/>. Al renderizarlo, Astro inyecta ahí todo el contenido Markdown o HTML del archivo.- La estructura/estilo del archivo, se toman del
Layoutimportado.
Si en el futuro se decide cambiar el diseño de todos los archivos de una colección, se haces en un solo archivo
de Astro: ([id].astro) sin tocar un solo Markdown/HTML de la misma.
Archivo de página dinámica para la paginación (Portada de la Colección)
Para crear un índice con páginas numeradas usaremos un archivo dinámico que se va a llamar
[page].astro. Al llamar al archivo [page].astro (con el texto page
entre corchetes), Astro entiende que va a dividir la lista de elementos de la colección, en páginas.
El código de este archivo que genere el índice automáticamente, es el siguiente:
---
import { getCollection } from 'astro:content';
import DefaultLayout from '../../layouts/DefaultLayout.astro';
export async function getStaticPaths({ paginate }) {
const todosLosPosts = await getCollection('posts');
const postsOrdenados = todosLosPosts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
return paginate(postsOrdenados, { pageSize: 6 });
}
const { page } = Astro.props;
---
<DefaultLayout title={`Blog - Página ${page.currentPage}`}>
<h1>Blog sobre Gatitos</h1>
<div class="posts-grid">
{page.data.map((post) => {
// Nos aseguramos de enlazar a la URL limpia sin extensión .md/.mdx:
const cleanId = post.id.replace(/\.[^/.]+$/, "");
return (
<article class="post-card">
<h2>{post.data.title}</h2>
<p>{post.data.description}</p>
<a href={`/blog/${cleanId}`}>Leer más</a>
</article>
);
})}
</div>
<nav class="paginacion">
{page.url.prev ? (
<a href={page.url.prev}>← Anterior</a>
) : (
<span class="disabled">← Anterior</span>
)}
<span class="pagina-actual">Página {page.currentPage} de {page.lastPage}</span>
{page.url.next ? (
<a href={page.url.next}>Siguiente →</a>
) : (
<span class="disabled">Siguiente →</span>
)}
</nav>
</DefaultLayout>
<style>
.posts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; margin-bottom: 2rem; }
.post-card { border: 1px solid #eee; padding: 1.5rem; border-radius: 8px; }
.paginacion { display: flex; justify-content: space-between; align-items: center; margin-top: 3rem; }
.disabled { color: #ccc; cursor: not-allowed; }
</style>
pageSize es el límite de tarjetas (cards) de posts que se mostrarán en cada página del índice.
Si en una colección se tienen por ejemplo, 15 posts en total y se configura pageSize: 6,
Astro generará 3 Urls de forma automática:
- Página 1 (/blog/1): Mostrará los posts del 1 al 6 (6 cards).
- Página 2 (/blog/2): Mostrará los posts del 7 al 12 (6 cards).
- Página 3 (/blog/3): Mostrará los posts restantes del 13 al 15 (3 cards).
Astro calcula todo esto de manera automática. Solo con cambiar el número de pageSize a 9, 12,
etc..., se reestructurarán todas las páginas y los enlaces de Anterior y Siguiente al
compilar la web.