Paginación con paginate()
Una colección de doscientas entradas no cabe en una sola página. La función paginate, que Astro entrega dentro de getStaticPaths, trocea un array en páginas numeradas y genera de golpe todas las rutas que las materializan. Cómo se invoca, qué devuelve, cómo el tamaño de página decide cuántas rutas nacen y por qué el fichero debe llevar un segmento page.
Listar contenido tiene un límite físico: nadie quiere una página con doscientos artículos, ni un HTML de varios megas para leer los diez primeros. La paginación es la respuesta clásica —trocear un conjunto grande en páginas manejables y numeradas—, y en un sitio estático plantea un reto peculiar: cada una de esas páginas debe existir como una ruta real, horneada en el build. Astro resuelve las dos mitades del problema con una sola herramienta, paginate, una función que recibes dentro de getStaticPaths y que, a partir de un array y un tamaño, te devuelve ya construida la lista completa de rutas paginadas. No calculas los cortes ni fabricas las URLs a mano: describes el conjunto y el tamaño, y Astro deriva cuántas páginas hacen falta y qué le toca a cada una.
- Recibir
paginatecomo propiedad del argumento degetStaticPaths. - Trocear una colección pasándola a
paginatejunto a unpageSize. - Nombrar el fichero con un segmento
[page]que capture el número de página. - Generar rutas paginadas por grupos pasando
paramsypropsadicionales.
paginate: la función que llega en el argumento
Hasta ahora getStaticPaths era una función que tú definías y que devolvía un array de objetos con params. La paginación añade un matiz: getStaticPaths recibe un argumento, un objeto del que puedes extraer paginate. Esa función hace el trabajo pesado —calcular los cortes, numerar las páginas, adjuntar los datos de cada tramo— y devuelve exactamente el array de rutas que getStaticPaths debe retornar.
---
// src/pages/blog/[page].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog');
return paginate(posts, { pageSize: 10 });
}
const { page } = Astro.props;
---
<h1>Blog, página {page.currentPage}</h1>
<ul>
{page.data.map((post) => <li>{post.data.title}</li>)}
</ul>
Fíjate en la firma: getStaticPaths desestructura paginate del objeto que Astro le pasa. La llamada paginate(posts, { pageSize: 10 }) toma dos cosas —el array completo de entradas y un objeto de opciones donde pageSize fija cuántas caben en cada página— y devuelve un array donde cada elemento es una ruta lista: lleva su params con el número de página y, dentro de props, un objeto page con la rebanada de datos correspondiente y toda la metainformación de navegación. Ese page es el que lees luego en Astro.props.
La aritmética la resuelve Astro, y conviene tenerla presente. Con doscientas entradas y un pageSize de diez, nacen veinte rutas; con veinticinco entradas y el mismo tamaño, nacen tres —diez, diez y cinco—, porque la última página recoge el resto aunque no llene el cupo. El número de páginas no lo escribes: lo dicta la división entre el total de datos y el tamaño que pediste, redondeando hacia arriba.
A menudo querrás ordenar o filtrar antes de trocear, y el sitio para hacerlo es justo antes de llamar a paginate, sobre el array completo:
---
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog');
const publicados = posts
.filter((p) => !p.data.borrador)
.sort((a, b) => b.data.fecha.valueOf() - a.data.fecha.valueOf());
return paginate(publicados, { pageSize: 10 });
}
---
Así la paginación opera sobre datos ya limpios y ordenados: descartas los borradores y colocas lo más reciente primero antes de repartir en páginas, de modo que la página uno contenga de verdad lo último publicado. Todo lo que quieras que sea cierto de la serie —el orden, qué entra, qué se excluye— se decide aquí, aguas arriba del troceo.
No confundas los papeles. Tú sigues siendo quien exporta getStaticPaths y quien retorna su valor; lo que cambia es que ese valor ya no lo construyes a mano, sino que se lo pides a paginate. La función es una fábrica de rutas: le entregas datos y un tamaño, y te devuelve el array de objetos con params y props en la forma exacta que getStaticPaths espera. Puedes incluso transformar o filtrar la colección antes de pasarla —ordenar por fecha, descartar borradores— y paginar el resultado ya limpio.
El nombre del fichero: [page] y dónde vive la primera página
La paginación necesita un lugar donde poner el número de página, y ese lugar es un segmento dinámico en el nombre del fichero. Un src/pages/blog/[page].astro genera /blog/1, /blog/2, /blog/3 y así hasta la última. El corchete [page] captura el número, y paginate se encarga de rellenar ese parámetro por ti en cada ruta que fabrica.
Aquí surge una decisión de diseño con una solución elegante. Con [page], la primera página vive en /blog/1, y muchos preferimos que la primera esté en la raíz de la sección —/blog a secas— y que la numeración empiece a verse a partir de la segunda. Para eso se usa un segmento rest opcional: al nombrar el fichero src/pages/blog/[...page].astro, la primera página recibe el parámetro como indefinido y se materializa en /blog, mientras que /blog/2, /blog/3 y las siguientes conservan su número.
src/pages/blog/[page].astro
pagina 1 -> /blog/1
pagina 2 -> /blog/2
src/pages/blog/[...page].astro
pagina 1 -> /blog (param indefinido)
pagina 2 -> /blog/2
La elección entre uno y otro es de gusto y de URLs, no de mecánica: paginate funciona igual en ambos casos, y lo único que cambia es dónde aterriza la primera página. La forma con [...page] produce direcciones más limpias para la portada de la sección y es la que verás en la mayoría de blogs cuidados; la forma con [page] es más explícita y a veces más cómoda si toda la navegación se construye con números.
Como en cualquier ruta dinámica, el nombre entre corchetes es el contrato. paginate rellena por defecto un parámetro llamado page, así que el fichero debe ser [page].astro o [...page].astro. Si nombras el corchete de otra forma sin decírselo a paginate, la ruta no casa y el build no genera nada útil. Cuando pagines dentro de una estructura con más parámetros —lo veremos enseguida—, tendrás que ser explícito sobre qué segmento es el de la página.
Paginar por grupos: params y props adicionales
El caso simple pagina una colección entera, pero a menudo quieres paginar dentro de una división: las entradas de cada categoría, los artículos de cada autor, los productos de cada familia. Eso significa cruzar dos ejes —el grupo y el número de página— y paginate lo admite aceptando params y props extra en su segundo argumento, que se suman a los que la propia función genera.
---
// src/pages/[categoria]/[page].astro
import { getCollection } from 'astro:content';
export async function getStaticPaths({ paginate }) {
const posts = await getCollection('blog');
const categorias = [...new Set(posts.map((p) => p.data.categoria))];
return categorias.flatMap((categoria) => {
const deCategoria = posts.filter((p) => p.data.categoria === categoria);
return paginate(deCategoria, {
params: { categoria },
pageSize: 10,
});
});
}
---
El patrón es un flatMap sobre los grupos. Por cada categoría filtras sus entradas, las paginas, y le pasas a paginate un params con la categoría para que ese valor quede fijo en todas las rutas de ese grupo. El resultado es un array de arrays aplanado: /tecnologia/1, /tecnologia/2, /diseno/1, y así por cada combinación de grupo y página. Astro rellena el [page] y tú rellenas el [categoria]; entre los dos completáis la URL.
flowchart TD A[coleccion de 25 entradas] --> P[paginate con pageSize 10] P --> R[array de rutas con params page] R --> P1[pagina 1 con 10 entradas] R --> P2[pagina 2 con 10 entradas] R --> P3[pagina 3 con 5 entradas] style A fill:#89b4fa,color:#11111b style P1 fill:#a6e3a1,color:#11111b style P2 fill:#a6e3a1,color:#11111b style P3 fill:#a6e3a1,color:#11111b
También puedes adjuntar props estáticas a cada página del grupo —el nombre legible de la categoría, su descripción— para no recalcularlas en el cuerpo del componente. Como con cualquier getStaticPaths, todo lo que pasa por params y props se congela en el build: la paginación por grupos no ejecuta nada en tiempo de visita, solo hornea de antemano tantas rutas como grupos por páginas haya.
El tamaño de página no es un número mágico. Uno muy grande devuelve el beneficio de paginar —vuelves a servir HTML pesados y listas interminables—; uno muy pequeño multiplica las rutas y obliga al lector a saltar sin parar. Piensa el pageSize en función de cuánto pesa cada entrada renderizada y de cuántas caben antes de que la página canse: una tarjeta rica pide páginas cortas, una línea de índice admite muchas. Y recuerda que en estático cada página es un fichero horneado, así que un pageSize diminuto sobre una colección enorme infla el build sin dar nada a cambio.
Trocear sin aritmetica
paginate calcula los cortes y numera las paginas. Tu das el array y el pageSize, no los indices.
pageSize manda
El tamano de pagina decide cuantas rutas nacen: total entre pageSize, redondeado hacia arriba.
La primera pagina
Con corchete page vive en barra 1; con rest opcional vive en la raiz de la seccion.
Paginar por grupos
Un flatMap sobre las categorias, un paginate por grupo con su params fijo, y salen todas las rutas.
Vale la pena ver paginate como algo más profundo que un ayudante de cómodo. Lo que hace es tomar una estructura de datos de una dimensión —una lista— y proyectarla sobre el espacio de las URLs siguiendo una regla precisa: agrupa los elementos en bloques de tamaño fijo y asigna a cada bloque una dirección numerada. Es, en el fondo, una función total del contenido a las rutas, y esa totalidad es la que te libera de un trabajo tedioso y propenso a errores. Piensa en lo que tendrías que orquestar a mano: calcular cuántas páginas caben, cortar el array por los índices correctos, generar cada URL, adjuntar a cada una su rebanada de datos y su metainformación de vecinos, y mantener todo eso sincronizado cuando el contenido crece o mengua. Cada uno de esos pasos es una oportunidad de fallar por un desajuste de más o de menos —el clásico error de contar mal el último elemento—. paginate colapsa todo ese cálculo en una declaración: estos datos, en bloques de este tamaño. Y como se ejecuta en el build sobre el conjunto ya conocido, el resultado es un universo de rutas cerrado y verificable: sabes exactamente cuántas páginas existen, cuál es la última, qué contiene cada una, antes de que nadie visite el sitio. Esta es la misma filosofía que atraviesa todo el enrutado estático de Astro llevada a un caso concreto y frecuente: en lugar de resolver la paginación en vivo, petición a petición, como haría un servidor tradicional que consulta una base de datos con desplazamiento y límite, la resuelves una vez, de golpe, materializando cada página como un fichero plano. Cambias la flexibilidad de recalcular al vuelo por la certeza y la velocidad de tenerlo todo horneado. paginate no es solo azúcar sintáctico; es la encarnación de una postura sobre cuándo pagar el trabajo de organizar tu contenido, y esa postura es la que hace que un blog estático con miles de entradas se sirva tan rápido como uno con diez.
- Crea
src/pages/blog/[page].astrocon ungetStaticPathsque recibapaginatey devuelvapaginate(posts, { pageSize: 5 })sobre una colección; visita/blog/1y/blog/2. - Cambia el fichero a
[...page].astroy comprueba que la primera página pasa a vivir en/blogmientras la segunda sigue en/blog/2. - Ajusta el
pageSizey observa cómo el número de rutas generadas cambia según el total de entradas dividido por el tamaño. - Monta una paginación por grupos en
src/pages/[categoria]/[page].astrocon unflatMapque pagine cada categoría pasando suparams.