Pandoc HTML

From Markdown to HTML via GNU/Make and Pandoc

Génération statique de mon site web avec GNU/Make et Pandoc.

Jovian HERSEMEULE

2023-06-05

Article explicatif sur Pandoc piloté par GNU/Make pour générer mon site web statique.

Introduction

Cet article détaille le changement de technologie opéré en 2023 pour mon site web, et s’adresse à un public possédant certaines notions du développement web comme le HTML et l’hébergement, ainsi que les fondamentaux des systèmes *nix (GNU/Linux, Unix).

Historique

La première version de mon site, qui possède par ailleurs son propre article, souffre de quelques lacunes techniques qui m’ont poussé à engager une refonte.

En voici un rapide aperçu.

Adhérence technologique

Des traces de PHP sont utilisées pour factoriser des composants. J’impose un environnement d’exécution PHP pour le serveur d’hébergement alors que j’en ai une utilisation très partielle.

Complexité d’écriture

Je dois écrire le contenu en code bas niveau, en HTML ou PHP. De plus, certaines tâches sont manuelles. Par exemple, à chaque fois que je créée une nouvelle page pour un projet, je dois ensuite l’ajouter dans la page listant tous les projets.

Déploiement maison

Déploiement à base de scripts shell maison non standards, dont la compréhension, l’utilisation et la maintenance s’avéraient fastidieuses.

Planification

J’ai donc décidé de refondre mon site afin de résoudre ces points de friction.

Besoins

Mon désir est d’avoir une solution simple, efficace et durable.

Mon principal objectif est de faciliter la production de contenus sur mon site. Je suis un adepte du Markdown : j’utilise QOwnNotes pour prendre mes notes personnelles et professionnelles. J’admire sa simplicité et ses fonctionnalités, j’y suis familier, et je le juge tout à fait apte pour formater mon futur contenu.

J’avais également envie d’une solution élégante et techniquement intéressante, basée sur du logiciel libre dont je souhaite promouvoir les valeurs et l’usage.

Veille technologique

J’ai donc réalisé quelques recherches pour me documenter sur les solutions libres existantes qui pourraient répondre à mon besoin.

CMS

CMS signifie Content Management System : les CMS sont architecturés pour prendre en charge tous les besoins techniques d’édition de contenus avancée (authentification, droits, rédaction, publication, présentation) tout en les présentant de manière accessible via une interface graphique. Une des solutions libres les plus connues est Wordpress, mais nous pouvons aussi citer Wagtail.

J’ai hésité à utiliser un CMS pour m’épargner la tâche de structuration du contenu, mais plusieurs points m’ont amené à délaisser cette option : la difficulté de la migration, la complexité d’une IHM de gestion et la difficulté d’hébergement. J’avais envie d’une solution plus sobre.

Générateurs de site statiques

J’ai trouvé la notion de site statique très intéressante pour sa simplicité et sa sobriété. Le principe est d’avoir un service minimal côté serveur : les pages sont stockées au format brut HTML et seront distribuées telles quelles au clients webs. Cela facilite la gestion de l’hébergement, et augmente par ailleurs sa performance car aucune tâche n’est réalisée entre la réception de la requête et l’envoi de la réponse.

La conséquence de cela est que le contenu ne peut pas être interactif : une fois publié, il ne peut être modifié seulement par un nouveau déploiement.

L’inconvénient est qu’il faut produire ces pages au format brut. Afin de faciliter et d’industrialiser cette tâche, des outils de génération de contenus statiques sont apparus. On peut citer par exemple Jekyll, HUGO ou bien encore Grav.

Ces générateurs fournissent d’excellentes solutions et conviennent tout à fait à mon besoin, et je vous conseille de les utiliser si vous souhaitez faire quelque chose de sérieux.

Revenir aux bases

Je désirais cependant un peu plus de contrôle sur le processus de création, j’ai donc décidé d’explorer une voie un peu plus expérimentale et fondamentale.

J’ai eu en effet l’idée d’associer deux technologies différentes pour satisfaire mes besoins tout en me permettant de progresser techniquement.

