vendredi 25 septembre 2026

À propos

Contact

Headless & API

Pagination par curseur avec WPGraphQL : first, after et pageInfo

WPGraphQL pagine par curseur, pas par numéro de page. Comment ça fonctionne, et comment quand même afficher une pagination numérotée à l'utilisateur.

Par Clément Hadrot • 16 décembre 2021 • 5 min de lecture • Aucun commentaire
Pagination par curseur avec WPGraphQL : first, after et pageInfo

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 : true s’il existe des résultats après le curseur actuel.
  • hasPreviousPage : utile pour une pagination en arrière, avec les arguments last et before.
  • endCursor et startCursor : 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

L'essentiel à retenir : first et after remplacent page et offset ; pageInfo indique s'il reste des résultats ; Une pagination numérotée reste possible, avec des limites

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éFaisableLimite
Aller à la page suivante ou précédenteOuiAucune, c’est l’usage natif du curseur
Aller directement à une page jamais visitée (ex. page 8 depuis la page 1)Non fiableLe curseur de la page 8 n’existe que si les pages 2 à 7 ont été chargées avant
Afficher le nombre total de pagesPossibleNé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.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi