/*
 * https ; gemini ;
 * tout ; log ; twtxt ;
 * à propos (@) ; ;
 */

Maintenir un site statique avec un Makefile

Il y a des choses que j'ai longtemps mis de côté dans la longue liste des choses à "apprendre un jour sérieusement ça serait cool de savoir faire comme il faut" : les regex, sed, awk, tmux, des techniques de go (le jeu)... Parmi tout ça, il y a les Makefiles. Apprendre à écrire un Makefile est la meilleure des choses qui me soient arrivée depuis longtemps (en terme informatique, évidemment 😊).

Après avoir utilisé et même développé des générateurs de sites statiques pendant des années, j'ai désormais avec un simple Makefile le plaisir de pouvoir contrôler le rendu sans prise de tête. Je peux lister au moins les avantages suivants :

Je vous propose donc de décortiquer un fichier Makefile : celui qui me sert à maintenir ce site.

Organisation

Voici la structure que j'utilise:

$ ls -1
Makefile
bin/
foot.tpl
head.tpl
www/

À côté du Makefile, on peut trouver :

Je ne l'ai pas dit, j'utilise des fichiers au format gemtext tout simplement parce que je propose aussi mon site via le protocole gemini.

Ces fichiers sont convertis en html avec l'outil gemtext2html, à côté du fichier original. On pourrait très bien changer ce comportement et avoir un dossier ne content que du html à part, c'est assez simple à modifier. C'est juste plus pratique pour moi ainsi.

On pourrait aussi utiliser tout autre chose que du gemtext : du markdown, voire même du html directement. On remplacerait dans ces cas, respectivement, gemtext2html par un convertisseur markdown ou par la commande "cat".

Préparons le Makefile

On commence par indiquer à "make" quelles extensions seront à traiter avec ".SUFFIXES".

.SUFFIXES: .gmi .html

Puisque je convertis des fichiers vers du html, je vais définir une commande "TOHTML" que vous pourrez remplacer par le convertisseur de votre choix :

TOHTML = gemtext2html -b

Cela nous permettra ensuite de lui apprendre comment traiter les fichiers "gmi" pour les transformer en "html":

.gmi.html:
        @echo "$< -> $@"
        @cat head.tpl > $@
        @${TOHTML} < $< >> $@
        @cat foot.tpl  >> $@
        @bin/title.sh $@

Ici, "$@" correspond au fichier que l'on veut obtenir (page.html) et "$<" au fichier source (page.gmi). Comme vous pouvez le lire, la démarche est toute bête :

#!/bin/sh
# bin/title.sh
# add title after <title> line according to h1 found
DEFAULT=" "
TITLE="$(grep -m1 "<h1>" "${1}"| \
sed -n -e 's/.*<h1>\(.*\)<\/h1>.*/\1/p') — "

test -z "${TITLE}" && TITLE="${DEFAULT}"
sed -i "/<title>/a\\
${TITLE}" "${1}"

Maintenant que make sait comment traiter les fichiers "gmi", on va lui indiquer lesquels existent. La commande "find" est toute adaptée.

SRCDIR = www
SRC != find ${SRCDIR} -type f -name \*.gmi \
        -a ! -path ${GMILIST} \
        -a ! -path ${GMILOG} \
        -a ! -path ${SRCDIR}/code/\* \
        -a ! -path ${SRCDIR}/ah/\* \
        | sort

Ici, utiliser "!=" permet de remplir la variable "SRC" par le retour de la commande suivante. À ce propos :

On peut maintenant définir une variable qui contient la liste de tous les fichiers que l'on veut obtenir. Il s'agit de la liste précédente, en remplaçant les extensions "gmi" par "html" :

HTML = ${SRC:.gmi=.html}

Le plus dur est fait 😊.

Reste à ajouter une instruction indiquant qu'on veut traiter la liste précédente :

all: ${HTML}

