Site Statique

tbowan

27 Septembre 2017

Après avoir expérimenté le PHP, nous revenons aux sources avec une génération statique du site des arsouyes. Pour les curieux, voici comment nous nous y sommes pris.

Expérimenter, c'est bien

Comme vous vous en doutez sûrement, en plus de matérialiser notre présence dans le cyber-espace, le site des arsouyes nous sert de plateforme d'expérimentations.

Aux débuts, nous avions optés pour une génération statique du site web1 qui avait l'avantage de ne pas exposer de failles applicatives. Nous avons ensuite développé plusieurs version en PHP avec bases de données. Suivant la mode et retournant aux sources, nous sommes donc revenu à un système statique pour vous proposer cette nouvelle mouture du site des arsouyes.

Il existe déjà de nombreux systèmes pour générer des sites statiques plus ou moins aboutis et vous pouvez trouver une liste sur staticgen.com. Par contre, ils ont leurs propres convention (hiérarchie de répertoires, notion de blogs, tags et autres) qui ne vont pas vraiment à ce qu'on cherche à faire ici.

Comme, en plus, nous ne faisons confiance à personne et qu'on aime expérimenter, on a décidé de faire notre propre système, histoire de s'entraîner. Et comme on aime partager, voici les petites solutions qu'on a mis en oeuvre pour cette nouvelle mouture du site.

La solution des arsouyes

Makefile pour orchestrer la génération

Comme on voulait quelque chose de simple, on s'est tout naturellement tourné vers make pour compiler le site web à partir des sources. Ça fait un peut Captain Obvious dit comme ça mais c'est loin d'être une évidence tant la grande majorité des générateur passent par des scripts de plus haut niveau comme du python, du ruby voir du nodejs.

On a donc un makefile à la racine qui va orchestrer tout le système. Avec des règles génériques pour produire différent type de contenus et d'autres plus spécifiques pour les cas particuliers.

La première chose que je fait dans un makefile, c'est déclarer la cible all pour la faire dépendre de allatend qui, comme son nom l'indique, sera définie à la fin du fichier. Ceci me permet de déclarer et construire petit à petit tout ce dont j'ai besoin. Ca me garanti aussi qu'appeler make sans argument sera équivalent à un make all.

all: allatend

Je continue ensuite par déclarer des macros pour les répertoires où je travailles, ici les sources et le répertoire cible. L'utilisation du ?= me permet de surcharger ces macros à la ligne de commande et de pouvoir générer le contenu ailleurs (ou de générer un autre contenu).

SRC_DIR         ?= src
DST_DIR         ?= public

On peut maintenant s'attaquer à la génération des fichiers simples, ceux qu'il faut copier. Les deux premières macros listent le contenu à générer et la troisième les ajoute à la liste globale. La recette pour ces fichiers est très simple puisqu'il suffit de les copier. Pour éviter d'utiliser des dépendances pour les répertoires, j'utilise mkdir -p.

SRC_RAW_FILES   := $(shell find $(SRC_DIR) -type f)
DST_RAW_FILES   := $(subst $(SRC_DIR),$(DST_DIR),$(SRC_RAW_FILES))
ALL             += $(DST_RAW_FILES)

$(DST_DIR)/% : $(SRC_DIR)/%
    mkdir -p $(dir $@)
    cp $< $@

Et comme promis, à la toute fin du makefile, j'ajoute la cible allatend :

allatend : $(ALL)

Avec cette base, le makefile peut générer l'ensemble des fichiers statiques (css, images, ...).

Pandoc pour générer le html

Pour aller plus loin, et générer des pages HTML sans devoir les écrires toutes, j'utilise pandoc qui est l'outil à tout faire lorsqu'on veut traduire des formats entre eux, et surtout du markdown.

Ici encore, le schéma est similaire. Je commence par les macros pour trouver les fichiers sources et lister les fichiers à produire.

SRC_PANDOC_FILES ?= $(shell find $(SRC_DIR) -type f -name "*.md")
DST_PANDOC_FILES ?= $(subst .md,.html, \
                        $(subst $(SRC_DIR),$(DST_DIR), \
                            $(SRC_PANDOC_FILES)))

ALL              += $(DST_PANDOC_FILES)

Comme pour tout système de compilation, je poursuit ensuite avec les arguments communs (PANDOC_FLAGS) et dépendances spécifiques (PANDOC_TEMPLATE). Ici encore, l'emploi du '?=' me permet de surcharger ces macros directement en ligne de commande lorsque je veux tester des variantes.