La première s’appelle Pandoc, qui constitue une solution libre très connue pour convertir des documents. Je l’utilise notamment pour convertir mes notes prises en Markdown vers des langages de formatage compris par certains outils que j’utilise, comme les formats dokuwiki ou textile. Ce qui m’intéresse dans pandoc, c’est qu’il fonctionne très bien et sa responsabilité est simple : il convertit des documents. La conversion qui m’intéresse est celle qui permet d’obtenir un document HTML à partir d’un document Markdown.

La seconde s’appelle GNU make développée à la base pour gérer la compilation de programmes en C. J’en connais les bases suffisantes pour compiler mes projets en C++. GNU make prolonge l’idéologie des systèmes *nix qui prône que tout est fichier. Les fichiers makefile décrivent des recettes de fabrication en détaillant les relations entre les fichiers. J’y trouve là une solution élégante de développer un système de compilation tout en documentant la structure de mon projet.

Refonte

J’ai donc adopté Pandoc et Make pour propulser mon site web. Je vous guide pas à pas dans la mise en place de ces composants, en détaillant pour chaque problème rencontré la solution que j’ai mise en place.

Pandoc

Comment générer des pages HTML complètes à partir d’un article rédigé en markdown ?

carte des flux permettant a Pandoc de construire un fichier
Markdown en HTML

Prenons un exemple de base, dans lequel je décris un contenu simple sans contrainte technique.

# Titre 1

Bonjour **Markdown** ! Je parle de toi sur [mon site](www.jovian-hersemeule.eu).

Pour pouvoir le transformer en HTML, je peux invoquer pandoc avec le minimum d’arguments :

pandoc -o sortie.html entree.md
<h1 id="titre-1">Titre 1</h1>
<p>Bonjour <strong>Markdown</strong> ! Je parle de toi sur <a
href="www.jovian-hersemeule.eu">mon site</a>.</p>

Le résultat est celui attendu : pandoc réalise fidèlement son travail de conversion. On notera que pandoc ajoute automatiquement un identifiant id à chaque titre.

La syntaxe comprise par pandoc est un sur-ensemble du markdown, dont la description est disponible sur la documentation officielle.

Si vous souhaitez désactiver le retour à la ligne automatique, vous pouvez utilisez l’option --wrap=none.

HTML indépendant

L’exemple précédent produit un document HTML trop simple pour constituer une page web. On voudrait en effet entourer notre contenu avec des éléments de navigation, comme un menu d’en-tête et un pied de page.

Le fichier HTML produit est pour l’instant insuffisant pour être affiché en tant que page web pour le navigateur. En effet, le standard impose de caractériser la page, en utilisant les balises <!DOCTYPE html> pour préciser la version 5 du standard et la balise <head> qui peut contenir toutes sortes de paramètres. Cette encapsulation permet aussi de pouvoir styliser le contenu avec du CSS et d’ajouter des métadonnées pour caractériser notre contenu.

Pandoc a prévu le coup, et propose propose une option --standalone pour rendre notre document indépendant, c’est à dire lisible directement depuis un navigateur web.

pandoc --standalone -o sortie.html entree.md
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
  <meta charset="utf-8" />
  <meta name="generator" content="pandoc" />
  <!-- passage coupé pour éviter la surcharge -->
</head>
<body>
<h1 id="titre-1">Titre 1</h1>
<p>Bonjour <strong>Markdown</strong> ! Je parle de toi sur <a
href="www.jovian-hersemeule.eu">mon site</a>.</p>
</body>
</html>

On remarque ainsi que pandoc utilise un modèle pour rendre notre document auto-suffisant.

Templating

Pour contrôler le code ajouté autour de notre document pour le rendre indépendant, Pandoc propose un système de modèle.

Il reste maintenant à personnaliser ce modèle pour pouvoir lui ajouter notre style et nos menus de navigation. Pandoc assure et nous met à disposition une syntaxe pour écrire nos propres templates. En écrivant un template mon-template.html :

pandoc --template mon-template.html -o mieux.html entree.md

Remarquez que l’option --template implique l’option --standalone, que l’on peut donc retirer.

