Comment Creer Un Site Avec Hugo Partie 4 : Le Template Des Articles

Aldok

| 12 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 4 : le template des articles

Pour l’instant, nous n’avons aucun moyen de naviguer dans les différents posts depuis la page d’accueil — à part en recopiant les URLs dans la barre d’adresse évidemment.

On va donc voir comment lister automatiquement les différents articles (ou “pages enfants”) de la section blog, puis personnaliser leur affichage individuel.

1. Créer le template des pages qui listent d’autres pages

Dans le dossier themes/sandbox/layouts/_default/, on peut voir qu’il y a un fichier list.html.

Il s’agit du template par défaut des pages qui affichent des listes, comme la page /blog dont la fonction est de lister les différents articles qui en dépendent.

Dans le fichier list.html (qui est vide et donc n’affiche rien dans la partie blog), ajoutez ces lignes :

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

Avec ce template tout simple, on va pouvoir afficher la liste des pages qui dépendent des différents répertoires que vous créez dans /content.

Quelques explications sur le code généré :

{{ .Title }}

Reprend le titre indiqué dans le frontmatter des articles créés en MarkDown.

{{ .Content }}

Permet d’afficher le contenu de l’article créé en MarkDown.

{{ range .Pages }}
   <a href="{{ .Permalink }}">{{ .Title }}</a>
{{ end }}

La boucle range .Pages va chercher toutes les pages présentes dans le dossier dans lequel on se trouve, et affichera le titre avec son lien pour chaque élément trouvé.

Petit rappel :

Hugo génère l’arborescence du site en fonction de l’architecture des dossiers créés dans content/. Ainsi, si on crée un dossier /blog qui contient premier-post.md et deuxieme-post.md, ça va générer les pages suivantes : https://domaine.tld/blog/premier-post et https://domaine.tld/blog/deuxieme-post.

De la même façon, si on crée un autre répertoire /categorie1 et qu’on y ajoute des articles, ils seront générés côté public avec la logique https://domaine.tld/categorie1/titre-du-post.

Personnaliser la page d’index de la section

Telle que nous l’avons construite, la page générée par list.html se contente de lister les articles. Si on veut la personnaliser (rajouter une description de la catégorie par exemple), il faut créer une page _index.md à la racine du dossier /blog :

hugo new content blog/_index.md

Contenu de _index.md :

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

# Titre de mon super blog
Bonjour et bienvenue dans la catégorie "blog"

Côté public, on devrait avoir quelque chose dans ce genre (le rendu dépend du contenu de votre dossier /blog) :

Exemple premier post

2. Créer le template des pages de contenus

On va commencer par créer une nouvelle page enfant dans le dossier /blog, dans un sous-dossier /2026 :

hugo new content blog/2026/premier-post-de-2026.md

Contenu du fichier premier-post-de-2026.md :

---
title: "Mon premier post de l'année 2026"
date: 2026-03-29T11:13:57+02:00
draft: false
---

## Test nouveau post

Bonjour ceci est un article de test.

### Titre de niveau 3

Contenu relatif au titre niveau 3.

### Titre de niveau 3 bis

Contenu relatif au titre niveau 3 bis.

Côté public, ça va afficher le contenu mais sans aucun style ; on va remédier à ça en modifiant le template relatif aux pages enfants.

Pour personnaliser l’apparence des pages qui vont se trouver dans la catégorie /blog, il suffit de créer un sous-répertoire dans layouts/ qui porte le même nom, à savoir /blog.

Hugo applique alors en priorité tout template qui se trouve dans ce répertoire aux contenus du sous-répertoire content/blog.

On crée donc le template qui gère l’apparence des articles en créant un fichier single.html dans ce dossier /blog. On se retrouve avec une arborescence ainsi :

themes/sandbox/layouts/blog/single.html

Ajoutons maintenant du contenu à single.html :

{{ define "main" }}
<section class="section">
  <article>
    <div class="columns is-centered">
      <div class="column max-800px">
        <h1 class="title is-1">{{ .Title }}</h1>
        <div class="content">
          {{ .Content }}
        </div>
      </div>
    </div>
  </article>