PANDOC_FLAGS    ?= \
                    -f markdown+definition_lists \
                    -t html \
                    --base-header-level 1 \
                    --email-obfuscation=javascript

PANDOC_TEMPLATE ?= assets/template.html

On entre alors dans le vif du sujet, la génération des fichiers html. Comme je veux que le site soit visible sans serveur web, j'utilise realpath --relative-to pour passer le chemin vers la racine au template.

$(DST_DIR)/%.html : $(SRC_DIR)/%.md $(PANDOC_TEMPLATE)
    mkdir -p $(dir $@)
    pandoc\
        $< \
        $(PANDOC_FLAGS) \
        --template=$(PANDOC_TEMPLATE) \
        -o $@ \
        --variable=root:`realpath --relative-to=$(dir $@) $(DST_DIR)`

Avec cette version, les pages html sont également générées et on pourrait s'arrêter là.

Si vous vous souvenez de la section précédente, vous remarquerez que les fichiers sources sont également copiés en prod. Dans notre cas, ce n'est pas une erreur car on trouve pratique d'avoir accès au source directement en ligne ; vous pouvez par exemple nous envoyer des corrections ;-).

mod_rewrite pour corriger les liens

Il se trouve que lorsque j'écrit les liens dans les pages en markdown, je n'aime pas devoir ajouter l'extension .html. À ce stade, je n'ai que des fichiers .md qui n'ont pas encore été compilés et je préfère que mes liens disent où chercher plutôt que vers où ils pointeront lorsque tout sera bon.

Comme je ne peux pas ajouter ces extensions à la compilation, je dois le faire au runtime, c'est à dire par le serveur web apache. Et ça tombe bien puisqu'il a justement un module fait pour ça : mod_rewrite.

Cette fois, c'est dans le fichier src/.htaccess que ça se passe. On commence par activer le mod_rewrite :

RewriteEngine on

On peut alors ajouter les règles pour rediriger les visiteurs vers les pages avec l'extension. Pour ça, je vérifie d'abord que le lien n'est pas déjà valide, puis qu'un fichier html existe également. La troisième règle est là pour capturer l'URL du visiteur plutôt que les chemins dans le système de fichiers.

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteCond %{THE_REQUEST} " ([^ ]+) "
RewriteRule ^(.*)$ %1.html [R=301,L]

De manière similaire, je n'aime pas les liens qui contiennent index.html que je vais donc également modifier. Cette fois, plutôt qu'ajouter, on supprime une partie de l'URL mais la méthode est la même.

RewriteCond %{REQUEST_FILENAME} -f
RewriteCond %{THE_REQUEST} " (.*/)index\.html "
RewriteRule ^.*$ %1 [R=301,L]

En bonus, je déclare quelques pages d'erreurs que j'ai écrit en markdown pour avoir un minimum de cohérence dans la charte graphique du site.

ErrorDocument 403 /error/403.html
ErrorDocument 404 /error/404.html

mod_autoindex pour les listings

Plutôt que de devoir maintenir des listings à jours à la main, ou faire un script qui les génère lors de la compilation, on a choisi ici de laisser la main au serveur web via les auto index2.

On commence donc par activer la génération des indexes.

Options +Indexes

Comme le old school, ça a quand même ses limites, on active quelques options bien pratiques pour avoir un rendu plus sympa.

IndexOptions Charset=UTF-8      # Pour être sûr de l'encodage
IndexOptions FoldersFirst       # Les catégories en premier
IndexOptions HTMLTable          # Des <tables> plutôt qu'un <pre>
IndexOptions SuppressRules      # Supprimer les lignes autour du listing
IndexOptions NameWidth=40       # Tronquer les noms de fichiers
IndexOptions DescriptionWidth=* # Laisser libre la longueur des descriptions

On continue en personnalisant les icones en fonction des extensions de fichiers. Je ne vous met pas toutes les addIcon, vous voyez l'idée.

DefaultIcon /images/icons/folder_green.png        # pour le répertoire parent
AddIcon /images/icons/folder.png ^^DIRECTORY^^    # pour les répertoires

AddIcon /images/icons/ssl_certificates.png .crt
AddIcon /images/icons/file_extension_3gp.png .3gp
AddIcon /images/icons/file_extension_7z.png .7z
...

On peut maintenant rentrer dans le vif du sujet en personnalisant vraiment les pages générées. Pour ça, on va générer ce qui vient avant et après les listings et dire à apache d'utiliser nos fichiers plutôt que de générer son code html.

