Comment Creer Un Site Avec Hugo Partie 3 : Le Layout

Aldok

| 9 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 3 : le layout

Pour l’exemple, nous allons créer un thème basé sur Bulma, un framework CSS qui a l’avantage d’être moderne, simple à utiliser et purement basé sur du CSS (pas de dépendance JavaScript) : https://bulma.io/

Ça permettra d’ajouter une charte simple et propre, rapidement et sans réinventer la roue. Au moment où j’écris ces lignes la version stable de Bulma est la 1.x : elle apporte plein de nouveautés par rapport aux versions précédentes (dark mode natif, variables CSS pures, refonte des couleurs…). Si vous tombez sur un tutoriel qui utilise Bulma 0.7 ou 0.9, sachez que la philosophie reste la même mais quelques classes et variables ont évolué.

Petite mise au point sur le vocabulaire

Avant d’attaquer, levons une ambiguïté qui revient souvent : on a deux fichiers qui se ressemblent dans Hugo, head.html et header.html.

  • head.html correspond à la balise <head> du HTML — c’est l’en-tête invisible qui contient les méta-données, les liens vers les feuilles de styles, les scripts, etc.
  • header.html correspond à la balise <header> du body — c’est le haut visible du site, généralement la barre de navigation.

Les deux vivent dans layouts/partials/ dans le thème. On va commencer par le head.html.

Ajouts dans le head

Pour ajouter Bulma à la page, il faut d’abord l’inclure dans le <head> en ajoutant un lien vers sa feuille de styles. Deux solutions :

Solution 1 — via un CDN (la plus simple)

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">

Pensez à consulter bulma.io pour récupérer la dernière version au moment où vous lisez cet article.

Solution 2 — en local

Téléchargez la feuille de styles depuis bulma.io et placez-la dans themes/sandbox/static/css/bulma.min.css. Ensuite, dans head.html :

<link rel="stylesheet" href="{{ "css/bulma.min.css" | relURL }}">

Petite note technique : dans les anciens tutos Hugo, vous trouverez la fonction absURL au lieu de relURL. Les deux fonctionnent toujours, mais relURL est généralement préférable pour la portabilité du site (deploy preview Netlify, sous-domaines, etc.).

On en profite pour ajouter quelques icônes avec FontAwesome 6 (qui a remplacé les versions 4 et 5 plus anciennes, avec des centaines de nouvelles icônes) :

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" crossorigin="anonymous">

Et c’est tout pour les scripts ! Vous remarquerez l’absence de jQuery — les versions modernes de Bulma n’en ont pas besoin, et on va faire le menu mobile en JavaScript vanilla un peu plus loin. C’est une migration que je vous recommande chaudement : jQuery a fait son temps, et en 2026 le JS natif gère très bien tout ce dont on a besoin.

Le baseof.html

Retournons dans le fichier baseof.html (themes/sandbox/layouts/_default/baseof.html).

Pour rappel, il s’agit du template qui s’applique automatiquement à toutes les pages par défaut.

Voilà ce qu’il doit contenir :

<!DOCTYPE html>
<html lang="{{ .Site.Language.Locale }}">
    {{- partial "head.html" . -}}
    <body>
        {{- partial "header.html" . -}}
        <main>
            {{- block "main" . }}{{- end }}
        </main>
    {{- partial "footer.html" . -}}
    </body>
</html>

Note importante : dans les anciens tutos, vous verrez {{ .Site.LanguageCode }} au lieu de {{ .Site.Language.Locale }}. La première forme a été dépréciée à partir de Hugo 0.158 et provoque un warning aujourd’hui. Pensez à utiliser la nouvelle syntaxe sur vos nouveaux projets.

Pour que ça fonctionne, ajoutez dans votre hugo.toml à la racine du projet :

defaultContentLanguage = "fr"
locale = "fr-FR"

Nous avons déjà rempli la partie head.html, qui contient l’en-tête invisible pour les internautes ; nous allons désormais travailler dans le partial header.html, qui permet de gérer l’apparence du haut du site visible.

La partie header

Rendez-vous dans le fichier layouts/partials/header.html du thème et ajoutez ce code :