Un simple "make" permettra désormais d'obtenir tous les fichiers "html" qui auraient besoin d'être mis à jour ou qui n'existent pas. Et c'est à mon avis la partie la plus intéressante : on ne s'occupe pas des fichiers déjà à jour, c'est un gain de temps considérable.

Allons un peu plus loin

Maintenant qu'on a une base qui fonctionne bien, on va ajouter les éléments suivants :

Liste de toutes les pages

On commence par indiquer où se situera le fichier contenant la liste de toutes les pages:

GMILIST = ${SRCDIR}/all/index.gmi
HTMLLIST = ${GMILIST:.gmi=.html}

Vous remarquez que je précise avant tout un fichier "gmi" avant de changer son extension car je veux pouvoir proposer une liste aussi avec le protocole gemini.

Pour pouvoir justement générer cette liste, je vais passer à un script la liste de toutes les pages du site contenues dans "${SRC}" :

${GMILIST}: ${SRC}
    @echo "$@"
    @bin/gmilist.sh ${GMILOG} ${SRC} > $@

Vous remarquerez que "${GMILIST}" dépend de "${SRC}" : cela permet de mettre à jour cette page à chaque fois que le contenu de "${SRC}" a changé.

On appelle donc un script "gmilist.sh" qui lit ce qu'on lui donne en argument pour ressortir une liste au format gemini

#!/bin/sh

printf '%s\n\n' "# Tout"

for l in $@
do
        TITLE="$(grep -m1 "#" "${l}"| sed 's/^#//')"
        if [ -n "${TITLE}" ]; then
                gemini="$(printf "%s" ${l} | cut -d'/' -f2- )"
                printf '=> /%s    %s\n' "${gemini}" "${TITLE}"
        fi
done

Il faut toutefois que la page html correspondante soit générée. Il faut alors jouer un peu avec les dépendances :

all: gemini html
gemini: ${GMILIST}
html: ${HTMLLIST} ${HTML}

Ainsi, lorsqu'on lance "make", les cibles "gemini" et "html" seront à produire dans l'ordre.

La cible "gemini" dépend de "${GMILIST}" indiquée juste avant.

La cible "html" contient toutes nos page + la liste de toutes les pages à convertir.

Création d'un flux ATOM

On ajoute le chemin vers le fichier atom à générer :

ATOM = ${LOGDIR}/atom.xml

On l'ajoute dans la liste des éléments à produire :

all: gemini html ${ATOM}

Enfin, on ajoute une règle indiquant comment produire ce fichier :

${ATOM}: ${LOGSRC}
    @echo "$@"
    @bin/atom.sh ${LOGSRC} > $@

Ce dernier ne sera généré que si une liste qu'on n'a pas encore rencontré jusqu'ici est modifiée : "${LOGSRC}". Il s'agit de la liste de tous mes billets de blog, que j'obtiens ainsi :

LOGDIR = ${SRCDIR}/log
GMILOG = ${LOGDIR}/index.gmi

LOGSRC != find ${LOGDIR} -type f -name \*.gmi \
    -a ! -path ${GMILOG} \
    | sort -r

Le "sort -r" permet d'avoir une liste listant les fichiers les plus récents en premier.

Pour que ça fonctionne comme prévu, je dois nommer les fichiers en indiquant la date pour éviter les surprises. Par exemple "2021-01-27-billet-de-blog.gmi".

Le script "atom.sh" est un peu plus complexe, et profite de la façon dont les fichiers sont nommés. Il est spécifique à OpenBSD car la commande "stat" permet d'obtenir la date de dernière modification d'un fichier, et le format n'est pas le même sur une distro GNU :

#!/bin/sh
# files must be YYYY-MM-DD.gmi
max=42
domain="si3t.ch"
i=0

feed_updated=$(date +%Y-%m-%dT%TZ)

