Comment Creer Un Site Avec Hugo Partie 5 : Le Template Des Pages
Aldok
| 9 minutes

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 5 : le template des pages
Le template par défaut de la page de contenu
Si vous jetez un œil au template themes/sandbox/layouts/_default/single.html, vous verrez qu’il est vide : c’est le fichier par défaut que Hugo va aller chercher s’il ne trouve rien qui ne corresponde mieux au template d’une page simple.
Comme on l’a fait pour les autres templates, on va lui ajouter un peu de contenu par défaut :
{{ define "main" }}
<section class="section">
<div class="container max-800px">
<h1 class="title is-3">
{{ .Title }}
</h1>
<div class="content">
{{ .Content }}
</div>
</div>
</section>
{{ end }}
Avec ce template léger, toutes les pages vont se calquer sur la largeur max de 800px commune aux autres templates qu’on a déjà faits.
La page “À propos”
Nous allons maintenant créer une page “À propos” qui va afficher un petit laïus sur les objectifs du site, comme on en voit souvent :
hugo new content a-propos.md
Pour donner à cette page son propre layout, on crée le template themes/sandbox/layouts/_default/a-propos.html avec ce contenu de base :
{{ define "main" }}
<div class="section">
<div class="container max-800px">
<div class="content">
{{ .Content }}
</div>
</div>
</div>
{{ end }}
Mais on ne va pas s’arrêter en si bon chemin : pour suivre la tendance actuelle, on va ajouter un hero header (et puisque c’est mignon et que je ne suis pas particulièrement inspiré au moment d’écrire ces lignes, on va mettre une image de chat).
Le résultat devrait être celui-ci :