<header>
    <nav class="navbar" role="navigation" aria-label="navigation principale">
        <div class="container">
            <div class="navbar-brand">
                <a href="/" title="Accueil" class="navbar-item">
                    <span class="logo"><h1>{{ .Site.Title }}</h1></span>
                </a>
                {{ range .Site.Menus.social }}
                <a href="{{ .URL }}" class="navbar-item is-hidden-desktop" title="{{ .Name }}">
                    <span class="icon">{{ .Pre | safeHTML }}</span>
                </a>
                {{ end }}
                <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbar-menu">
                    <span aria-hidden="true"></span>
                    <span aria-hidden="true"></span>
                    <span aria-hidden="true"></span>
                </a>
            </div>

            <div id="navbar-menu" class="navbar-menu">
                <div class="navbar-start">
                    {{ range .Site.Menus.main }}
                    <a href="{{ .URL }}" class="navbar-item">{{ .Name }}</a>
                    {{ end }}
                </div>
                <div class="navbar-end">
                    {{ range .Site.Menus.social }}
                    <a href="{{ .URL }}" class="navbar-item is-hidden-touch" title="{{ .Name }}">
                        <span class="icon">{{ .Pre | safeHTML }}</span>
                    </a>
                    {{ end }}
                </div>
            </div>
        </div>
    </nav>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const burger = document.querySelector('.navbar-burger');
            const target = document.getElementById(burger.dataset.target);
            burger.addEventListener('click', () => {
                burger.classList.toggle('is-active');
                target.classList.toggle('is-active');
            });
        });
    </script>
</header>

C’est un gros morceau de code, que l’on peut schématiser ainsi :

header
    navbar
        container
            navbar-brand
                navbar-item (logo)
                navbar-item (icônes sociales mobile)
                navbar-burger (menu mobile)
            navbar-menu
                navbar-start (items menu principal)
                navbar-end (icônes sociales desktop)
    script (toggle menu burger)

Quelques commentaires sur le code

  • La classe navbar de Bulma gère l’apparence d’une barre de navigation. Dans cette barre, on crée un container pour limiter la largeur totale (évite que le logo colle au bord sur les écrans larges).
  • navbar-brand concerne la partie gauche : logo + icônes sociales (visibles uniquement sur mobile grâce à is-hidden-desktop) + bouton burger.
  • navbar-menu contient la partie droite : navigation principale dans navbar-start, icônes sociales desktop dans navbar-end. La séparation start/end permet de positionner les éléments aux extrémités.
  • Le data-target="navbar-menu" sur le navbar-burger indique au script JS quel élément doit s’ouvrir/se fermer. C’est une convention propre à Bulma : on lie le bouton burger à son menu via l’ID.
  • Le {{ .Pre | safeHTML }} sert à afficher le HTML brut stocké dans le menu (notamment les balises <i> des icônes FontAwesome) sans qu’Hugo ne l’échappe.

Le script vanilla JS

Le petit bloc de JavaScript en bas du fichier remplace ce qui se faisait historiquement en jQuery. Décortiquons-le :

document.addEventListener('DOMContentLoaded', () => {
    const burger = document.querySelector('.navbar-burger');
    const target = document.getElementById(burger.dataset.target);
    burger.addEventListener('click', () => {
        burger.classList.toggle('is-active');
        target.classList.toggle('is-active');
    });
});
  • DOMContentLoaded : on attend que le HTML soit chargé avant de manipuler les éléments
  • querySelector('.navbar-burger') : on récupère le bouton burger
  • burger.dataset.target : on lit l’attribut data-target qui contient l’ID du menu
  • classList.toggle('is-active') : à chaque clic, on bascule la classe is-active (Bulma s’occupe du reste)

C’est moins long que la version jQuery, plus rapide à charger, et ça ne nécessite aucune librairie externe. Tout le monde y gagne !

Déclarer les menus

Pour ajouter des liens de navigation de manière dynamique grâce à la boucle range, il suffit de les déclarer dans le fichier de configuration hugo.toml :

[[menu.main]]
  name = "A propos"
  url = "/a-propos"

[[menu.main]]
  name = "Blog"
  url = "/blog"

Notez que si la page “a-propos” n’existe pas, il faut la créer pour que le lien fonctionne :-)

Pour activer les icônes sociales, idem, on déclare les liens dans le fichier de config :

[[menu.social]]
  name = "GitHub"
  url = "https://github.com/VotreCompteGithub"
  pre = "<i class='fa-brands fa-github'></i>"