</section>
{{ end }}

Si vous avez bien tout suivi, je structure mon contenu en utilisant les classes Bulma autour des boucles {{ .Title }} et {{ .Content }}.

Pour avoir quelque chose de plus sympa visuellement, je vais définir la largeur max à 800px en ajoutant simplement cette classe à style.css :

.max-800px {
    max-width: 800px;
}

J’ajoute également un peu de Lorem Ipsum afin de densifier le contenu, on se rend mieux compte du résultat final côté public sur la page /blog/2026/premier-post-de-2026 :

Exemple de post publié

Restons un peu dans cette rubrique blog : à votre avis, comment modifier l’apparence de la page qui liste uniquement les articles de cette catégorie ?

En créant un template list.html dans themes/sandbox/layouts/blog/ !

Faites le test : passez le titre en rouge avec un style inline :

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

Ensuite, rendez-vous à la page /blog côté public : vous devriez voir le titre s’afficher en rouge. Le template blog/list.html a donc bel et bien pris la priorité sur le fichier _default/list.html.

(Vous pouvez effacer ce fichier après vérification, c’était juste pour confirmer la bonne compréhension des priorités !)

3. Améliorer l’ensemble : metadatas, photo de l’auteur, image à la une

Les métadonnées et la photo de l’auteur

Pour ajouter une photo d’auteur, il faut d’abord ajouter l’image à utiliser dans le dossier /static/images à la racine du site (et non dans le thème, car l’auteur est lié au site, pas au thème).

Ensuite, on déclare l’image dans hugo.toml :

[params]
    author = "Alex"
    authorImage = "/images/avatar.jpg"

Pour styler tout ça, on applique un peu de CSS dans style.css :

.author-image {
    object-fit: cover;
    border-radius: 50%;
    width: 48px;
    height: 48px;
}

Ça crée une image circulaire de 48px.

Maintenant, ajoutons le code d’intégration à single.html pour inclure la photo et les métadonnées :

<div class="title subtitle heading is-6">
    <div class="columns is-vcentered is-mobile">
      {{ with .Site.Params.authorImage }}
      <div class="column is-narrow">
        <img src="{{ . }}" class="author-image" alt="Photo de l'auteur">
      </div>
      {{ end }}
      <div class="column">
        <p>{{ .Site.Params.Author }}</p>
        <p>
          <time datetime="{{ .PublishDate.Format "2006-01-02" }}">{{ time.Format ":date_long" .PublishDate }}</time>
          |
          {{ .ReadingTime }} {{ if eq .ReadingTime 1 }} minute {{ else }} minutes {{ end }}
        </p>
      </div>
    </div>
</div>

Petit focus sur le format de date

L’article original utilisait .PublishDate.Format "January 2, 2006", ce qui renvoyait toujours la date en anglais quelle que soit la langue du site. C’est un problème historique que Hugo a résolu depuis la version 0.87 avec la fonction time.Format et les pseudo-formats localisés :

{{ time.Format ":date_long" .PublishDate }}     {{/* 14 mai 2026 */}}
{{ time.Format ":date_medium" .PublishDate }}   {{/* 14 mai 2026 */}}
{{ time.Format ":date_short" .PublishDate }}    {{/* 14/05/2026 */}}
{{ time.Format ":date_full" .PublishDate }}     {{/* jeudi 14 mai 2026 */}}

Hugo utilise la valeur de locale définie dans votre hugo.toml (locale = "fr-FR" dans notre cas) pour traduire automatiquement les noms de mois et de jours. Plus besoin de hacks ou de plugins externes pour avoir des dates en français !

Petit focus sur le reading time

.ReadingTime est une variable propre à Hugo qui calcule approximativement le temps de lecture de l’article (basé sur un débit moyen de 200 mots par minute). C’est purement automatique, vous n’avez rien à faire.

Le fichier single.html doit ressembler à ça dans son intégralité :