Pour arriver à ce résultat :
Choisissez l’image qui servira d’illustration et uploadez-la dans
static/images/(chez moi, elle s’appellechat.jpg).Dans le template
a-propos.html, ajoutez la mise en forme du hero header juste au-dessus de la section qui affiche le contenu :
<div class="hero is-dark is-fullheight-with-navbar" {{ with .Params.heroimage }}
style="background: url({{ . }}) center top; background-size:cover;" {{ end }}>
<div class="hero-body">
<div class="container max-800px">
<h1 class="title is-1 has-background-primary narrow-background">
{{ .Title }}
</h1>
{{ with .Params.herotext }}
<h2 class="title subtitle is-4 has-background-primary narrow-background">
{{ . }}
</h2>
{{ end }}
</div>
</div>
</div>
- Ajoutez ces lignes au frontmatter de
a-propos.md:
heroimage: /images/chat.jpg
herotext: Je m'appelle Whisper, pour vous servir !
Quelques explications s’imposent :
Le template va s’appuyer sur les infos écrites dans le frontmatter de l’article :
- La boucle
{{ with .Params.heroimage }} ... {{ end }}entre dans la condition “si le paramètreheroimageest renseigné dans le frontmatter”. Si c’est le cas, alors le background va chercher l’image qui correspond au chemin renseigné (/images/chat.jpgici). - Le
{{ .Title }}récupère le titre de la page tel que renseigné dans le frontmatter. - La boucle
{{ with .Params.herotext }} ... {{ . }} ... {{ end }}récupère la valeur deherotextdans le frontmatter.
Pour la mise en forme, on applique la classe hero de Bulma. Pour bien fixer le contenu textuel (titre et sous-titre), on ajoute un peu de CSS dans style.css :
.narrow-background {
display: table;
padding: 0.1em;
}
Ce n’est peut-être pas la façon la plus propre de faire, mais je ne vais pas m’attarder sur les éléments HTML/CSS : ce n’est pas l’objet principal du tuto !
Voilà à quoi doit ressembler la page de contenu a-propos.md (avec un peu de contenu aléatoire) :
---
title: "À Propos"
date: 2026-05-14T11:15:50+02:00
draft: false
layout: a-propos
heroimage: /images/chat.jpg
herotext: Je m'appelle Whisper, pour vous servir !
---
## À propos de ce super site
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris et urna a odio consectetur sollicitudin id eu massa. Donec ante eros, dignissim ut iaculis vel, dignissim in eros.
Aenean scelerisque sem a nulla iaculis, a tempus nulla faucibus. Phasellus fermentum nisl vehicula sollicitudin varius. Pellentesque eros lectus, dignissim vitae accumsan vel, ullamcorper eget metus.
Et la page template correspondante a-propos.html dans son intégralité :
{{ define "main" }}
<div class="hero is-dark is-fullheight-with-navbar" {{ with .Params.heroimage }}
style="background: url({{ . }}) center top; background-size:cover;" {{ end }}>
<div class="hero-body">
<div class="container max-800px">
<h1 class="title is-1 has-background-primary narrow-background">
{{ .Title }}
</h1>
{{ with .Params.herotext }}
<h2 class="title subtitle is-4 has-background-primary narrow-background">
{{ . }}
</h2>
{{ end }}
</div>
</div>
</div>
<div class="section">
<div class="container max-800px">
<div class="content">
{{ .Content }}
</div>
</div>
</div>
{{ end }}
Libre à vous d’adapter le HTML/CSS comme bon vous semble : le principal, c’est d’avoir compris le fonctionnement des boucles qui appellent les données du fichier markdown !
Création d’une page Contact
Pour la page contact, on va partir sur un layout similaire mais avec en plus un formulaire et une carte.
1. Ajouter une image dans votre dossier /static/images, qui servira d’illustration dans le hero header (pour ma part j’ai utilisé un fichier appelé contact.jpg).
2. Créer la page template themes/sandbox/layouts/_default/contact.html :
{{ define "main" }}
<div class="hero is-dark is-medium" {{ with .Params.heroimage }}
style="background: url({{ . }}) center center; background-size:cover;" {{ end }}>
<div class="hero-head">
<div class="container">
<h1 class="title is-2 has-background-primary narrow-background">
{{ .Title }}
</h1>
{{ with .Params.herotext }}
<h2 class="title subtitle is-4 has-background-primary narrow-background">
{{ . }}
</h2>
{{ end }}
</div>
</div>
<div class="hero-body"></div>
</div>
<section class="section">
<div class="container">
<div class="columns">
<div class="column">
<div class="content">
{{ .Content }}
</div>
{{ partial "widgets/contact-form.html" . }}
</div>
{{ with .Params.map }}
<div class="column is-one-third">
{{ partial "widgets/map.html" . }}
</div>
{{ end }}
</div>
</div>
</section>
{{ end }}
On utilise deux partials dédiés (un pour le formulaire, un pour la carte) — c’est plus propre et ça permet de réutiliser ces composants ailleurs si besoin.
3. Créer la page de contenu en markdown :
hugo new content contact.md
Contenu de contact.md :
---
title: "Contact"
date: 2026-05-14T16:00:02+02:00
layout: contact
heroimage: /images/contact.jpg
map: Rouen, France
---
## Me contacter
Un projet, une question, une remarque ? N'hésitez pas à m'envoyer un message via le formulaire ci-dessous !
4. Ajouter l’élément de menu dans hugo.toml pour créer le lien vers la page contact :
[[menu.main]]
name = "Contact"
url = "/contact"
Le formulaire de contact avec Netlify Forms
Comme nous n’avons pas de serveur backend (c’est tout l’intérêt d’un site statique), il faut passer par un service tiers pour gérer l’envoi des messages. Bonne nouvelle : Netlify (qu’on utilisera dans la partie 9 pour héberger le site) propose un service de gestion de formulaires intégré et gratuit jusqu’à 100 soumissions par mois.
C’est une solution bien plus propre que les iframes Google Forms qu’on voyait dans les vieux tutos : pas de pop-up, pas de tracking tiers, pas de jQuery, juste du HTML standard.
Créez le partial themes/sandbox/layouts/partials/widgets/contact-form.html :
<form name="contact" method="POST" data-netlify="true" netlify-honeypot="bot-field" action="/merci">
<input type="hidden" name="form-name" value="contact">
<p hidden>
<label>Ne remplissez pas ce champ : <input name="bot-field"></label>
</p>
<div class="field">
<label class="label" for="name">Nom</label>
<div class="control">
<input class="input" type="text" name="name" id="name" required>
</div>
</div>
<div class="field">
<label class="label" for="email">Email</label>
<div class="control">
<input class="input" type="email" name="email" id="email" required>
</div>
</div>
<div class="field">
<label class="label" for="message">Message</label>
<div class="control">
<textarea class="textarea" name="message" id="message" rows="6" required></textarea>
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-primary" type="submit">Envoyer</button>
</div>
</div>
</form>
Décortiquons les attributs spécifiques à Netlify :
data-netlify="true": indique à Netlify qu’il doit gérer ce formulaire. Au moment du build, Netlify le détecte automatiquement et l’enregistre dans le dashboard du site.name="contact": nom du formulaire, qui apparaîtra dans le dashboard Netlify.netlify-honeypot="bot-field": champ caché anti-spam. Si un bot le remplit (parce qu’il remplit tous les champs automatiquement), Netlify rejette la soumission.action="/merci": URL de redirection après envoi (vous pouvez créer une pagemerci.mdqui remerciera l’internaute).<input type="hidden" name="form-name" value="contact">: nécessaire pour que Netlify retrouve le bon formulaire à la soumission.
Pour les classes CSS : field, label, control, input, textarea, button sont des classes Bulma qui stylisent automatiquement les éléments de formulaire. Très pratique.
Important : Netlify Forms ne fonctionne que sur un site déployé sur Netlify. En local (hugo server), la soumission ne fera rien — c’est normal. Une fois le site déployé, allez dans Netlify Dashboard → Forms pour voir les soumissions et configurer les notifications par e-mail.
Alternatives à Netlify Forms si vous n’utilisez pas Netlify :
- Formspree — service similaire, gratuit jusqu’à 50 soumissions/mois
- Getform — gratuit jusqu’à 50 soumissions/mois
- Web3Forms — illimité gratuit, basé sur des clés d’accès
La carte
Pour afficher une carte, on a longtemps utilisé Google Maps via une iframe <iframe src="https://maps.google.com/...">. Cette méthode présente plusieurs inconvénients aujourd’hui :
- Google a renforcé les conditions d’utilisation, certaines intégrations historiques peuvent casser
- Tracking utilisateur dès le chargement de la page
- Limite d’utilisation et nécessité de plus en plus fréquente d’une clé API
L’alternative recommandée en 2026 c’est OpenStreetMap, le pendant libre et collaboratif. L’iframe d’OSM ne nécessite ni clé API ni création de compte.
Créez le partial themes/sandbox/layouts/partials/widgets/map.html :
<div class="map-responsive">
<iframe
src="https://www.openstreetmap.org/export/embed.html?bbox=1.0741%2C49.4271%2C1.1241%2C49.4471&layer=mapnik&marker=49.4431%2C1.0991"
title="Carte"
loading="lazy"
style="border: 0;">
</iframe>
</div>
L’URL utilise une bounding box (bbox) qui définit les coordonnées des coins de la zone affichée : longitude_min,latitude_min,longitude_max,latitude_max. Le paramètre marker ajoute un point rouge sur des coordonnées précises.
Comment générer l’iframe pour votre propre adresse ?
- Rendez-vous sur openstreetmap.org
- Cherchez votre adresse
- Cliquez sur Partager (icône à droite)
- Cochez Inclure un marqueur, ajustez la zone visible
- Copiez le code HTML proposé sous “Code HTML”
Pour que la carte s’affiche bien en mode responsive, ajoutez ce style à style.css :
.map-responsive {
overflow: hidden;
padding-bottom: 100%;
position: relative;
height: 0;
}
.map-responsive iframe {
left: 0;
top: 0;
height: 100%;
width: 100%;
position: absolute;
}
Pour aller plus loin avec Leaflet
Si vous voulez une carte vraiment interactive (zoom personnalisé, plusieurs marqueurs, popups, couches custom…), regardez du côté de Leaflet, une librairie JavaScript open-source qui consomme les tuiles OpenStreetMap. C’est gratuit, sans clé API, et c’est ce que beaucoup de gros sites utilisent (y compris des médias comme Le Monde). Mais pour un simple “voilà où je suis”, l’iframe OSM suffit largement.
En conclusion
Nous avons fait le tour des quelques fonctionnalités qui permettent de créer des pages avec des templates spécifiques. À cette étape, vous devriez commencer à beaucoup mieux comprendre le fonctionnement de Hugo : nous allons à présent voir comment gérer l’apparence des listes et des taxonomies !