[[menu.social]]
  name = "Mastodon"
  url = "https://mastodon.social/@VotreCompte"
  pre = "<i class='fa-brands fa-mastodon'></i>"

Note FontAwesome 6 : la syntaxe des classes a changé depuis FA5. Les marques (GitHub, Twitter/X, Mastodon, etc.) utilisent désormais fa-brands au lieu de fab. Les icônes solides utilisent fa-solid au lieu de fas. Les anciennes classes restent supportées en mode rétrocompatibilité, mais préférez la nouvelle syntaxe.

Comme pour la partie header, on va travailler dans le fichier layouts/partials/footer.html :

<footer class="footer">
    <div class="container">
        <div class="columns has-text-centered">
            {{ range .Site.Menus.footer }}
                <div class="column is-narrow">
                    <a href="{{ .URL }}">{{ .Name }}</a>
                </div>
            {{ end }}
        </div>
    </div>
</footer>

On utilise la classe footer de Bulma, dans laquelle on ajoute un container pour limiter la largeur sur les écrans larges, puis une structure en columns (basée sur Flexbox). Pour plus d’infos sur le fonctionnement des colonnes Bulma : https://bulma.io/documentation/columns/.

La boucle range se répète autant de fois qu’on déclare un lien en footer dans hugo.toml. Chaque entrée génère sa propre colonne :

[[menu.footer]]
  name = "A propos"
  url = "/a-propos"

[[menu.footer]]
  name = "Protection des données"
  url = "/protection-des-donnees"

Le contenu

Maintenant que le header et le footer sont prêts, on s’attaque au contenu. On commence par le template de la page d’accueil, layouts/index.html :

{{ define "main" }}
<div class="container">
    <div class="section">
        <div class="content">
            {{ .Content }}
        </div>
    </div>
</div>
{{ end }}
  • La classe container cadre le contenu sur les résolutions élevées
  • La classe section ajoute du padding pour éviter que le texte ne soit collé aux bords sur mobile
  • La classe content active la typographie par défaut de Bulma (titres, paragraphes, listes, etc.)

Ajout de styles personnalisés

Pour ajouter ses propres styles, créez un fichier CSS qui viendra surcharger les styles de Bulma.

Créez le fichier themes/sandbox/static/css/style.css (laissez-le vide pour l’instant).

Puis ajoutez cette ligne dans head.html, après la ligne qui charge Bulma (l’ordre compte : le dernier CSS chargé prime) :

<link rel="stylesheet" href="{{ "css/style.css" | relURL }}">

À ce stade, côté front, on devrait avoir un visuel propre avec navbar, contenu et footer.

Quand le contenu est peu rempli, le footer a tendance à remonter haut dans la page. Pour le coller en bas même avec peu de contenu, on transforme le <body> en conteneur flex.

Dans style.css, ajoutez :

body {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

main {
    flex: 1;
}

Le <body> devient un conteneur flex en colonne contenant 3 éléments : <header>, <main> et <footer>. En attribuant flex: 1 au <main>, il occupe tout l’espace vertical disponible, ce qui pousse mécaniquement le footer en bas.

Pour rappel sur Flexbox, ce guide est une excellente ressource : https://css-tricks.com/snippets/css/a-guide-to-flexbox/

Pour aller plus loin : l’asset pipeline

Pour cette série, on charge Bulma et notre CSS depuis /static/, ce qui est simple et fonctionne très bien. Mais Hugo propose une fonctionnalité plus puissante via le dossier /assets/ : c’est ce qu’on appelle l’asset pipeline.

Avec ce pipeline, vous pouvez :

  • Écrire votre CSS en SCSS/SASS et le compiler automatiquement (nécessite la version “extended” de Hugo)
  • Minifier le CSS et le JS automatiquement
  • Fingerprinter les fichiers pour le cache busting (style.abc123.css)
  • Bundler plusieurs fichiers en un seul pour réduire les requêtes HTTP

Je consacrerai un article bonus à ce sujet — pour l’instant, restons sur la version simple qui fait très bien le travail.

C’est tout pour aujourd’hui !

Voilà pour cette partie : notre site commence à ressembler à quelque chose, du moins sa page d’accueil. On va pouvoir s’attarder désormais sur les pages de contenu en commençant par les articles.

Vous devriez également aimer ce qui suit...