Autre avantage de cette fonctionnalité modèle, c’est qu’on peut aussi l’utiliser sur des pages que l’on souhaite garder en HTML. C’est par exemple le cas de ma page CV où j’ai fait des efforts de présentation pour la rendre adaptable à toute taille d’écran. Il suffit de supprimer les décorations du document HTML pour ne laisser que l’essentiel, et laisser pandoc rajouter les décorations :

pandoc --template mon-template.html -o encore.html deja.html

Attention cependant, pandoc n’est pas un moteur de template : la conversion HTML → HTML n’est pas idem-potente. En effet, pandoc utilise en interne une représentation intermédiaire appelée AST (Abstract Syntax Tree) qui sert d’aiguillage entre tous les formats. Nous avons déjà vu que le HTML rajoutait des identifiants inoffensifs, mais des légendes sont parfois ajoutées aux images et les classes disparaissent des balises qui ne sont pas des <div>.

Exemple de fichier source en HTML : src.html

<h1>Bonjour HTML</h1>
<img src="pingouin.jpg" alt="Un magnifique pingouin">
<p>
Le <b class="magnifique">pingouin</b> est un superbe animal.
</p>

Conversion depuis et vers l’HTML :

pandoc -o different.html src.html

Le fichier different.html obtenu :

<h1 id="hello-html">Hello HTML</h1>
<p><img src="pingouin.jpg" alt="Un magnifique pingouin" /></p>
<p>Le <strong>pingouin</strong> est un superbe animal.</p>
Les métadonnées

Les plus observateur·ices d’entre vous auront peut-être remarqué que l’attribut lang de la balise html était vide dans le code produit dans la version auto-contenue du document HTML.

<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">

En effet, pandoc implémente un système de méta-données qui est nécessaire pour piloter le rendu du template.

Voici à quoi ressemble la ligne en question dans le template :

<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>

Ainsi, si je précise la métadonnée manquante :

pandoc --metadata=lang:fr --standalone -o avec-lang.html entree.md

Cela donne :

<html xmlns="http://www.w3.org/1999/xhtml" lang="fr" xml:lang="fr">

Ce système de métadonnées me sera très précieux pour personnaliser les pages.

Avec l’ancienne technologie, j’instanciais une variable PHP avant d’inclure un fragment :

<?php
    $title = "Contact";
    include("include/head.php");
?>

Avec pandoc, la variable $title pourra être convertie en une métadonnée qui sera utilisée dans le template.

Le système de templating de pandoc est puissant tout en restant simple. Il est notamment possible de découper son template en parties, d’appliquer des conditions, et de réaliser des formatages simples.

Pour finir, afin de faciliter l’usage de plusieurs métadonnées, il est possible de les rajouter directement dans le fichier Markdown ou dans un fichier dédié :

author: Jovian HERSEMEULE
title: Bienvenue !
img:
        share:
                path: "/include/images/avatar-cartoon600.png"
                alt: Jovian avatar cartoon
description: Le site officiel de Jovian HERSEMEULE.
lang: fr

# Technical
version: development
date: 2023-06-18
css:
        - "/include/css/bootstrap.css"
        - "/include/css/custom/main.css"

On nommant ce fichier meta.yml, il peut être référencé via la ligne de commande :

pandoc --metadata-file=meta.yml --standalone -o avec-fichier-meta.html entree.md

Les arguments metadata-file et metadata peuvent être combinés ; cela permettra d’avoir d’une part des données statiques provenant de fichiers, comme le titre ou l’auteur, et d’autre part avoir des données calculées passées à la ligne de commande, comme la date ou la version.

Je fais un usage avancé des métadonnées pour ce nouveau site, ce qui sera détaillé dans la partie suivante dédiée à la construction.

GNU Make

Comment orchestrer la génération successive des fichiers pour aboutir à un site entier ?

graphe des dependances Make
La philosophie de Make

GNU make est un utilitaire d’aide à la compilation qui fonctionne à partir d’un makefile, un document de recettes qui décrit chaque fichier, comment le construire et quelles sont ses dépendances.

Si Make est nouveau pour vous, n’hésitez pas à consulter cette courte introduction qui vous permettra de vous familiariser rapidement avec le concept de base.

