Comment Créer Un Site Avec Hugo Partie 6 : Le Template Des Listes

Aldok

| 13 minutes

Revenir à l'index du tuto : Creer un site avec hugo

Mise à jour — Mai 2026

Ce tutoriel a été intégralement révisé pour rester à jour avec Hugo 0.161 et les outils modernes de l'écosystème (Bulma 1.x, vanilla JS, Netlify Forms, OpenStreetMap, Giscus...). Les anciennes syntaxes dépréciées sont signalées quand c'est utile pour comprendre l'évolution.

Créer un site avec Hugo partie 6 : le template des listes et taxonomies

Nous avons vu comment gérer le template et les contenus des articles de blog, ainsi que les pages uniques. Voyons maintenant un autre pilier de tout blog ou site internet : les pages d’index des taxonomies (ou pour faire plus simple : les pages de catégories ou de tags, qui listent les autres pages qui y sont rattachées).

Petit rappel sur le terme “taxonomie”

Si vous avez l’habitude de travailler avec des CMS, vous devez souvent rencontrer ce terme. Pour ma part, ça fait des années que je le rencontre et je ne me suis jamais vraiment intéressé à sa définition de base… Le linguiste qui sommeille (profondément) en moi est donc allé faire quelques recherches.

En ouvrant un dictionnaire, on peut tomber sur cette définition : “Science de la classification des êtres vivants, inventée par le botaniste Augustin Pyrame de Candolle”.

Si on reprend l’étymologie, l’origine du mot regroupe “taxis” (en grec signifiant arrangement, ordre) et “nomos” (qui désigne les règles, le système) : aucun doute quant à sa définition, c’est un mot pour exprimer le rangement en suivant une logique !

Bon, la minute culture est terminée, passons à la partie technique 🙂

Les pré-requis

Pour lister des choses, encore faut-il qu’elles existent : commencez par créer quelques pages avec leurs illustrations (que vous aurez soigneusement sélectionnées sur des sites d’images libres de droits comme Unsplash ou Pixabay).

Créez 2 autres articles de blog en leur affectant 2 images différentes, et en les mettant dans la catégorie “blogging” par exemple. Vous devriez désormais avoir 3 articles dans votre dossier /blog.

Le template list.html

Rendez-vous dans themes/sandbox/layouts/_default/list.html.

On a déjà créé un template par défaut qui doit ressembler à ça :

{{ define "main" }}
<div class="container">
    <div class="section">
        <div class="content">
            <h1>{{ .Title }}</h1>
            {{ .Content }}
            <ul>
            {{ range .Pages }}
                <li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
            {{ end }}
            </ul>
        </div>
    </div>
</div>
{{ end }}

Comme ce fichier se trouve dans _default, Hugo va l’utiliser à défaut d’autre template plus spécifique. On va maintenant créer un template spécifique au blog.

Allez dans themes/sandbox/layouts/blog/ : comme vous devez le deviner, pour créer le template qui liste les articles du blog, il faut créer un fichier list.html dans ce dossier.

Ajoutez ce contenu :

{{ define "main" }}
<div class="container">
    <div class="section">
        <div class="content">
            <h1>{{ .Title }}</h1>
            {{ .Content }}
        </div>
    </div>
</div>
{{ end }}

Pour vérifier que le template a bien été appliqué, rendez-vous sur http://localhost:1313/blog/ : les liens vers les articles ne doivent plus apparaître, car nous avons retiré la boucle.

Comme pour le reste, on va utiliser Bulma pour créer une mise en page en 3 colonnes. Ajoutez ceci après la div qui contient le titre de la page :

<div class="columns is-multiline">
    {{ range .Pages }}
    <div class="column is-one-third">
        <a href="{{ .Permalink }}">{{ .Title }}</a>
        <div class="content">
            {{ .Summary }}
        </div>
    </div>
    {{ end }}
</div>

Côté public, votre page doit désormais afficher 3 colonnes avec le titre et un lien vers l’article, plus un résumé :

Liste des articles