cat <<EOF
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://${domain}/log/</id>
<title>(B|Gem)log de prx</title>
<icon>https://${domain}/favicon.png</icon>
<link rel="alternate" type="text/html" href="https://${domain}/log/" />
<link rel="alternate" type="text/gemini" href="gemini://${domain}/log/" />
<link rel="self" type="application/atom+xml" href="/log/atom.xml" />
<author><name>prx</name></author>
<updated>${feed_updated}</updated>
EOF

for line in $@; do
	test $i -ge $max && break
	i=$(($i+1))
	f="$(basename ${line})"
	created="${f%.*}"

	published="$(date -ur $(stat -f %c ${line}) +%Y-%m-%dT%TZ)"
	updated="$(date -ur $(stat -f %m ${line}) +%Y-%m-%dT%TZ)"
	title="$(grep -m1 "#" "${line}"| sed -E 's/^# ?//')"
	link="$(printf "%s" ${line} | cut -d'/' -f2- | sed 's/\.gmi$/.html/')"
	tag="$(basename ${line} | sha256)"

	cat <<EOF
<entry>
<id>tag:${domain},${created}:${tag}</id>
<title type="text">${title}</title>
<updated>${updated}</updated>
<published>${published}</published>
<link rel="alternate" href="/${link}"/>
<summary type="text">
	À lire sur: gemini://${domain}/${link} ou sur https://${domain}/${link} -- 

	$(sed -n '2,7p' ${line})
	...
</summary>
</entry>
EOF
done

echo "</feed>"

Au niveau du contenu, je n'indique pas l'article en entier par fainéantise. C'est tout à fait possible, mais je ne veux pas publier du html alors que je propose mon site au format gemini. J'ai voulu mettre directement du texte brut, mais les retours à la ligne ne sont pas respectés par les lecteurs de flux, donnant juste un gros bloc de texte moche. À la place, j'affiche les 5 premières lignes après le titre.

Création d'un sitemap

On crée une nouvelle cible :

SITEMAP = ${SRCDIR}/sitemap.xml
...
all: gemini html ${SITEMAP} ${ATOM}
...
${SITEMAP}: ${SRC}
        @echo "$@"
        @bin/sitemap.sh > ${SITEMAP}
        @gzip --best -c ${SITEMAP} > ${SRCDIR}/sitemap.gz

${SITEMAP} dépend de ${SRC}, notre liste de pages. Cela veut dire que le sitemap est renouvelé lorsqu'un changement a lieu sur le site.

Le script sitemap est un peu différent : il va rappeler la commande "find" au lieu de lire une liste en entrée car je veux pouvoir prendre en compte des fichiers html que j'aurais obtenu par un autre moyen que par conversion d'un fichier gmi:

#!/bin/sh
ndd="si3t.ch"

printf '%s\n' \
'<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'

find www -name \*.html | while read -r l
do
	link="$(printf "%s" ${l} | cut -d'/' -f2-)"
	printf '<url><loc>%s</loc><lastmod>%s</lastmod></url>\n' \
		"https://${ndd}/${link}"\
		"$(date -r $(stat -f %m ${l}) +%Y-%m-%d)"
done

printf '%s' '</urlset>'

Aperçu du site

Pour avoir un aperçu du site avant upload, on entrera "make serve" puis on ouvre un navigateur à l'adresse http://localhost:8000 :

serve:
        cd ${SRCDIR} && python3 -m http.server

Mise à jour automatique lors d'un changement

On outilise pour ça l'outil "entr" et on entre "make entr":

entr:
        find . -type f -name \*.gmi | entr make

Mise en ligne du site

On entre "make upload". J'utilise des clés SSH pour ne pas avoir à entrer de mot de passe.

SSHCRED = pi
REMOTEDIR = /var/www/htdocs/si3t.ch/

upload: all
        openrsync -e "ssh" -rv ${SRCDIR}/ ${SSHCRED}:${REMOTEDIR}