Les prochaines parties vous renseigneront en même temps sur la structure de mon projet et sur mon usage de make.

Convertir un fichier source

Tous les fichiers sources du projet sont dans le dossier src. Les fichiers qui portent le contenu texte au format source possèdent la particule src dans leur nom.

Exemples de fichiers de contenu :

src/index.src.html
src/cv/content.src.html
src/projets/pandoc_site/content.src.md

Peu importe le radical du fichier, mais comme mon arborescence est suffisamment fine, je peux me permettre de les appeler tous content.

J’ai donc écrit une recette pour appliquer pandoc sur ces fichiers source, en décidant d’utiliser le suffixe .gen.html pour les fichiers produits.

PANDOC_GEN := pandoc --wrap=none --template src/templates/main.html --metadata-file src/meta.yml

%.gen.html: %.src.md
    $(PANDOC_GEN) --output $@ $<

%.gen.html: %.src.html
    $(PANDOC_GEN) --output $@ $<

Ce qui donne en pratique :

make src/cv/content.gen.html
pandoc --wrap=none --template src/templates/main.html --metadata-file src/meta.yml --output src/cv/content.gen.html src/cv/content.src.html

On obtient donc les sorties dérivées des fichiers sources.

Produire tous les fichiers sources

Maintenant que Make connaît la relation entre un fichier source et sa version compilée, je vais créer une recette permettant de générer tous les fichiers avec une instruction build.

.PHONY: build
build: $(GEN_CONTENT_FILES)
    $(info HTML generated)

Que je pourrai appeler de la manière suivante pour compiler toutes mes sources :

make build

J’ai utilisé plusieurs variables successives avant d’aboutir au calcul de la liste de tous les fichiers à générer contenue dans GEN_CONTENT_FILES :

SRC_HTMLS := $(shell find src -name '*.src.html')
SRC_MDS := $(shell find src -name '*.src.md')

GEN_HTMLS := $(SRC_HTMLS:%.src.html=%.gen.html)
GEN_MDS := $(SRC_MDS:%.src.md=%.gen.html)
GEN_CONTENT_FILES := $(GEN_HTMLS) $(GEN_MDS)

J’utilise la commande shell find pour trouver les fichiers sources et la mécanique de translation de nom native à GNU Make pour en déduire la liste des noms à générer. Ce que va comprendre Make sera :

SRC_HTMLS := src/index.src.html src/cv/content.src.html
SRC_MDS := src/projets/pandoc_site/content.src.md

GEN_HTMLS := src/index.gen.html src/cv/content.gen.html
GEN_MDS := src/projets/pandoc_site/content.gen.html
GEN_CONTENT_FILES := src/index.gen.html src/cv/content.gen.html src/projets/pandoc_site/content.gen.html

La cible build a donc en dépendance tous les fichiers *.gen.html qui peuvent être construits, et utilisera les deux recettes présentées dans la partie précédente pour les générer s’ils sont absents ou si la source est plus récente que le fichier généré.

J’ai également créé une recette pour pouvoir nettoyer mon répertoire :

.PHONY: clean
clean:
    rm -f $(GEN_FILES)

Et j’ai rajouté les fichiers produits dans mon .gitignore pour éviter de les versionner.

*.gen.html
Produire un dossier distribuable

Pour l’instant, chaque fichier dérivé *.gen.html est généré dans le même dossier que son fichier source *.gen.* associé. Cela peut être pratique pour comparer la source et son équivalent généré, mais cela ne convient pas pour un déploiement. Le besoin est donc de créer un dossier contenant tous les fichiers à déployer, mais seulement les fichiers à déployer.

.PHONY: install
install: $(DIST_DIR)
    $(info Distributed folder generated)

$(DIST_DIR): $(DIST_FILES)

$(DIST_DIR)/%.html: src/%.gen.html
    mkdir --parents $(dir $@)
    cp $< $@

Ces recettes accomplissent deux objectifs :

Il reste à construire la liste DIST_FILES des fichiers à distribuer.

DIST_DIR := dist

DIST_GEN_FILES := $(GEN_CONTENT_FILES:src/%.gen.html=$(DIST_DIR)/%.html)

Ce qui donne lorsque que l’on invoque make install :