Bien sûr ces colonnes sont responsives : essayez de redimensionner votre navigateur pour un format mobile, et vous verrez les posts s’empiler. Pas besoin de revenir là-dessus, merci Bulma !

Améliorer l’affichage avec des partials

Maintenant, on va voir un truc pratique : les partials.

Vous vous souvenez des partials ? On en a rapidement parlé au début : c’est une méthode qui permet de créer des mini-templates dans vos templates. On peut comparer ça aux composants/widgets qu’on rencontre dans d’autres frameworks.

Pour que ça soit plus concret, on va donner l’apparence de cartes à nos différents posts (en suivant la doc de Bulma à ce sujet : https://bulma.io/documentation/components/card/).

  1. Créez d’abord un dossier partials/widgets dans le thème : themes/sandbox/layouts/partials/widgets/
  2. Dans ce dossier, ajoutez le fichier post-card.html
  3. Dans ce fichier, ajoutez d’abord les lignes qui permettent d’appeler le titre et le résumé de l’article (comme dans list.html) :
<a href="{{ .Permalink }}">{{ .Title }}</a>
<div class="content">
    {{ .Summary }}
</div>
  1. Ensuite, dans list.html, effacez ces lignes et remplacez par l’appel au partial :
{{ range .Pages }}
<div class="column is-one-third">
    {{ partial "widgets/post-card.html" . }}
</div>
{{ end }}
  1. Retournez sur votre page /blog : normalement, rien n’a changé, et c’est normal — c’est le contenu du partial qui a pris le relais ! Désormais, vous travaillerez uniquement dans ce partial post-card.html pour gérer l’affichage des articles en liste.

Améliorons maintenant le contenu de cette carte, en ajoutant :

  • Un cadre autour de chaque extrait
  • Une image d’illustration
  • Le nom de l’auteur et la date de publication
  • Des liens vers les catégories auxquelles sont attachés les articles
{{ $permalink := .Permalink }}
<div class="card">
    <div class="card-image">
        <figure class="image is-3by2">
            {{ with .Params.images }}
            <a href="{{ $permalink }}"><img src="{{ index . 0 }}" alt="Illustration de l'article"></a>
            {{ end }}
        </figure>
    </div>
    <div class="card-content">
        <a class="title is-4" href="{{ .Permalink }}">{{ .Title }}</a>
        <span class="heading">
            {{ .Site.Params.Author }} |
            <time datetime="{{ .PublishDate.Format "2006-01-02" }}">{{ time.Format ":date_long" .PublishDate }}</time>
        </span>
        <div class="content">
            {{ .Summary }}
        </div>
        <div class="tags is-pulled-right">
            {{ range .Params.categories }}
            <a class="tag is-primary is-radiusless" href="{{ printf "/categories/%s" (. | urlize) | relURL }}">{{ . }}</a>
            {{ end }}
        </div>
    </div>
</div>

Quelques explications sur ce code :

Je ne vais pas m’attarder sur le HTML et le CSS, il suffit de lire la doc du lien donné juste avant.

On remarque une nouvelle forme de variable en toute première ligne :

{{ $permalink := .Permalink }}

Cette ligne affecte la valeur de .Permalink à une variable $permalink qui va nous servir dans un contexte qui ne permet pas l’appel direct de .Permalink.

Regardez un peu plus bas :

{{ with .Params.images }}
    <a href="{{ $permalink }}"><img src="{{ index . 0 }}" alt="..."></a>
{{ end }}

Vous vous demandez sûrement pourquoi on ne peut pas mettre directement <a href="{{ .Permalink }}"> sur l’image ?

La réponse est assez simple : on se retrouve dans un contexte relatif aux images (à cause du with .Params.images). À l’intérieur de ce bloc, . ne réfère plus à la page mais aux images. L’appel à .Permalink n’aurait donc plus aucun sens dans ce contexte. C’est pourquoi on lui affecte une valeur en dehors de ce bloc pour pouvoir l’utiliser à l’intérieur.

Pour les développeurs, c’est une question de scope : à chaque niveau de boucle ou de with, le contexte courant (.) change. Garder des variables nommées comme $permalink permet de naviguer entre les scopes.

Pour la boucle finale qui affiche les catégories :

{{ range .Params.categories }}
    <a class="tag is-primary is-radiusless" href="{{ printf "/categories/%s" (. | urlize) | relURL }}">{{ . }}</a>
{{ end }}

On récupère les paramètres de catégorie tels que renseignés dans le frontmatter, et on utilise urlize pour créer une URL propre (cf. la partie 4 pour plus d’infos).

Petite note sur le format de date : on utilise time.Format ":date_long" qui produit une date localisée en français (“14 mai 2026”) grâce au locale = "fr-FR" défini dans hugo.toml. C’est la méthode moderne, qui remplace les anciens .PublishDate.Format "January 2, 2006" qui sortaient toujours en anglais.

La pagination

Au fil du temps, les articles du site s’accumulent et ça va devenir compliqué de tout afficher sur la même page : c’est là qu’intervient la fonction de pagination de Hugo.

Par défaut, Hugo affiche 10 articles à la fois. Pour changer ça (par exemple 6), ajoutez ces lignes dans hugo.toml :

[pagination]
  pagerSize = 6

⚠️ Attention si vous suivez un vieux tutoriel : avant Hugo 0.128 (mi-2024), on configurait la pagination avec une simple ligne paginate = 6 au top-level. Cette syntaxe est désormais dépréciée et finira par être supprimée. Utilisez bien le bloc [pagination] avec pagerSize pour rester moderne.

Il faut ensuite appeler la pagination dans le template list.html. Au lieu de :

<div class="columns is-multiline">
    {{ range .Pages }}
    ...

On écrit :

<div class="columns is-multiline">
    {{ range .Paginator.Pages }}
    ...

Si vous changez maintenant pagerSize dans hugo.toml (par exemple à 2), vous ne verrez plus que 2 articles sur cette page. Pour en savoir plus sur la pagination : https://gohugo.io/templates/pagination/.

Maintenant il faut ajouter des liens qui permettent de naviguer vers les autres pages : Hugo propose un partial intégré clé en main. Intégrez ce bout de code après la dernière colonne :

{{ partial "pagination.html" . }}

Note : dans les vieux tutos vous verrez {{ template "_internal/pagination.html" . }} à la place. Cette ancienne syntaxe fonctionne toujours, mais depuis Hugo 0.146 la convention recommandée est {{ partial "pagination.html" . }} (les templates internes sont devenus des partials embarqués).

Ça devrait vous donner un truc dans le genre côté front :

Pagination

Pas super sexy comme présentation ; on va donc personnaliser cette zone.

Hugo expose le code source de son template de pagination par défaut : https://github.com/gohugoio/hugo/blob/master/tpl/tplimpl/embedded/templates/pagination.html

Et voilà le code adapté au framework Bulma, à copier-coller dans themes/sandbox/layouts/partials/widgets/pagination.html :

{{ $pag := $.Paginator }}
{{ if gt $pag.TotalPages 1 }}
<nav class="pagination" role="navigation" aria-label="pagination">
    <ul class="pagination-list">
        {{ with $pag.First }}
        <li>
            <a href="{{ .URL }}" class="pagination-link" {{ if not $pag.HasPrev }} disabled{{ end }} aria-label="Première page">
                <span aria-hidden="true">&laquo;&laquo;</span>
            </a>
        </li>
        {{ end }}
        <li>
            <a href="{{ if $pag.HasPrev }}{{ $pag.Prev.URL }}{{ end }}" class="pagination-link" {{ if not $pag.HasPrev }} disabled{{ end }} aria-label="Page précédente">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        {{ $ellipsed := false }}
        {{ $shouldEllipse := false }}
        {{ range $pag.Pagers }}
            {{ $right := sub .TotalPages .PageNumber }}
            {{ $showNumber := or (le .PageNumber 3) (eq $right 0) }}
            {{ $showNumber := or $showNumber (and (gt .PageNumber (sub $pag.PageNumber 2)) (lt .PageNumber (add $pag.PageNumber 2)))  }}
            {{ if $showNumber }}
                {{ $ellipsed = false }}
                {{ $shouldEllipse = false }}
            {{ else }}
                {{ $shouldEllipse = not $ellipsed }}
                {{ $ellipsed = true }}
            {{ end }}
            {{ if $showNumber }}
                <li><a class="pagination-link {{ if eq . $pag }}is-current{{ end }}" href="{{ .URL }}">{{ .PageNumber }}</a></li>
            {{ else if $shouldEllipse }}
                <li class="pagination-link" disabled><span aria-hidden="true">&nbsp;&hellip;&nbsp;</span></li>
            {{ end }}
        {{ end }}
        <li>
            <a href="{{ if $pag.HasNext }}{{ $pag.Next.URL }}{{ end }}" class="pagination-link" {{ if not $pag.HasNext }}disabled{{ end }} aria-label="Page suivante">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
        {{ with $pag.Last }}
        <li>
            <a href="{{ .URL }}" class="pagination-link" {{ if not $pag.HasNext }}disabled{{ end }} aria-label="Dernière page">
                <span aria-hidden="true">&raquo;&raquo;</span>
            </a>
        </li>
        {{ end }}
    </ul>
</nav>
{{ end }}

Maintenant, remplacez l’appel au partial embarqué par notre nouveau partial. À la place de :

{{ partial "pagination.html" . }}

Mettez :

{{ partial "widgets/pagination.html" . }}

Ensuite, retournez voir votre site côté front : la présentation est tout de suite plus sympa !

Pagination stylée

Pour centrer la pagination, il suffit de l’intégrer avec la classe Bulma is-centered :

<div class="columns is-centered">
    <div class="column is-narrow">
        {{ partial "widgets/pagination.html" . }}
    </div>
</div>

Ajouter une description à la catégorie

Vous aurez remarqué que le template autorise l’utilisation des variables .Title et .Content : ça permet de gérer l’affichage du contenu et du titre de la page content/blog/_index.md (le fichier markdown de la première page de la section blog).

Vous pouvez par exemple utiliser ce contenu :

---
title: "Blog"
date: 2026-05-14T11:00:27+02:00
---

## Titre de la page d'accueil de la rubrique blog

Bonjour et bienvenue dans la catégorie "blog" !

Voilà à quoi doit ressembler votre page in fine (à peu près, et si vous avez défini une structure en 3 colonnes) :

Mise en page d’une liste

Les pages de catégories

On a créé un template pour la liste générale des articles. Maintenant, il faut créer le template des sous-catégories, qui peut différer un peu de la page d’accueil de /blog. En effet, à l’heure actuelle, si vous cliquez sur l’une des étiquettes (la catégorie “blogging” par exemple), la page affichée est vide.

En général, pour ne pas trop perturber les visiteurs, on garde une structure uniforme sur toutes les pages. On va donc garder cet affichage sous forme de cartes (et ça nous arrange bien, il n’y aura qu’à reprendre la structure HTML/CSS déjà faite !).

Pour gérer l’affichage des pages de catégories individuelles, créez le template themes/sandbox/layouts/categories/taxonomy.html :

{{ define "main" }}
<div class="container">
    <div class="section">
        <a href="{{ "/categories/" | relURL }}">← Liste des catégories</a>
        <div class="content">
            <h1>Catégorie : {{ .Title }}</h1>
            {{ .Content }}
        </div>
        <div class="columns is-multiline">
            {{ range .Paginator.Pages }}
            <div class="column is-one-third">
                {{ partial "widgets/post-card.html" . }}
            </div>
            {{ end }}
        </div>
        <div class="columns is-centered">
            <div class="column is-narrow">
                {{ partial "widgets/pagination.html" . }}
            </div>
        </div>
    </div>
</div>
{{ end }}

Vous pouvez vérifier que le template fonctionne bien : regardez à nouveau votre page “blogging”. Seuls les articles rangés dans cette catégorie devraient apparaître.

La page qui liste les catégories

Cliquez sur le lien “Liste des catégories” en haut de page : vous devriez reculer d’un cran dans l’arborescence, mais tomber sur une page vide. On va remédier à ça en créant un template dédié : themes/sandbox/layouts/categories/terms.html :

{{ define "main" }}
<div class="container">
    <div class="section">
        <div class="content">
            <h1>{{ .Title }}</h1>
            {{ .Content }}
        </div>
        <div class="columns is-mobile is-multiline">
            {{ range .Data.Terms.ByCount }}
            <div class="column is-half-mobile is-one-third-tablet is-one-quarter-desktop is-one-fifth-widescreen">
                <div class="card">
                    <div class="card-image">
                        <a href="{{ .Page.Permalink }}">
                            <figure class="image is-3by2">
                                {{ $firstChild := index .Pages 0 }}
                                {{ with $firstChild.Params.images }}
                                <img src="{{ index . 0 }}" alt="Illustration de la catégorie">
                                {{ end }}
                            </figure>
                        </a>
                    </div>
                    <div class="card-content has-text-centered">
                        <div>
                            <a class="title is-5 is-size-6-mobile" href="{{ .Page.Permalink }}">{{ .Page.Title }}</a>
                            {{ $pageCount := len .Pages }}
                            <p>{{ $pageCount }} article{{ if ne $pageCount 1 }}s{{ end }}</p>
                        </div>
                    </div>
                </div>
            </div>
            {{ end }}
        </div>
    </div>
</div>
{{ end }}

Quelques explications s’imposent :

  • Le début est similaire à ce que vous avez déjà vu : on affiche le titre et le contenu en haut de la page.
  • Ensuite, dans une structure responsive qui s’adapte à la résolution d’écran, les catégories s’affichent en 2, 3, 4 ou 5 colonnes (les différentes classes Bulma sont explicites).
  • Cette structure est conditionnée par la boucle {{ range .Data.Terms.ByCount }} qui, comme l’indique la documentation, permet de classer les catégories en fonction du nombre d’articles : https://gohugo.io/variables/taxonomy/.
  • Il est possible de choisir un classement alphabétique avec {{ range .Data.Terms.Alphabetical }}, et d’appliquer l’ordre inverse en ajoutant .Reverse à la fin ({{ range .Data.Terms.Alphabetical.Reverse }} par exemple).
  • Ensuite, la catégorie affiche l’image du premier article qui y est publié :
{{ $firstChild := index .Pages 0 }}
{{ with $firstChild.Params.images }}
<img src="{{ index . 0 }}" alt="...">
{{ end }}
  • Enfin, on récupère le nombre d’articles :
{{ $pageCount := len .Pages }}
<p>{{ $pageCount }} article{{ if ne $pageCount 1 }}s{{ end }}</p>

NB pour les développeurs — vous remarquerez des termes familiers, index et len, qui sont très souvent utilisés lorsqu’on manipule des tableaux/slices. Hugo considère chaque liste de pages comme un slice. index .Pages 0 récupère le premier élément, et len .Pages renvoie le nombre total d’éléments.

Voilà à quoi ça doit ressembler chez vous :

Liste des catégories

Ajouter du contenu à cette page

Si vous voulez ajouter un peu de contenu à cette page de liste des catégories, rien de plus simple : il suffit de créer la page d’index du dossier /categories !

hugo new content categories/_index.md

Ajouter la liste des catégories au menu

Pour finir, on va ajouter cette page dans le menu principal du site. Ajoutez un item dans hugo.toml :

[[menu.main]]
    name = "Catégories"
    url = "/categories"

L’élément “Catégories” devrait maintenant apparaître dans le menu principal !

Fin de la partie sur les taxonomies

Ce n’était pas un petit morceau, mais maintenant vous devriez avoir une connaissance assez poussée du langage de templating de Hugo. On a fait la plus grosse partie du travail, on va maintenant ajouter des trucs un peu plus funs !

Vous devriez également aimer ce qui suit...