Disclaimer : je propose ici une solution technique, et un workflow de publication, que j’ai adopté. La force des sites statiques étant leur grande souplesse, chaque partie détaillée dans cet article est interchangeable avec une autre solution de votre choix. Aussi, j’aurais pu utiliser Netlify qui est adapté aux sites statiques avec leur CDN, mais je ne souhaitais pas changer mon DNS racine.

Overview

Les chapitres suivants sont résumés dans ce diagramme :

Overall architecture diagram

Le moteur de blog : Hugo

Au coeur de notre workflow, il y a l’outil qui va nous permettre de construire notre site final, le blog. Pour produire un site dont le but est d’exposer du contenu accessible en lecture, les générateurs de sites statiques sont l’outil idéal, grâce à la souplesse qu’ils apportent.

À la différence d’un CMS (par exemple WordPress ou Drupal) qui traite dynamiquement chaque requête client via du code serveur et/ou une base de données, les générateurs de sites statiques produisent l’intégralité des fichiers HTML qui pourront être accessibles.

Par exemple, cet article que vous êtes en train de lire est un simple fichier HTML, qui a été généré à partir d’un fichier de contenu écrit en Markdown. De plus, le tag hugo est utilisé. Il existe donc un autre fichier HTML, qui contient la liste des articles associés à ce tag. L’important à retenir c’est qu’il n’y a pas de traitement serveur, ou de langage de programmation derrière, on sert simplement du contenu statique.

Les générateurs de sites statiques ne sont pas nouveaux, mais j’aime donner du contexte à ce que je publie et ne pas présumer du niveau du lecteur.

Dans ce qui nous intéresse ici, j’ai donc choisi le framework Hugo, écrit en Go, qui est probablement l’un des plus populaires au monde. Il en existe beaucoup d’autres, comme Jekyll (Ruby), Gatsby (JS), Zola (Rust), Pelican (Python), Sculpin (PHP), etc.

Finalement, peu importe la solution retenue, l’important c’est de trouver l’outil qui couvre les fonctionnalités que vous souhaitez intégrer à votre blog. Ici par exemple, la contrainte était principalement de gérer nativement le contenu multi-langues.

Je ne vais pas m’étendre sur l’installation et la configuration d’Hugo, pour cela la documentation est très complète et facile d’accès. L’essentiel est de pouvoir générer votre blog statique en une ligne de commande, ici simplement hugo.

La rédaction en local : Zettlr + git

Les générateurs de sites statiques offrent la plus grande flexibilité, puisque la seule chose nécessaire pour publier du contenu est d’écrire un fichier en Markdown.

Note : d’autres formats que Markdown sont supportés, mais c’est à mon avis le plus simple d’utilisation, tout en étant très complet.

Tous mes articles sont enregistrés dans le dossier /content/blog/ et correspondent au format suivant : YYYY-MM-DD-nom-d-article.lang.md. C’est simple, lisible et efficace.

Pour l’éditeur, évidemment tout est possible. Il est possible de tout rédiger sous ViM ou VSCode. Pour ma part, j’ai préféré me tourner vers des éditeurs spécialisés dans le Markdown. J’ai le sentiment que ma productivité est meilleure lorsque j’utilise un outil qui me procure une expérience utilisateur agréable et immersive.

J’ai commencé à écrire mes articles en local avec Typora avant de découvrir Zettlr qui est un bonheur à utiliser. Les deux outils sont puissants, mais ce dernier gère mieux la correction orthographique, la sauvegarde automatique, les métadonnées en en-tête, et le couple syntaxe markdown/formatage visuel.

Et bien sûr, tout le blog est placé dans un repository git. Plus précisément, dans un repo privé Github, avec le dossier /public ignoré. Vous allez comprendre pourquoi dans la suite de cet article lorsque l’on va attaquer l’automatisation du déploiement.

Il est privé parce que je fonctionne beaucoup avec des prises de note et des brouillons avant d’arriver à un article publié, et je ne souhaite pas que ce processus de création soit public. Pascal Martin décrit dans ce billet un fonctionnement dans lequel je me retrouve, même si mon cycle d’écriture est un peu plus court.

Dockerize me

Il est temps de mettre en ligne notre blog flambant neuf. Encore une fois, chacun a sa recette. Tout ce que l’on souhaite, c’est mettre nos fichiers HTML statiques derrière un serveur web.

Dans mon architecture, que je décrirai plus en détail à l’avenir, j’ai un ensemble de containers Docker qui tournent sur un serveur dédié, tous reliés à un reverse proxy nginx qui automatise la génération de certificats SSL Let’s Encrypt.