Le dossier en question sera dist par défaut, mais pourra être surchargé pour déployer sur un serveur web local de test par exemple.

DIST_DIR=/var/www/html make -e install

Notez l’usage de l’option -e de make pour prendre en compte les variables d’environnement afin de surcharger la valeur par défaut présente dans le fichier.

Il ne faut pas oublier que parmi les fichiers à déployer figurent aussi des ressources autres que tu contenu HTML : les fichiers de style CSS, les images ou bien les éventuels scripts JS.

Pour inclure l’ensemble de ces fichiers, on les déclare en tant que ressources à distribuer :

INCLUDED_FILES := $(shell find src/include -type f)
EMBEDDED_FILES := $(shell find src -name '*.png' -or -name '*.svg' -or -name '*.pdf')
DIST_INC_FILES := $(INCLUDED_FILES:src/%=$(DIST_DIR)/%)
DIST_EMB_FILES := $(EMBEDDED_FILES:src/%=$(DIST_DIR)/%)
DIST_FILES := $(DIST_GEN_FILES) $(DIST_INC_FILES) $(DIST_EMB_FILES)

Dans mon cas, les ressources partagées comme les scripts JS et styles CSS sont regroupées dans le dossier src/include et listées dans INCLUDED_FILES. Alors que les fichiers image peuvent être présents dans le dossier contenant la page dans laquelle ils sont inclus ; la recherche est donc basée sur les extensions de fichier désirées car afin de pouvoir explorer tous les dossiers. Les fichiers ainsi trouvés sont listés dans EMBEDDED_FILES.

La variable DIST_FILES contient au final la totalité de tous les fichiers distribuables.

Pour que make puisse copier ces fichiers sans modification, on rajoute une règle générique :

$(DIST_DIR)/%: src/%
    mkdir --parents $(dir $@)
    cp $< $@

Et pour terminer, une recette, que certain·es pourraient trouver un peu dangereuse à cause du risque de perte de données, dont l’objectif est de nettoyer ce dossier de distribution :