Vous remarquerez que la règle "upload" dépend de "all". Ainsi, si j'ai modifié une page, je peux directement générer et uploader avec "make upload" d'un coup.

Régénération du site

On peut nettoyer avec "make clean"

clean:
        find . -name \*.html -delete

Pour finir

Pouvoir jouer avec les "dépendances" avec un Makefile est à mon avis très pratique. De pus, puisqu'on peut y intégrer des commandes shell, "find" devient un compagnon vraiment efficace.

make + find = ♥

Voici à quoi ressemble mon Makefile à l'heure actuelle. Il y a en plus de ce qu'on a vu avant des instructions spécifiques à la partie "blog" de mon site que je ne souhaite pas lister de la même façon.

.SUFFIXES: .gmi .html

# configuration
SRCDIR = www
# converter
TOHTML = gemtext2html -b
# SSH
SSHCRED = coco
REMOTEDIR = /var/www/htdocs/si3t.ch/

# files
SITEMAP = ${SRCDIR}/sitemap.xml
LOGDIR = ${SRCDIR}/log
ATOM = ${LOGDIR}/atom.xml

GMILIST = ${SRCDIR}/all/index.gmi
GMILOG = ${LOGDIR}/index.gmi

HTMLLIST = ${GMILIST:.gmi=.html}
HTMLLOG = ${GMILOG:.gmi=.html}

LOGSRC != find ${LOGDIR} -type f -name \*.gmi \
	-a ! -path ${GMILOG} \
	| sort -r

SRC != find ${SRCDIR} -type f -name \*.gmi \
	-a ! -path ${GMILIST} \
	-a ! -path ${GMILOG} \
	-a ! -path ${SRCDIR}/code/\* \
	-a ! -path ${SRCDIR}/ah/\* \
	| sort
HTML = ${SRC:.gmi=.html}

all: gemini html ${SITEMAP} ${ATOM}
gemini: ${GMILIST} ${GMILOG}
html: ${HTMLLIST} ${HTMLLOG} ${HTML}

${GMILIST}: ${SRC}
	@echo "$@"
	@bin/gmilist.sh ${GMILOG} ${SRC} > $@

${HTMLLIST}: ${GMILIST}
	@echo "$@"
	@cat head.tpl > $@
	@sed "s/\.gmi/.html/g" ${GMILIST} | ${TOHTML} >> $@
	@cat foot.tpl  >> $@
	@bin/title.sh $@

${GMILOG}: ${LOGSRC}
	@echo "$@"
	@bin/gmilog.sh ${LOGSRC} > $@

${HTMLLOG}: ${GMILOG}
	@echo "$@"
	@cat head.tpl > $@
	@sed "s/\.gmi/.html/g" ${GMILOG} | ${TOHTML} >> $@
	@cat foot.tpl  >> $@
	@bin/title.sh $@

${ATOM}: ${LOGSRC}
	@echo "$@"
	@bin/atom.sh ${LOGSRC} > $@

.gmi.html:
	@echo "$< -> $@"
	@cat head.tpl > $@
	@${TOHTML} < $< >> $@
	@cat foot.tpl  >> $@
	@bin/title.sh $@

${SITEMAP}: ${SRC}
	@echo "$@"
	@bin/sitemap.sh > ${SITEMAP}
	@gzip --best -c ${SITEMAP} > ${SRCDIR}/sitemap.gz

clean:
	find . -name \*.html -delete

serve:
	cd ${SRCDIR} && python3 -m http.server

entr:
	find . -type f -name \*.gmi | entr make

upload: all
	openrsync -e "ssh" -rv ${SRCDIR}/ ${SSHCRED}:${REMOTEDIR}

Liens

man make
Format gemtext
gemini
gemtext2html.html
gemtext2html.gmi
Configuration de SSH

Une réaction?

📧 Envoyez votre commentaire par mail.
📫 Abonnez-vous pour recevoir les réponses
📚 Consultez les archives.
💨 Vous désinscrire