# Ne pas lister les fichiers inutilement
IndexIgnore Readme.html favicon.ico *.md .[^.]*

# Ne pas ajouter de code html autour de la table
IndexOptions SuppressHTMLPreamble

# Insérer le contenu de .Header.html avant la table
HeaderName .Header.html

# Insérer le contenu de .Readme.html après la table
ReadmeName .Readme.html

La suite se passe dans le makefile qui va gérer la compilation de ces deux fichiers .Header.html et .Readme.html lorsque c'est nécessaire. En effet, si le répertoire contient un fichier index.md qui sera compilé en index.html, l'auto-index n'est pas utilisé par apache.

SRC_DIRS       ?= $(shell find $(SRC_DIR) -type d)
DIR_WITH_INDEX ?= $(subst /index.md,, \
                      $(shell find $(SRC_DIR) -type f -name "index.md"))
DIR_WITHOUT    ?= $(filter-out $(DIR_WITH_INDEX), $(SRC_DIRS))

Maintenant qu'on a tous les répertoires nécessaires, on peut créer la liste des .Header.html et .Readme.html.

AUTOINDEX_HEADERS ?= $(addsuffix /.Header.html,$(DIR_WITHOUT))
AUTOINDEX_READMES ?= $(addsuffix /.Readme.html,$(DIR_WITHOUT))

ALL += $(subst $(SRC_DIR),$(DST_DIR),\
           $(AUTOINDEX_HEADERS) \
           $(AUTOINDEX_READMES))

Il ne reste plus qu'à compiler ces fichiers. Nous avons pris comme convention que si un fichier readme.md était présent, il serait utilisé pour générer les fichiers pour l'auto-index.

$(DST_DIR)%/.Header.html : $(SRC_DIR)%/Readme.md $(PANDOC_HEADER)
    mkdir -p $(dir $@)
    pandoc\
        $< \
        $(PANDOC_FLAGS) \
        --template=$(PANDOC_HEADER) \
        -o $@ \
        --variable=root:`realpath --relative-to=$(dir $@) $(DST_DIR)`

$(DST_DIR)%/.Readme.html : $(SRC_DIR)%/Readme.md $(PANDOC_FOOTER)
    mkdir -p $(dir $@)
    pandoc\
        $< \
        $(PANDOC_FLAGS) \
        --template=$(PANDOC_FOOTER) \
        -o $@ \
        --variable=root:`realpath --relative-to=$(dir $@) $(DST_DIR)`

Et si aucun fichier n'existe, alors nous utilisons un fichier par défaut et le nom du répertoire comme titre.

AUTOINDEX_README_DEFAULT ?= assets/Readme.md

$(DST_DIR)%/.Header.html : $(AUTOINDEX_README_DEFAULT) $(PANDOC_HEADER)
    mkdir -p $(dir $@)
    pandoc\
        $< \
        $(PANDOC_FLAGS) \
        --template=$(PANDOC_HEADER) \
        -o $@ \
        --variable=root:`realpath --relative-to=$(dir $@) $(DST_DIR)` \
        --variable=title:$(shell basename $(dir $@))
        
$(DST_DIR)%/.Readme.html : $(AUTOINDEX_README_DEFAULT) $(PANDOC_FOOTER)
    mkdir -p $(dir $@)
    pandoc\
        $< \
        $(PANDOC_FLAGS) \
        --template=$(PANDOC_FOOTER) \
        -o $@ \
        --variable=root:`realpath --relative-to=$(dir $@) $(DST_DIR)` \
        --variable=title:$(shell basename $(dir $@))

Des liens symboliques pour le treilli

À ce stade, il manque encore la notion de treilli. Plutôt qu'une simple arborescence qui, pour chaque fichier, n'a qu'un seul chemin pour y aboutir, on préfère les treillis. Les documents apparaissent donc dans plusieurs répertoires et pour éviter de les copier, on utilise des liens symboliques.

Cette fois encore, c'est le makefile qui s'y colle mais avec une limitation. Comme il n'est pas capable de choisir une règle en fonction du type de fichier mais uniquement en fonction des noms, on a du choisir une extension pour nos liens symboliques.

On commence donc par générer la liste des liens dans le source et ceux qu'on va devoir générer.

LINKS_EXT      ?= lnk
SRC_LINKS      ?= $(shell find $(SRC_DIR) -type l -name "*.$(LINKS_EXT)")
DST_LINKS      ?= $(subst $(SRC_DIR),$(DST_DIR),$(basename $(SRC_LINKS)))

Il suffit alors de copier les liens symboliques en s'assurant de garder la notion de lien (option -d).