.PHONY: clean-dist
clean-dist:
    rm -rf $(DIST_DIR)/*

Et enfin, une recette pour tout nettoyer, à la fois les fichiers générés *.gen.html et le dossier de distribution :

.PHONY: clean-all
clean-all: clean clean-dist
Automatiser le déploiement distant

Le déploiement d’un site statique n’est en réalité qu’une copie simple des fichiers générés dans le dossier de distribution vers le dossier de notre hébergeur.

Pas de magie ni d’élégance dans cette partie : j’ai amélioré mon vieux script maison sans utiliser Make. Les seules évolutions que j’ai réalisées sur cette partie lors de la refonte est l’utilisation de sftp à la place du ftp simple. Le s signifie sécurisé, et évite que mon mot de passe ftp transite en clair sur le réseau. La seconde évolution consiste à créer automatiquement les nouveaux dossiers, ce qui est important quand on sait que je créée maintenant un dossier par projet.

J’utilise le shell fish pour écrire mon script. Voici un extrait qui montre l’agencement des différentes commandes (j’ai omis le contrôle des variables):

#!/usr/bin/fish

set make_dirs (find $DIST_DIR -type d -printf "mkdir $FTP_DEST/%P\n")
set files (find $DIST_DIR -type f)

set ftp_sync "put -R $DIST_DIR/* $FTP_DEST"
printf %s\n $make_dirs $ftp_sync 'bye' | sftp -P $FTP_PORT $FTP_USER@$FTP_HOST

On reconnaît la variable DIST_DIR qui était également utilisée dans mon makefile. Pour définir automatiquement toutes les variables sans versionner d’informations sensibles, toutes sont regroupées dans un fichier dédié .env.local :

#!/bin/usr/fish
set --export --global DIST_DIR /var/www/html

set --export --global FTP_HOST 'ftp.my-host.fr'
set --export --global FTP_USER 'me'
set --export --global FTP_PORT '2222'
set --export --global FTP_DEST 'www'

Pour injecter les variables dans mon environnement courant, j’utilise la commande source :

source .env.local

YQ

Comment générer la page listant tous les projets ?

En plus de Pandoc et GNU Make, j’ai eu besoin d’une troisième dépendance afin de manipuler programmatiquement les métadonnées stockées dans des fichiers YAML.

illustration liste des liens vers chaque page projet
Recenser les projets

Mon besoin est d’avoir une page qui recense tous mes projets. Chaque projet possède un encart avec une description, une illustration et une icône, ainsi qu’un lien vers la page détaillée qui permet d’en savoir plus.

L’objectif serait d’automatiser la découverte des projets et la génération de cette page d’indexation.

Description d’une page de projet

Avant toute chose, regardons comment est structuré un projet.

Le contenu du fichier content.meta.yml contient toutes les métadonnées utilisées dans le modèle Pandoc qui sont spécifiques à la page associée :

---
title: Pandoc HTML
img:
        icon:
                path: "pandocHtmlIcon.png"
                alt: "Pandoc : HTML document with Markdown inside it"
        main:
                path: "pandocMakeHtmlLogo.png"
                alt: "From Markdown to HTML via GNU/Make and Pandoc"
subtitle: Génération statique de mon site web avec GNU/Make et Pandoc.
content-date: 2023-06-05
content-description:
        Article explicatif sur Pandoc piloté par GNU/Make
        pour générer mon site web statique.
...

Ces métadonnées du contenu, en plus d’enrichir les métadonnées des documents HTML générés, sont utilisés dans mon modèle personnalisé de pandoc pour générer du contenu visible normalisé :

<main role="main" class="container">
$if(title)$
            <header id="title-block-header" class="focus-center">
                <h1 class="title">$title$</h1>
$if(img.main)$
                <img class="img-fluid rounded" src="$img.main.path$"$if(img.main.alt)$ alt="$img.main.alt$"$endif$>
$elseif(img.icon)$
                <img class="img-fluid rounded" src="$img.icon.path$"$if(img.icon.alt)$ alt="$img.icon.alt$"$endif$>
$endif$

Pouvoir parcourir les métadonnées de tous les projets est une bonne piste pour générer automatiquement une page indexant les projets.

Itérer sur la liste des projets

Étant donné une liste de projets au format yaml, nous pouvons utiliser les parties du moteur de template de Pandoc pour l’appliquer à chaque élément de la liste et ainsi formater une liste de projets.

Étant donné un fichier yaml listant tous les projets (je l’ai tronqué pour un souci de concision) que l’on pourrait passer en entrée à Pandoc via l’argument --metadata-file :

list:
  - data:
      title: ASCII Space Destroyer
      img: {}
      subtitle: Jeu vidéo réalisé en C++
      content-date: 2015-08-15
      content-description: ASCII description
    target:
      content: content.html
      dir: ascii_space
  - data:
      title: Pandoc HTML
      img: {}
      subtitle: Génération statique de mon site web avec GNU/Make et Pandoc.
      content-date: 2023-06-05
      content-description: Pandoc description
    target:
      content: content.html
      dir: pandoc_site

Il est possible d’utiliser les parties de Pandoc pour itérer sur chaque projet et générer l’aperçu associé. Voici l’extrait de code à ajouter au template principal :

$if(list)$
            <div class="card-columns">
                    $^$${ list:card() }
            </div>
$endif$

Il faut alors que le template card.html existe et soit dans le même dossier que le modèle principal, l’élément de la liste courant étant accessible via la variable it :

<div class="card">
    <img class="card-img-top" src="$it.target.dir$/$it.data.img.main.path$"$if(it.img.main.alt)$ alt="$it.data.img.main.alt$"$endif$>
    <div class="card-body">
        <h4 class="card-title">
            <img class="icon" src="$it.target.dir$/$it.data.img.icon.path$"$if(it.img.icon.alt)$ alt="$it.data.img.icon.alt$"$endif$>
            $it.data.title$
        </h4>
        <p class="card-text">
            $it.data.content-description$
        </p>
        <a href="$it.target.dir$/$it.target.content$" class="btn btn-success btn-block">
            Page du projet
        </a>
    </div>
</div>

Côté makefile, il faut créer une recette particulière pour ajouter le fichier list.gen.yml pour la page listant les projets seulement, sinon les projets seraient listés dans toutes les pages.

DIR_OF_PROJECTS := src/projets
GEN_LIST_YML := $(DIR_OF_PROJECTS)/list.gen.yml

$(DIR_OF_PROJECTS)/content.gen.html: $(DIR_OF_PROJECTS)/content.src.html $(DIR_OF_PROJECTS)/content.meta.yml $(GEN_LIST_YML) $(SRC_TEMPLATES)
    $(PANDOC_GEN) --metadata-file $(DIR_OF_PROJECTS)/content.meta.yml --metadata-file $(GEN_LIST_YML) --output $@ $<

Si le dossier list.gen.yml existe, tout est prêt pour générer automatiquement la page des projets.

make src/projets/content.gen.html

Il reste à déterminer comment construire ce nécessaire fichier list.gen.yml.

Construire automatiquement la liste de projets

L’enjeu est donc de construire la liste YAML des projets automatiquement à partir des metadonnées de chaque projet pour éviter la double saisie.

J’ai pour cela eu recours à yq, qui est un utilitaire qui permet de manipuler les fichiers yaml.

La première étape consiste à simplement aggréger les fichiers yaml présents dans tous les sous-dossiers du dossier projets en un seul fichier.

yq eval-all '. as $item ireduce ([]; . + $item) | {"list": .}' projets/*/*.meta.yml

