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.
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].astroimport { 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.
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/2src/pages/blog/[...page].astro pagina 1 -> /blog (param indefinido) pagina 2 -> /blog/2La 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.
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].astroimport { 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.
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.