De fait, pour monter un nouveau service, tout ce que j’ai à faire c’est de créer un container Docker relié au réseau, généralement avec Docker Compose pour se simplifier la vie, et tout roule automatiquement.

Je ne crois pas qu’Hugo fournisse d’image officielle, mais une rapide requête dans mon moteur de recherche préféré m’a rapidement redirigé vers l’image de jojomi, qui en plus fourni une image annexe avec un nginx préconfiguré pour servir les fichiers statiques.

C’est la beauté de Docker, à partir de là, je n’ai plus rien à faire à part écrire un docker-compose.yml avec les variables qui vont bien :

version: '2'

services:
  hugo:
    image: jojomi/hugo:0.74.3
    volumes:
      - ./:/src
      - /data/blog/output/:/output
    environment:
      - HUGO_REFRESH_TIME=3600
      - HUGO_THEME=hello-friend-ng
      - HUGO_BASEURL=https://hoa.ro/
    restart: always
    network_mode: bridge

  web:
    image: jojomi/nginx-static
    volumes:
      - /data/blog/output:/var/www
    environment:
      - VIRTUAL_HOST=hoa.ro
      - VIRTUAL_PORT=80
      - LETSENCRYPT_HOST=hoa.ro
      - LETSENCRYPT_EMAIL=arthur@hoa.ro
    ports:
      - 80
    network_mode: bridge
    restart: always

On lance ensuite l’éternel

docker-compose up -d

Et vous y êtes. Ni plus, ni moins.

Maintenant, comment mettre à jour notre contenu ? Dans un premier temps, réfléchissons à le faire manuellement. C’est la première étape de l’automatisation.

Notre blog est relié à notre repository Github, donc il suffit de pull les dernières modifications, et de régénérer le contenu statique ; rappelez-vous que le dossier /public n’est pas synchronisé sur Git.

Une bonne habitude à prendre, c’est de toujours mettre dans un script ce type de commandes parce que, croyez-moi, on oublie rapidement. update.sh:

#!/bin/bash

git pull origin master
docker-compose exec -T hugo hugo -d /output

Même pas besoin de redémarrer le container !

Cette commande va exécuter hugo -d /output dans notre container Hugo et le tout ne prend que quelques secondes. Le dossier de sortie est modifié, car c’est ce qui a été spécifié dans notre Docker Compose.

Bien. On a un blog et un script de mise à jour. C’est rapide, il n’y a pas d’interruption de service. Mais on ne va pas s’amuser à se connecter en SSH chaque fois que l’on veut faire une mise à jour !

Continuous Deployment : Github’s webhooks

Des solutions de CD (Continuous Deployment), il y en a autant que de marques de lessive. Je ne vais pas les lister ni les analyser ici. En revanche, je sais exactement ce que je veux :

Quand un commit est effectué sur la branche master de mon repository sur Github, je veux exécuter le script update.sh de mon serveur dédié.

En bon développeur, au-delà de rechercher une solution adaptée, je cherche également la plus simple à mettre en place. Et un moyen simple de déclencher des évènements suite à un commit, ce sont les webhooks proposés par Github.

Un webhook c’est une requête HTTP POST qui est envoyée sur une URL de notre choix, chaque fois qu’un évènement se produit sur le repo. Ici, on ne s’intéresse qu’aux évènements de type push. Parfait, c’est tout ce dont on a besoin. Il n’y a plus qu’à traiter l’évènement sur notre serveur.

Encore une fois, je préfère aller au plus simple, et ça tombe bien puisque c’est ce que propose webhookd, un outil développé en Go :

A very simple webhook server launching shell scripts.

L’installation est simple, il suffit de suivre le README. Par contre, ce qui n’est pas vraiment expliqué, c’est comment utiliser ce logiciel comme un service systemd, afin de pérenniser notre installation.

Nous allons donc créer un service webhookd. Évidemment, je vous laisse adapter le tout à votre environnement. Ici, j’utilise un Debian et c’est root qui fait le café.

/etc/systemd/system/webhookd.service:

[Unit]
Description=WEBHOOKD

[Service]
ExecStart=/root/work/bin/webhookd
User=root
Group=root
EnvironmentFile=-/root/work/src/github.com/ncarlier/webhookd/etc/default/webhookd.env
EnvironmentFile=-/root/webhookd.env
Restart=always
Type=simple
RestartSec=30s

[Install]
WantedBy=multi-user.target

/root/webhookd.env:

WHD_SCRIPTS=/root/scripts  
WHD_LISTEN_ADDR=":<port>"

J’ai configuré l’installation pour glisser tous mes scripts dans /root/scripts, et j’ai configuré un port personnalisé sur lequel webhookd écoute.