On obtient alors le code yaml suivant dans la sortie standard :

list:
  - title: ASCII Space Destroyer
    img: {}
    subtitle: Jeu vidéo réalisé en C++
    content-date: 2015-08-15
    content-description: ASCII description
  - title: Pandoc HTML
    img: {}
    subtitle: Génération statique de mon site web avec GNU/Make et Pandoc.
    content-date: 2023-06-05
    content-description: Pandoc description

La deuxième partie fut un peu plus acrobatique et consistait à construire une entrée target pour chaque élément afin de construire le lien vers la page du projet concerné. Heureusement, yq fournit une variable filename contenant le nom du fichier origine du fichier yaml en cours de traitement.

yq eval-all '{"data": ., "target": filename} as $item ireduce ([]; . + $item) | {"list": .}' projets/*/*.meta.yml

Vous remarquerez que j’en ai aussi profité pour regrouper les données brutes dans le bloc data :

list:
  - data:
      title: ASCII Space Destroyer
      img: {}
      subtitle: ASCII subtitle
      content-date: 2015-08-15
      content-description: ASCII description
    target: ascii_space/content.meta.yml
  - data:
      title: Pandoc HTML
      img: {}
      subtitle: Pandoc subtitle
      content-date: 2023-06-05
      content-description: Pandoc description
    target: pandoc_site/content.meta.yml

Il reste maintenant à créer deux entrées dans target : une pour le dossier du projet, une autre pour le nom de fichier de contenu. Cette séparation est nécessaire pour que les images inclues dans la liste des projets soient relatives au dossier du projet.

Cette opération est possible avec yq, grâce à sa fonction de substitution sub qui accepte les expressions régulières en argument. La lisibilité est cependant fortement impactée, accrochez vous :

yq eval-all '{"data": ., "target": {"content": filename | sub("meta.yml", "html") | sub(".*/", ""), "dir": filename | sub("projets/", "") | sub("/[^/]*.yml", "")}} as $item ireduce ([]; . + $item) | {"list": .}' projets/*/*.meta.yml

Mais cela permet bien d’obtenir l’aggrégation nécessaire à notre modèle Pandoc.

list:
  - data:
      title: ASCII Space Destroyer
      img: {}
      subtitle: ASCII subtitle
      content-date: 2015-08-15
      content-description: ASCII description
    target:
      content: content.html
      dir: ascii_space
  - data:
      title: Pandoc HTML
      img: {}
      subtitle: Pandoc subtitle
      content-date: 2023-06-05
      content-description: Pandoc description
    target:
      content: content.html
      dir: pandoc_site

Je vous laisse parcourir la documentation yq pour déchiffrer les commandes yq. Je vous préviens, un peu de pratique est nécessaire pour en maîtriser la puissance.

