Pagination Hugo et sections imbriquées : la page 2 que personne n'a demandée
Ou : Comment j’ai écrit un script pour effacer des pages que je commandais moi-même
En avril 2026, avec Hugo 0.160, mon site a commencé à publier des pages sans raison d’être : des /page/2/ et des /page/3/ sous des sections imbriquées qui n’avaient rien à paginer, avec le même contenu que la section et « Page 2 » ajouté au titre. J’ai cherché une option pour couper la pagination section par section. Il n’y en a pas. J’ai donc écrit un script qui efface ces dossiers après chaque construction du site, et il tourne encore. Six mois plus tard, j’ai refait l’essai sur un site vierge avec Hugo 0.168 pour écrire cet article. J’avais raison sur l’option. J’avais tort sur le coupable.
Une page 2 qui répète la page 1, c’est du contenu dupliqué servi aux robots, des adresses en plus dans l’exploration, et un titre qui ment. Mon crawler SEO écrit en Go les voit dès qu’un build oublie le script. Il fallait comprendre d’où elles venaient.
Pourquoi Hugo génère des pages /page/N/ dans les sections imbriquées
Hugo ne pagine que ce qu’on lui demande
La documentation est nette : pour paginer une page d’accueil, une section, une taxonomie ou un terme, le gabarit doit appeler .Paginate ou .Paginator1. Pas d’appel, pas de pages /page/N/. Or mon gabarit de liste commençait par ceci :
| |
L’intention était innocente : écrire « (Page 2) » dans le titre de la deuxième page d’une liste. Le problème, c’est que ce gabarit sert à toutes les sections qui se trouvent dessous. À chaque section rendue, .Paginator découpe ses pages en paquets de pagerSize, et dès qu’il y en a plus que ce nombre, Hugo écrit une /page/2/, plus une /page/1/ qui redirige vers la section. C’est un photocopieur réglé pour numéroter les pages : il numérote aussi les cartes de visite.
Sur un site de test avec une section /docs/, deux niveaux de sous-sections (/docs/reseau/ et /docs/reseau/dns/), douze pages par niveau et pagerSize = 8, Hugo 0.168 produit exactement ce que j’avais en production :
| |
Six fichiers, dont quatre que personne n’a voulus.
Les options essayées, et pourquoi aucune ne suffit
En avril, j’ai tenté trois réglages : une cascade depuis hugo.toml qui fixe un pagerSize énorme pour les sous-sections, un bloc pagination dans le front matter du _index.md d’une sous-section, et l’ancienne clé paginate. Sur le site de test et Hugo 0.168, aucun ne change un seul fichier, à aucun niveau, même avec la forme de cible que la documentation recommande pour une section et tous ses descendants2 :
| |
Résultat : les mêmes six fichiers. L’explication tient en deux lignes de documentation. La pagination est un réglage du site, ou d’une langue, pas un attribut de page : elle ne figure pas dans la liste des champs du front matter3, et la cascade ne transporte que du front matter. Quant à la clé paginate, Hugo 0.156 l’a retirée de la configuration en février 2026, au profit de pagination.pagerSize4. Reste une nuance : .Paginator ne permet pas de changer la taille des paquets, seul .Paginate accepte un nombre en second argument1.
Donc oui, aucune option ne suffit. Hugo n’a pas d’interrupteur par section. Il a un gabarit qui demande, ou pas.
Le script qui efface les symptômes
Faute d’interrupteur, j’ai pris un balai. Voici le principe, ramené au site de test :
| |
-mindepth 2 épargne docs/page/, la seule pagination légitime, et emporte celles des sous-sections. Ça marche : sur mon site, une centaine de fichiers HTML disparaissent à chaque construction. Mais chaque construction doit passer par là, en local comme sur le serveur, et le jour où l’une l’oublie, les pages fantômes reviennent.
Le commentaire en tête de mon script citait même une source : l’issue #7609 du dépôt de Hugo, présentée comme la preuve d’une « limitation confirmée ». Je l’ai relue pour cet article. Elle date d’août 2020, elle parle d’un .Paginate qui renvoie une liste vide, et Bjørn Erik Pedersen l’a fermée en vingt minutes avec cette réponse : il faut appeler .Paginate ou .Paginator, pas les deux, car le premier l’emporte5. La source censée prouver que Hugo était en faute désignait le gabarit.
DevOps Dave : J’ai un script qui supprime les pages en trop après chaque build. Il tourne depuis six mois sans un raté.
Security Sarah : Qui les crée, ces pages ?
DevOps Dave : Hugo. C’est un bogue connu, j’ai mis le numéro de l’issue en commentaire.
Security Sarah : Tu l’as lue, l’issue ?
DevOps Dave : Je l’ai citée. C’est presque pareil.
La correction tient dans une condition
Puisque Hugo pagine ce que le gabarit lui demande, il suffit de ne demander qu’au bon endroit :
| |
Sur le site de test, il ne reste que docs/page/1/ et docs/page/2/. Aucun script, aucune suppression après coup. Si une sous-section doit malgré tout passer par la pagination, .Paginate .Pages 1000 la ramène à une seule page, mais Hugo écrit encore l’alias /page/1/. Il faut alors disableAliases = true dans le bloc [pagination] du site, ce qui vaut pour tout le site : avec les deux réglages, plus aucun dossier page sous les sous-sections.
Un piège voisin mérite d’être connu. La documentation précise que le premier appel à la pagination est mis en cache, et que les appels suivants réutilisent ce résultat sans tenir compte de leurs arguments1. Un .Paginator placé en tête de gabarit, juste pour lire un numéro de page, fige donc la collection paginée : un .Paginate plus bas, avec un autre tri ou un autre filtre, sera ignoré sans message d’erreur. C’est exactement l’histoire de l’issue #7609, et c’est aussi la ligne qui ouvrait mon gabarit.
Depuis le 25 octobre, la condition est en place sur mon site. Construit sans le script, il produit exactement les mêmes fichiers que l’ancienne construction suivie du script, à l’octet près : les 33 dossiers page en trop ne sont plus effacés après coup, ils ne sont plus créés. Le script tourne encore, par habitude, et ne trouve plus rien à supprimer. Le diagnostic d’avril tenait en deux affirmations : « aucune option ne suffit » et « c’est un bogue de Hugo 0.160 ». La première était juste. La seconde décrivait un logiciel qui, pendant six mois, à chaque construction, a fait exactement ce que je lui demandais, une ligne au-dessus de mon titre.
Ce n’est d’ailleurs pas la première fois que Hugo me surprend en faisant exactement ce qui est écrit. Ni la dernière fois qu’un outil fait son travail à la lettre : c’est aussi ce qui permet de publier un article par jour avec une simple date future.
Documentation de Hugo, Pagination : méthodes
PaginateetPaginator, taille des paquets, et mise en cache du premier appel (« Regardless of pagination method, the initial invocation is cached and cannot be changed »). ↩︎ ↩︎ ↩︎Documentation de Hugo, Cascade, cible
{/articles,/articles/**}pour une section et ses descendants. En avril, j’avais noté que ma cascade agissait au premier niveau et s’arrêtait au second. Je n’ai pas gardé Hugo 0.160 pour refaire la mesure ; avec 0.168, elle n’agit sur la pagination à aucun niveau. Au passage, Hugo 0.166 (septembre 2026) a réécrit son moteur de motifs, utilisé par les cibles de cascade, après une longue liste de correctifs :**y traverse les/,*non. ↩︎Documentation de Hugo, Front matter : la liste des champs reconnus va de
aliasesàweightet ne contient paspagination. ↩︎Notes de version de Hugo 0.156.0, 18 février 2026 : retrait de
paginate(remplacé parpagination.pagerSize) et depaginatePath(remplacé parpagination.path). ↩︎Issue #7609 du dépôt gohugoio/hugo, « Pagination does not work with .RegularPagesRecursive, or sub-sections », ouverte et fermée le 30 août 2020. ↩︎