{{ define "main" }}
<section class="section">
  <article>
    <div class="columns is-centered">
        <div class="column max-800px">
          <h1 class="title is-1">{{ .Title }}</h1>
          <div class="title subtitle heading is-6">
            <div class="columns is-vcentered is-mobile">
              {{ with .Site.Params.authorImage }}
              <div class="column is-narrow">
                <img src="{{ . }}" class="author-image" alt="Photo de l'auteur">
              </div>
              {{ end }}
              <div class="column">
                <p>{{ .Site.Params.Author }}</p>
                <p>
                  <time datetime="{{ .PublishDate.Format "2006-01-02" }}">{{ time.Format ":date_long" .PublishDate }}</time>
                  |
                  {{ .ReadingTime }} {{ if eq .ReadingTime 1 }} minute {{ else }} minutes {{ end }}
                </p>
              </div>
            </div>
          </div>
          <div class="content">
            {{ .Content }}
          </div>
        </div>
      </div>
  </article>
</section>
{{ end }}

Au final, côté public, on devrait avoir un rendu dans ce style :

Exemple de titre

L’illustration de l’article (l’image à la une)

Pour l’exemple, prenez une image d’illustration trouvée sur Pixabay, Unsplash ou tout autre site d’images libres de droits. Je prendrai une image d’un cliché de la Terre vue de l’espace.

Question pratique : où ranger l’image ?

On serait tenté de répondre “dans /static du thème”, mais l’image étant une illustration d’article, elle est liée au contenu, pas au thème. On la place donc dans le dossier /static à la racine du site.

Libre à vous de définir une arborescence pour ranger vos images. Personnellement, je les classe par section : static/images/blog/ pour les articles de blog, static/images/tutos/ pour les tutos, etc.

Note pour aller plus loin : Hugo propose aussi le concept de page bundles, qui consiste à mettre l’article et ses images dans un même dossier (par exemple content/blog/2026/mon-article/index.md + content/blog/2026/mon-article/illustration.jpg). C’est une approche plus moderne qui co-localise le contenu et ses assets, et qui permet d’utiliser le pipeline d’images de Hugo (redimensionnement automatique, WebP, etc.). On y reviendra dans un article bonus — pour cette série, on reste sur la méthode simple avec /static.

Une fois l’image dans son dossier, on l’appelle via le frontmatter de l’article :

images: ["/images/blog/terre.jpg"]

Notez qu’il n’y a pas besoin de rappeler le dossier /static dans le chemin : c’est de là que part Hugo pour servir les fichiers statiques.

Ensuite, on l’affiche dans single.html, juste avant le contenu :

{{ with .Params.images }}
<figure class="title-image">
    <img src="{{ index . 0 }}" alt="Illustration de l'article">
</figure>
{{ end }}

L’image colle un peu trop au titre ; ajoutez ceci à style.css :

.title-image {
    padding-bottom: 1.5em;
}

4. Afficher les catégories et la navigation entre articles

4.1 Le lien vers les catégories

Via Hugo, on peut jouer avec les taxonomies. Pour rappel, une taxonomie est le nom un peu barbare utilisé pour définir des options de tri : par exemple, des catégories ou des tags.

Pour ajouter une taxonomie à un article, il faut ajouter une variable dans le frontmatter : soit la variable categories, soit la variable tags (ou n’importe quelle taxonomie custom que vous auriez définie).

Utilisons la variable categories pour l’exemple, en affectant 2 catégories à l’article :

categories: ["Blogging", "Développement web"]

Ensuite, on affiche ces catégories via le template. Modifiez la <div class="title subtitle heading is-6"> ainsi :