Il reste un dernier effort à accomplir pour injecter cette commande dans notre makefile :

DIR_OF_PROJECTS := src/projets
GEN_LIST_YML := $(DIR_OF_PROJECTS)/list.gen.yml

SRC_LIST_YML := $(shell echo $(DIR_OF_PROJECTS)/*/*.meta.yml)

LIST_GEN := yq eval-all '{"data": ., "target": {"content": filename | sub("meta.yml", "html") | sub(".*/", ""), "dir": filename | sub("$(DIR_OF_PROJECTS)/", "") | sub("/[^/]*.yml", "")}} as $$item ireduce ([]; . + $$item) | {"list": .}'

$(GEN_LIST_YML): $(SRC_LIST_YML)
    $(LIST_GEN) $(SRC_LIST_YML) > $@

Et voilà, nous avons achevé notre makefile permettant la génération complète de mon site web !

Bonus coloré

Avantage collatéral d’avoir utilisé Pandoc, c’est que j’ai pu bénéficier de la coloration syntaxique. Et cela sans effort supplémentaire !

Lorsque j’écris dans mon fichier markdown :

```php
<?php
        include("include/navbar.php")
?>
```

Cela donne dans le rendu final :

<?php
        include("include/navbar.php")
?>

Je ne comptais pas sur cette fonctionnalité, mais elle m’a beaucoup servi dans cet article, en permettant de rendre les extraits de code plus agréables à lire.

Pour connaître la liste des langages colorisés par Pandoc, il suffit de lancer la commande suivante :

pandoc --list-highlight-languages

Pour aller plus loin

Voici quelques idées d’amélioration possibles pour mon générateur de site.

Minimiser l’empreinte du site produit

Scanner les fichiers inclus depuis les fichiers sources et les indiquer comme étant ressources à distribuer.

Empêche le déploiement de fichiers inutiles, en permettant de les détecter et de les supprimer.

Minifier les fichiers déployés

Enlever les caractères blancs, les commentaires, supprimer le CSS ou JS inutilisé.

Auto-compiler à chaque modification

Déclencher un make install à chaque sauvegarde de fichier avec un watcher, ou bien après chaque commit avec un post-commit git hook.

Utiliser les fonctionnalités de référence de Pandoc

Pour avoir un sommaire et des références en bas de page. Vous aurez peut-être remarqué que le lien sur les parties Pandoc était présent à plusieurs endroits, ce qui n’est pas facile à maintenir en cas de changement de lien ou en cas d’envie d’avoir une liste complète des références.

Augmenter les métadonnées de chaque projet

Rajouter des étiquettes pour catégoriser les projets, rajouter des références comme des liens vers les sites externes (code source, page de téléchargement, etc.).

Ajouter des recettes de création de composant

Ajouter une recette qui permet de créer un squelette de nouveau projet en une commande.

Valider les metadata

Mettre en place un YAML schéma pour s’assurer que les metadata sont bien au bon format.

Localisation

Assigner une langue à chaque fichier de contenu, pour pouvoir sauvegarder des traductions. Et structurer le site pour permettre au visiteur de choisir sa langue.

Conclusion

J’ai donc construit un système plus léger au niveau de la rédaction de contenu qui me permettra de facilement produire du nouveau contenu en markdown. J’ai troqué la dépendance serveur PHP contre deux dépendances au build, pandoc et GNU Make. L’opération est donc un succès et j’utilise aujourd’hui ce projet en production.

Cet article est le premier réalisé avec la pile technique que je viens de vous présenter. Je vous confirme que j’ai gagné en confort, en employant des technologies stables que je maîtrise déjà.

Mais cela reste un projet d’exploration que je ne recommande pas forcément pour un usage pro. Ces choix souffrent de quelques limites : j’arrive aux limites du moteur de template de pandoc. Et contrairement à Jekyll, je ne dispose pas (encore) de rechargement à chaud. Pour les besoins de mon site ce n’est pas très grave car il est déjà construit et je compte seulement rajouter des projets ou articles, mais si j’ai besoin de faire quelque chose de sérieux j’utiliserai Jekyll au autre technologie destinée à cet usage.

Merci pour votre lecture !

Refs