# Pagination Hugo et sections imbriquées : la page 2 que personne n'a demandée

> En avril, Hugo 0.160 a semé des pages /page/2/ sous des sections imbriquées qui n'avaient rien à paginer. Aucune option ne les coupait, alors un script les efface après chaque construction. Six mois plus tard, l'essai refait sur Hugo 0.168 désigne un autre coupable.


*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](/blog/crawler-seo-go-audit-site/) 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 `.Paginator`[^1]. Pas d'appel, pas de pages `/page/N/`. Or mon gabarit de liste commençait par ceci :

```go-html-template
{{/* On récupère le numéro de page pour l'afficher dans le titre */}}
{{ $pageNum := 0 }}{{ with .Paginator }}{{ $pageNum = .PageNumber }}{{ end }}
<h1>{{ .Title }}{{ if gt $pageNum 1 }} (Page {{ $pageNum }}){{ end }}</h1>
```

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 :

```text
docs/page/1/index.html
docs/page/2/index.html
docs/reseau/page/1/index.html
docs/reseau/page/2/index.html
docs/reseau/dns/page/1/index.html
docs/reseau/dns/page/2/index.html
```

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 descendants[^2] :

```toml
[[cascade]]
  [cascade.target]
    path = "{/docs,/docs/**}"
  [cascade.pagination]
    pagerSize = 10000
    disableAliases = true
```

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 matter[^3], 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.pagerSize`[^4]. Reste une nuance : `.Paginator` ne permet pas de changer la taille des paquets, seul `.Paginate` accepte un nombre en second argument[^1].

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 :

```bash
#!/bin/bash
# Supprime les /page/ des sous-sections de /docs/, garde celle de /docs/ elle-même
set -euo pipefail
PUBLIC="$1"
find "$PUBLIC/docs" -mindepth 2 -type d -name page -exec rm -rf {} +
```

`-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'emporte[^5]. 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 :

```go-html-template
{{/* Seule la racine de la section est une vraie liste paginée */}}
{{ $pageNum := 0 }}
{{ if eq .Path "/docs" }}
  {{ with .Paginator }}{{ $pageNum = .PageNumber }}{{ end }}
{{ end }}
```

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 arguments[^1]. 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](/blog/hugo-amp-limite-css-chroma-noclasses/). 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](/blog/hugo-publier-article-date-future-sans-cms/).

[^1]: Documentation de Hugo, [Pagination](https://gohugo.io/templates/pagination/) : méthodes `Paginate` et `Paginator`, taille des paquets, et mise en cache du premier appel (« Regardless of pagination method, the initial invocation is cached and cannot be changed »).

[^2]: Documentation de Hugo, [Cascade](https://gohugo.io/configuration/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.

[^3]: Documentation de Hugo, [Front matter](https://gohugo.io/content-management/front-matter/) : la liste des champs reconnus va de `aliases` à `weight` et ne contient pas `pagination`.

[^4]: Notes de version de [Hugo 0.156.0](https://github.com/gohugoio/hugo/releases/tag/v0.156.0), 18 février 2026 : retrait de `paginate` (remplacé par `pagination.pagerSize`) et de `paginatePath` (remplacé par `pagination.path`).

[^5]: [Issue #7609](https://github.com/gohugoio/hugo/issues/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.