<div class="title subtitle heading is-6">
    <div class="columns is-vcentered">
        <div class="column">
            <div class="columns is-vcentered is-mobile">
                {{ with .Site.Params.authorImage }}
                    <div class="column is-narrow">
                        <img src="{{ . }}" class="author-image" alt="Photo de l'auteur">
                    </div>
                {{ end }}
                <div class="column">
                    <p>{{ .Site.Params.Author }}</p>
                    <p>
                      <time datetime="{{ .PublishDate.Format "2006-01-02" }}">{{ time.Format ":date_long" .PublishDate }}</time>
                      |
                      {{ .ReadingTime }} {{ if eq .ReadingTime 1 }} minute {{ else }} minutes {{ end }}
                    </p>
                </div>
            </div>
        </div>
        <div class="column">
            {{ range $idx, $category := .Params.categories }}
                {{- if ne $idx 0 }}, {{ end }}
                <a href="{{ "categories/" | relURL }}{{ $category | urlize }}">{{ $category }}</a>
            {{- end }}
        </div>
    </div>
</div>

Cela ajoute une colonne à côté de la date, qui passera automatiquement en dessous en responsive.

Exemple d'affichage des catégories sous le titre

Quelques explications sur le code qui appelle les catégories :

{{ range $idx, $category := .Params.categories }}
    {{- if ne $idx 0 }}, {{ end }}
    <a href="{{ "categories/" | relURL }}{{ $category | urlize }}">{{ $category }}</a>
{{- end }}

La fonction range ouvre une boucle (l’équivalent d’un for en Go). Ici elle fait deux choses : elle récupère l’index de la catégorie (numéro dans la liste, à partir de 0) dans la variable $idx, et la valeur de la catégorie dans $category.

Pour chaque itération, on affiche une virgule sauf pour la première (if ne $idx 0, “si l’index n’est pas égal à 0”).

Ensuite, le lien dans <a> est généré dynamiquement par :

{{ "categories/" | relURL }}{{ $category | urlize }}

Arrêtons-nous sur cette ligne : elle utilise le caractère |, qu’on appelle un pipe en langage de templating Go : https://gohugo.io/templates/introduction/#pipes

Voici ce que fait ce code : on passe la chaîne "categories/" dans la fonction relURL, qui génère une URL relative. Puis on passe $category dans urlize, qui transforme la chaîne en version “URL-friendly” (suppression des accents, espaces → tirets, etc.).

Ainsi, “Développement web” devient developpement-web, et le lien final ressemble à /categories/developpement-web.

Bonus pratique : essayez de cliquer sur le lien généré, et votre page de catégorie s’affichera comme par magie ! Bon, elle est bien vide à ce stade (elle se base sur list.html qui est plutôt pauvre), mais on reviendra dessus dans la partie 6 pour la personnaliser.

4.2 Les liens vers les posts précédents et suivants

Pour finir le template, ajoutons ce qu’on trouve souvent à la fin des articles de blog : des liens “Article précédent” et “Article suivant”.

Hugo propose des variables prêtes à l’emploi : .PrevInSection et .NextInSection, qui pointent respectivement vers l’article précédent et suivant de la même section. Ajoutez cette section après la balise </article> :

<section class="section">
  <div class="columns is-centered">
    <div class="column max-800px">
      <div class="columns is-mobile">
        <div class="column has-text-left">
          {{ with .PrevInSection }}
            <p>Article précédent</p>
            <a href="{{ .Permalink }}">{{ .Title }}</a>
          {{ end }}
        </div>
        <div class="column has-text-right">
          {{ with .NextInSection }}
            <p>Article suivant</p>
            <a href="{{ .Permalink }}">{{ .Title }}</a>
          {{ end }}
        </div>
      </div>
    </div>
  </div>
</section>

Pour voir si le script fonctionne, il faut bien sûr avoir au moins 2 articles dans la même section :

hugo new content blog/2026/deuxieme-post-2026.md
hugo new content blog/2026/troisieme-post-2026.md

Les colonnes créées avec les classes Bulma permettent un affichage cohérent avec le reste du template. Voici à quoi doit ressembler le pied du deuxième article :

Exemple du pied de l'article

Conclusion : en route pour la création des pages !

On a vu comment fonctionne le template des articles, on va maintenant pouvoir s’attaquer aux pages un peu plus figées, comme la page “À propos” ou la page “Contact”.

On reviendra un peu plus tard sur le template des articles pour ajouter la possibilité de laisser des commentaires.

Vous devriez également aimer ce qui suit...