La première fois que j’ai voulu ajouter une pagination classique — « page 1, page 2, page 3 » avec numéros cliquables — à un front consommant WPGraphQL, j’ai cherché en vain un paramètre page dans le schéma. WPGraphQL suit la spécification de pagination Relay, basée sur des curseurs opaques (first, after, last, before), un modèle très différent de la pagination par numéro de page de l’API REST WordPress.
Cet article explique le fonctionnement de cette pagination par curseur, comment l’implémenter pour un défilement classique ou un chargement progressif, et comment (avec quelques limites) reconstruire une pagination numérotée si le projet l’exige. Il ne couvre pas la pagination de l’API REST, dont le mécanisme (page, per_page, en-tête X-WP-TotalPages) est entièrement différent.
Le principe du curseur
Plutôt que de demander « la page 3 », une requête Relay demande « les 10 éléments suivants après tel curseur ». Le curseur est une chaîne opaque encodée par le serveur, qui représente la position exacte dans le jeu de résultats, sans qu’aucune signification particulière ne doive lui être prêtée côté client.
query ArticlesPage($apres: String) {
posts(first: 10, after: $apres) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
title
slug
}
}
}
}
La première requête omet la variable apres (ou la passe à null) pour récupérer les dix premiers résultats. La requête suivante réutilise endCursor, renvoyé dans pageInfo, comme valeur du paramètre after, pour avancer d’une page.
Lire pageInfo correctement
L’objet pageInfo contient les informations nécessaires pour savoir s’il reste des résultats à charger, sans avoir à connaître le nombre total d’éléments :
hasNextPage:trues’il existe des résultats après le curseur actuel.hasPreviousPage: utile pour une pagination en arrière, avec les argumentslastetbefore.endCursoretstartCursor: les curseurs à réutiliser pour la requête suivante ou précédente.
const { hasNextPage, endCursor } = data.posts.pageInfo;
if (hasNextPage) {
const suite = await client.request(REQUETE, { apres: endCursor });
}
Implémenter un défilement infini

Ce modèle de pagination correspond naturellement à un défilement infini ou à un bouton « Charger plus », plutôt qu’à une pagination numérotée classique :
function useArticlesInfinis() {
const [articles, setArticles] = useState([]);
const [curseur, setCurseur] = useState(null);
const [aSuite, setASuite] = useState(true);
async function chargerPlus() {
const { posts } = await client.request(REQUETE, { apres: curseur });
setArticles((prec) => [...prec, ...posts.edges.map((e) => e.node)]);
setCurseur(posts.pageInfo.endCursor);
setASuite(posts.pageInfo.hasNextPage);
}
return { articles, chargerPlus, aSuite };
}
Reconstruire une pagination numérotée
Si le projet impose une pagination numérotée (contrainte de design ou d’accessibilité), c’est possible mais avec un coût : il faut soit stocker les curseurs de chaque page visitée dans un tableau côté front au fur et à mesure de la navigation, soit passer par un identifiant de décalage approximatif en combinant first avec un tri stable garanti (par exemple sur date puis id en cas d’égalité).
const curseursParPage = [null]; // page 1 : pas de curseur
async function allerAPage(numero) {
if (curseursParPage[numero - 1] === undefined) {
throw new Error('Cette page nécessite d\'avoir chargé les précédentes.');
}
const { posts } = await client.request(REQUETE, {
apres: curseursParPage[numero - 1],
});
curseursParPage[numero] = posts.pageInfo.endCursor;
return posts.edges.map((e) => e.node);
}
Limites de cette reconstruction
| Fonctionnalité | Faisable | Limite |
|---|---|---|
| Aller à la page suivante ou précédente | Oui | Aucune, c’est l’usage natif du curseur |
| Aller directement à une page jamais visitée (ex. page 8 depuis la page 1) | Non fiable | Le curseur de la page 8 n’existe que si les pages 2 à 7 ont été chargées avant |
| Afficher le nombre total de pages | Possible | Nécessite une requête séparée sur un champ de comptage si le schéma l’expose |
Sur un projet où le client tenait absolument à une pagination numérotée avec accès direct à n’importe quelle page, j’ai fini par proposer un compromis : une pagination numérotée limitée aux pages déjà visitées ou immédiatement adjacentes, avec un champ de recherche pour atteindre un contenu précis plutôt qu’une page arbitraire lointaine.
La pagination par curseur n’est pas une limitation de WPGraphQL, c’est un choix délibéré hérité de la spécification Relay : elle reste fiable même quand des éléments sont ajoutés ou supprimés pendant la navigation, ce qu’une pagination par numéro de page classique gère beaucoup moins bien.
En résumé
WPGraphQL pagine exclusivement par curseur, via first, after et l’objet pageInfo, un modèle pensé pour le défilement infini plutôt que pour une pagination numérotée classique. Reconstruire une pagination par numéro reste possible en stockant les curseurs des pages déjà visitées, mais l’accès direct à une page lointaine jamais chargée reste une limite structurelle du modèle, à anticiper dès la conception de l’interface.