J’ai ensuite créé un script intermédiaire, qui ajoute une vérification de token de sécurité, et qui lance notre fameux update.sh. webhookd gérant nativement les paramètres passés dans l’URL, le script n’est pas compliqué. /root/scripts/blog.sh:

#!/bin/bash  
  
if [[ $token != '<token>' ]]; then  
       echo "Access denied"  
       exit 1;  
fi  
  
cd /path/to/blog && ./update.sh

Et voilà, il ne reste qu’à créer un hook sur Github, et le tour est joué. Chaque push sur notre repo redéploie le blog statique avec les derniers changements.

Github's screenshot

NetlifyCMS

La mécanique est maintenant bien huilée, et je suis très satisfait de ce fonctionnement. Mes modifications apparaissent en production après un git push en quelques secondes.

Mais il reste un petit détail qui me chiffonne. Je l’ai dit plus haut, je fonctionne beaucoup à la prise de note ponctuelle, et aux brouillons sommaires avant de me lancer réellement dans la rédaction des articles. Et pour cela, devoir lancer un éditeur sur mon OS, une console, etc, ça peut être un peu lourd. C’est aussi pour cela que j’apprécie et utilise si facilement Shaarli.

En réponse à ce besoin, j’ai découvert NetlifyCMS. Il s’agit d’un outil open-source qui agit comme une UI de CMS, mais qui génère du contenu statique (des fichiers Markdown) et push le tout sur Git en un clic. Il fonctionne avec n’importe quel générateur de site statique, puisqu’il s’appuie uniquement sur un fichier de configuration TOML, et un import de fichier JS.

En clair, on met un fichier de configuration, un HTML avec une balise <script> dans notre repository, et on a une interface de rédaction d’article accessible dans le navigateur, qui publie automatiquement les modifications en production. Et la cerise sur le gâteau, il gère la rédaction multi-langues.

NetlifyCMS screenshot

La documentation est assez bien fichue, donc je ne vais pas trop rentrer dans le détail de ma configuration. Voilà ce que ça donne avec Hugo, et un thème qui gère le multi-langues, static/admin/config.yml:

backend:
  name: github
  repo: arthurhoaro/<repo-name>
  branch: master
media_folder: "static/img"
public_folder: "/img"
i18n:
  structure: multiple_files
  locales: [en, fr]
  default_locale: en
collections:
  - name: "blog"
    label: "Blog"
    folder: "content/blog"
    create: true
    i18n: true
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
    fields:
      - {label: "Title", name: "title", widget: "string", i18n: true}
      - {label: "Date", name: "date", widget: "datetime"}
      - {label: "Author", name: "author", widget: "hidden", default: "ArthurHoaro"}
      - {label: "Cover", name: "cover", widget: "hidden", default: ""}
      - {label: "Tags", name: "tags", widget: "list", i18n: true}
      - {label: "keywords", name: "keywords", widget: "list", i18n: true}
      - {label: "Description", name: "description", "string", i18n: true}
      - {label: "showFullContent", name: "showFullContent", widget: "hidden", default: true}
      - {label: "draft", name: "draft", widget: "boolean", default: true}
      - {label: "Body", name: "body", widget: "markdown", i18n: true}

Prévisualisation des brouillons

Il reste un petit détail à peaufiner. Pour la phase de relecture, je préfère le faire sur le rendu final plutôt que dans l’éditeur Markdown.

Évidemment, il est possible de faire tourner le serveur en local avec hugo server -D -F : -D pour compiler les brouillons, -F pour compiler les fichiers datés dans le futur. Sauf que ça n’est pas du tout pratique avec la publication via NetlifyCMS.

Encore une fois, j’ai opté pour une solution simple : monter une instance développement sur laquelle les brouillons sont accessibles. L’instance de test est protégée par une authentification basic HTTP.

Je vous invite à rependre les autres sections, mais pour résumer rapidement : nouvelle image Docker, nouveau script update.sh en compilant les brouillons, nouveau webhook Github, nouveau script webhookd et on est bon. Une fois qu’on a tout fait en production, cette étape est réglée en 5 minutes.

Conclusion

Cet article est un peu plus long que ce que j’avais anticipé. Je pense que j’ai mis plus de temps à l’écrire qu’à le mettre en place le workflow à l’origine.

Toujours est-il que vous avez toutes les clés en main pour mettre en place un blog statique, avec l’outillage de rédaction et le déploiement automatique.

J’ai décrit ici mon workflow, et j’ai tenté de faire en sorte que chaque section soit modulable, pour qu’elles soient adaptables à vos besoins et usages.

À vous de jouer ! Et n’hésitez pas à me partager vos améliorations.