$(DST_DIR)/%: $(SRC_DIR)/%.$(LINKS_EXT)
    mkdir -p $(dir $@)
    cp -d $< $@

ALL += $(DST_LINKS) 

Pour les liens vers les fichiers markdown, c'est plus subtil puisqu'il faut tenir compte que ces fichiers génèrent des pages html et qu'en toute logique, il faut également faire des liens pour ces fichiers générés.

DST_LINKS_HTML ?= $(subst .md,.html,$(DST_LINKS))

$(DST_DIR)/%.html: $(SRC_DIR)/%.md.$(LINKS_EXT)
    mkdir -p $(dir $@)
    target=`realpath --relative-to=$(dir $<) $< \
                | sed -e "s/.md/.html/"` ;\
            ln -f -s $$target $@

ALL += $(DST_LINKS_HTML)

Comme le serveur web n'est pas capable de rediriger les visiteurs lorsqu'il voit des liens (il ne peut que les suivres et fournir le même contenu pour les deux URIs), on va générer une liste de redirections pour mod_rewrite dans le fichier .htaccess.

$(DST_DIR)/.htaccess : \
                        $(SRC_DIR)/.htaccess \
                        $(DST_LINKS) \
                        $(DST_LINKS_HTML)
    ( \
        cat $(SRC_DIR)/.htaccess ; \
        for i in $(DST_LINKS) $(DST_LINKS_HTML); do \
            source=$${i#$(DST_DIR)} ;\
            target=`realpath -m $$i` ;\
            target=$${target#$(DST_DIR_REAL)} ;\
            echo "Redirect \"$$source\" \"$$target\"" ;\
        done | sort -u \
    ) > $@

Il ne reste plus qu'à laisser le serveur web suivre les liens pour que sa génération d'index automatique les prenne ne compte et le tour est joué.

Options +Indexes +FollowSymLinks

Gitlab pour le déploiement continu

Plus souvent appelé Continuous Delivery, il s'agit tout simplement de pousser les modification du site web en production de manière continue, c'est à dire automatiquement et sans intervention humaine.

Pour les applications plus conséquentes, c'est toute une histoire mais pour un site statique, c'est plutôt simple. Les sites statiques hébergés sur github et gitlab le permettent déjà.

Les sources du site des arsouyes, ainsi que le makefile et autres fichiers annexes sont versionné via un serveur gitlab installé sur notre plateforme.

On utilise deux branches : master qui contient la version en production et develop qui contient la prochaine version du site et déployée sur un serveur de tests. Ce qui nous permet de jouer et expérimenter le site en avant-première.

Pour le déploiement, on utilise donc un runner gitlab sur les deux serveur et un fichier de configuration à la racine du dépôt git.

Comme on reste très simple, on n'a qu'une seule étape deploy et un job par serveur.

stages:
    - deploy

# ----------------------------------------------------------------------------
# Deploying develop branch in pre production

deploy-develop:
    stage: deploy
    only:
    - develop
    script:
    - make cleanall
    - make DST_DIR=public
    - rsync -avz -c --delete-after public/ /public/arsouyes.org/www
    tags:
    - preprod

# ----------------------------------------------------------------------------
# Deploying master branch in production

deploy-master:
    stage: deploy
    only:
    - master
    script:
    - make cleanall
    - make DST_DIR=public
    - rsync -avz -c --delete-after public/ /public/arsouyes.org/www
    tags:
    - prod

Conclusion

Après tous ces efforts, on a donc un système plutôt léger pour générer notre site avec un simple appel à make lorsqu'on veut tester en local et on laisse à gitlab le soin de déployer en production automatiquement.

On pourrait bien sûr aller plus loin. On pourrait générer les index à la compilation pour éviter que le serveur web ne doive le faire ou encore ajouter une étape de vérification voir carrément un déploiement en green/blue... Mais on s'éloigne du but : avoir un système léger et pratique qui ne nous prenne pas tout notre temps.

La grosse contrepartie du site statique, et on s'en doute rapidement, c'est que vous ne pourrez pas ajouter de contenu, des commentaires ou créer des fils de discussions ici. Mais c'est complètement assumé : on n'est pas là pour ça et ces services sont déjà disponibles ailleurs (_e.g. twitter).


  1. Système modestement appelé Über Website Generator qui, pour ceux qui s'en souviennent, consistait en un fichier bash scannant le répertoire pour générer les indexes et ajoutant les header/footer aux pages.

  2. qui marchent très bien et font carrément plus old school.