Créer un blog avec Asciidoctor et Hugo
Ce blog est un site statique généré par Hugo à partir de contenu écrit en asciidoc, plus précisément asciidoctor. Dans cet article j’explique comment je m’y suis pris.
Avant propos
Dans ce qui suit je n’entrerai pas en détail dans:
-
L’utilisation de git
-
La configuration basique d’un serveur web statique avec certificat
Par ailleurs les commandes sont prévues pour Ubuntu. La plupart devrait fonctionner sur Debian en revanche il faudra les adapter pour CentOS ou autre.
Je ne maîtrise pas le développement web et certaines des solutions proposées peuvent être incomplètes ou incohérentes pour un vrai dévelopeur.
Pourquoi?
Pourquoi pas? Plus sérieusement je n’avais pas prévu de créer un blog, j’ai simplement l’habitude d’écrire ma documentation en Asciidoc que je trouve plus flexible et complet que le Markdown. J’avais déjà utilisé Hugo pour générer un site avec mon CV donc je connaissais l’outil.
Après avoir écrit la documentation pour la sauvegarde de mon instance Yunohost avec Restic, je l’ai postée sur les forums de Yunohost.
Après plusieurs éditions pour divers ajouts et corrections j’en ai eu marre d’aller à chaque fois faire la modification correspondante sur le forum, je devais à chaque fois faire la conversion Asciidoc → Markdown. Je sais qu’il existe des outils pour ça mais certaines choses en Asciidoc ne sont pas convertibles en Markdown (admonitions, callouts…).
C’est de là que m’est venue l’idée du site web.
Ajoutons à cela que sur les forums Yunohost j’ai publié l’article en français et en anglais, j’y ai vu une raison supplémentaire de créer un site web et de tirer partie de l’internationalisation proposée par Hugo.
Finalement ce système m’offre un confort maximal:
-
J’écris avec un format texte que j’affectione tout particulièrement
-
J’écris dans mon éditeur favori
-
Je peux versionner ce que j’écris
-
J’utilise ma plateforme DevOps préférée (hébergée sur mon instance Yunohost ;p) pour mettre automatiquement mon site à jour quand j’écris du nouveau contenu (cette partie fera sûrement l’objet d’un autre article).
-
Je n’ai pas besoin de gérer de base de données ou de php ou n’importe quel autre préprocesseur sur mon serveur web
Comment?
La section quick start du site web est suffisamment claire, la seule subtilité pour les fichier asciidoc est l’extension des fichiers créés et la préinstallation d’asciidoctor.
Pour résumer:
-
Installer quelques prérequis
apt-get update && apt-get install rubygems wget git unzip
-
Installer asciidoctor
gem install asciidoctor
-
Installer hugo
cd /tmp/ wget https://github.com/gohugoio/hugo/releases/download/v0.64.1/hugo_0.64.1_Linux-64bit.tar.gz -O hugo.tgz tar -xf hugo.tgz mv hugo /usr/local/bin/
-
Créer un nouveau site
hugo new site mysite
-
Créer un article
hugo new posts/myfirstpost.adoc (1) cat << EOPOST > content/posts/myfirstpost.adoc --- title: "My first post" date: 2020-02-17T21:34:00Z draft: true --- = My First post (2) :toc: This is my first post == First section Some content == Second section Some more content EOPOST
1 On crée ici un fichier avec l’extension adoc pour signifier à Hugo d’utiliser Asciidoctor pour générer le site 2 Ici attention à ne pas laisser d’espace entre le front matter (la partie délimitée par les ---
) et le début du document en asciidoc sinon vous aurez une erreur au traitement du fichier par asciidoctor. -
Ajouter un thème
git init (1) git submodule add https://github.com/budparr/gohugo-theme-ananke.git themes/ananke echo 'theme = "ananke"' >> config.toml
1 Puisqu’on manipule des fichiers texte, autant versionner le répertoire -
Lancer l’environnement de développement de Hugo
hugo serve -D
Comme notre article est en mode draft, il faut dire à Hugo de le traiter quand même avec l’option
-D
Il ne reste plus qu’à aller à l’adresse http://localhost:1313 pour voir le site.
Une fois qu’on est satisfait du résultat et qu’on souhaite passer en production:
-
Passer la variable
draft
à false -
Lancer la génération du site:
hugo -b https://subdomain.domain.tld
Il faut préciser le nom de domaine sous lequel le site va être hébergé car Hugo va l’utiliser dans les liens des pages générées. Si le site va tourner sur un sous-répertoire du domaine, il faut également le préciser ici:
hugo -b https://subdomain.domain.tld/subpath
-
Transférer le contenu du répertoire
public
à la racine du serveur web
En quelques commandes on a obtenu un site web fonctionnel dont le contenu est écrit en asciidoc.
On pourrait s’en contenter mais j’ai poussé un peu plus l’exercice…
Pour aller plus loin
Il ya quelques petites choses qui me manquaient dans le résultat obtenu.
Suppression des icônes de réseaux sociaux
Le thème propose par défaut l’affichage d’icones de réseaux sociaux sur chaque article, ça ne m’intéresse pas.
Heureusement on peut surcharger les modèles de pages fournies par le thème:
-
Créer un répertoire
layouts/_default
à la racine du projetmkdir layouts/_default -p
-
Copier le fichier de modèle des pages du thème dans les répertoire créé
cp themes/ananke/layouts/_default/single.html layouts/_default/
-
Modifier le fichier copié et y retirer la ligne
{{ partial "social-share.html" }}
Répliquer le style asciidoctor pour les admonitions et callouts
J’aime bien le style des documents générés par Asciidoctor et le problème c’est que pour l’instant ce que j’obtiens n’est pas vraiment comparable: il manque les icônes des admonitions (une ampoule pour les notes, un panneau avec un point d’exclamation pour les avertissements, …) et la numération des callouts manque de style:
J’ai pour ça répliqué ce que propose René Gielen dans son article sur le sujet (et j’ai ajouté une ou deux petites choses):
-
Créer un répertoire
static/css
à la racine du projet.mkdir static/css -p
-
Installer rouge
gem install rouge
-
Générer le fichier CSS du thème
molokai
de rougerougify style molokai > static/css/molokai.css
-
Créer un fichier css personalisé
static/css/custom.css
/* AsciiDoctor*/ table{border-collapse:collapse;border-spacing:0} .admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%} .admonitionblock>table td.icon{text-align:center;width:80px} .admonitionblock>table td.icon img{max-width:none} .admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase} .admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)} .admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0} .admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default} .admonitionblock td.icon .icon-note::before{content:"\f05a";color:#19407c} .admonitionblock td.icon .icon-tip::before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111} .admonitionblock td.icon .icon-warning::before{content:"\f071";color:#bf6900} .admonitionblock td.icon .icon-caution::before{content:"\f06d";color:#bf3400} .admonitionblock td.icon .icon-important::before{content:"\f06a";color:#bf0000} .conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(100,100,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold} .conum[data-value] *{color:#fff!important} .conum[data-value]+b{display:none} .conum[data-value]::after{content:attr(data-value)} pre .conum[data-value]{position:relative;top:-.125em} b.conum *{color:inherit!important} .conum:not([data-value]):empty{display:none} /* images */ img { padding:1px; border:2px solid #357edd; background-color: #333; } /* code inline `code` */ p code { border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px; border: 1px solid #BCBEC0; padding: 2px; font:12px Monaco,Consolas,"Andale Mono","DejaVu Sans Mono",monospace } /* block titles `.Some title` */ .listingblock .title { text-rendering:optimizeLegibility; text-align:left; font-family:"Noto Serif","DejaVu Serif",serif; font-size:0.9em; font-style:italic; line-height:1.5; color:#7a2518; font-weight:400; } /* callouts */ .colist td { font-size: 0.8em; padding: 5px; } /* justified paragraphs */ p { text-align: justify; text-justify: inter-word; } /* code highlight */ .hll { background-color: green; text-shadow: 1px 1px 2px white, 0 0 1em black, 0 0 0.2em blue; font-color: "black"; }
-
dire à hugo d’utiliser ces fichiers css en modifiant le fichier
config.toml
:[params] # Custom CSS custom_css = ["css/molokai.css","css/custom.css"]
Une fois ceci fait il y a du mieux mais il manque une chose essentielle, la police de caractères Awesome qui fournit les icônes entre autres des admonitions.
J’ai donc suivi la documentation:
-
Télécharger l’archive de la police
wget https://use.fontawesome.com/releases/v5.12.1/fontawesome-free-5.12.1-web.zip -O /tmp/fontawesome.zip unzip /tmp/fontawesome.zip -d /tmp/
-
Copier le fichier CSS et les polices nécessaires dans le projet
cp -r /tmp/fontawesome-free-*/webfonts static/ cp /tmp/fontawesome-free-*/css/all.css static/css/font-awesome.css
-
Dire à hugo d’utiliser le nouveau fichier css en modifiant à nouveau le fichier
config.toml
[params] # Custom CSS custom_css = ["css/molokai.css","css/custom.css","css/font-awesome.css"]
Et là enfin j’ai le résultat escompté.
Ajout des images d’entête et de leurs auteurs
Le thème Ananke permet d’utiliser une image pour illustrer chaque article, non seulement dans la liste des articles récents sur la page d’accueil mais aussi dans l’entête de chaque article.
La page d’accueil peut également avoir une image d’entête.
Comme décrit sur la documentation du thème:
-
Créer un répertoire pour les images
mkdir -p static/images
-
Y copier les images
-
Déclarer l’image pour la page d’accueil en modifiant le fichier
config.toml
[params] # Custom CSS custom_css = ["css/molokai.css","css/custom.css","css/font-awesome.css"] featured_image = '/images/nom-du-fichier-image.extension'
-
Déclarer l’image pour chaque article en ajoutant à chaque front matter la ligne
featured_image: '/images/nom-du-fichier-image.extension'
Super, tout ça commence à ressembler à quelque chose.
C’est bien mais je n’ai pas la moindre connaissance en graphisme et je n’ai pas de photos qui collent aux thèmes de mes articles. Je suis donc allé piocher dans Pixabay quelques images libres de droits. Le libre c’est génial on peut en faire à peu près ce qu’on veut mais du coup ça peut être sympa de doner crédit aux auteurs des images (comme c’est rappelé à chaque fois qu’on télécharge une image sur Pixabay).
Voici comment procéder:
-
Déclarer une variable
featured_image_by
pour la page d’accueilconfig.toml[params] # Custom CSS custom_css = ["css/molokai.css","css/custom.css","css/font-awesome.css"] featured_image = '/images/nom-du-fichier-image.extension' featured_image_by = 'Image par <Auteur> de Pixabay'
-
On fait pareil pour chaque article dans leur front matter
featured_image_by: 'Image par <Auteur> de Pixabay'
-
Surcharger les fichiers de définition des entêtes du thème
mkdir layouts/partials -p cp themes/ananke/layouts/partials/page-header.html layouts/partials/ cp themes/ananke/layouts/partials/site-header.html layouts/partials/
-
Modifier le fichier
layouts/partials/page-header.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
{{ $featured_image := .Params.featured_image }} {{ if $featured_image }} {{/* Trimming the slash and adding absURL make sure the image works no matter where our site lives */}} {{ $featured_image := (trim $featured_image "/") | absURL }} <header class="cover bg-top" style="background-image: url('{{ $featured_image }}');"> <div class="pb3-m pb6-l bg-black-60"> {{ partial "site-navigation.html" . }} <div class="tc-l pv6 ph3 ph4-ns"> {{ if not .Params.omit_header_text }} <h1 class="f2 f1-l fw2 white-90 mb0 lh-title">{{ .Title | default .Site.Title }}</h1> {{ with .Params.description }} <h2 class="fw1 f5 f3-l white-80 measure-wide-l center lh-copy mt3 mb4"> {{ . }} </h2> {{ end }} {{ end }} </div> </div> {{ if .Params.featured_image_by }}<div class="bg-black-60" style='text-shadow: 1px 1px 2px white, 0 0 1em black, 0 0 0.2em blue; font-color: "white"; background-color: "black"; position: "bottom"'>{{ .Params.featured_image_by }}</div>{{ end }} (1) </header> {{ else }} <header> <div class="{{ .Site.Params.background_color_class | default "bg-black" }}"> {{ partial "site-navigation.html" . }} </div> </header> {{ end }}
1 Si la variable existe, on affiche son contenu -
Modifier le fichier
layouts/partials/page-header.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
{{ $featured_image := .Param "featured_image"}} {{ $featured_image_by := .Param "featured_image_by"}} (1) {{ if $featured_image }} {{/* Trimming the slash and adding absURL make sure the image works no matter where our site lives */}} {{ $featured_image := (trim $featured_image "/") | absURL }} <header class="cover bg-top" style="background-image: url('{{ $featured_image }}');"> <div class="{{ .Site.Params.cover_dimming_class | default "bg-black-60" }}"> {{ partial "site-navigation.html" .}} <div class="tc-l pv4 pv6-l ph3 ph4-ns"> <h1 class="f2 f-subheadline-l fw2 white-90 mb0 lh-title"> {{ .Title | default .Site.Title }} </h1> {{ with .Params.description }} <h2 class="fw1 f5 f3-l white-80 measure-wide-l center mt3"> {{ . }} </h2> {{ end }} </div> {{ if $featured_image_by }}<div style='text-shadow: 1px 1px 2px white, 0 0 1em black, 0 0 0.2em blue; font-color: "white"; background-color: "black";'>{{ $featured_image_by }}</div>{{ end }} (2) </div> </header> {{ else }} <header> <div class="pb3-m pb6-l {{ .Site.Params.background_color_class | default "bg-black" }}"> {{ partial "site-navigation.html" . }} <div class="tc-l pv3 ph3 ph4-ns"> <h1 class="f2 f-subheadline-l fw2 light-silver mb0 lh-title"> {{ .Title | default .Site.Title }} </h1> {{ with .Params.description }} <h2 class="fw1 f5 f3-l white-80 measure-wide-l center lh-copy mt3 mb4"> {{ . }} </h2> {{ end }} </div> </div> </header> {{ end }}
1 On stocke le paramètre dans une variable pour un accès plus facile 2 Si la variable contient quelque chose, on l’affiche
Et voilà, on a pour chaque image d’entête une mention de l’auteur avec un léger effet d’ombre pour s’assurer qu’elle soit toujours visible peu importe l’image.
Mon niveau en css est équivalent à mon niveau en graphisme, il y a largement moyen de faire mieux mais cela me convient pour l’instant :p |
Drafts en transparence sur les sites de prévisualisation
Hugo utilise un système de drafts, on génère généralement les drafts pour une prévisualisation en mode développement mais pas en production.
Le problème c’est que parfois j’oublie de retirer le mode draft d’un article une fois que je l’ai fini et du coup il n’apparaît pas sur le site de production alors que j’ai tout mergé comme il fallait.
J’ai cherché un tout petit peu et j’ai trouvé une solution simple: diminuer l’opacité des articles en drafts pour que ça me saute aux yeux.
C’est toujours la même méthode, surcharger le thème et appliquer du CSS en fonction de la variable Draft
:
-
Copier les fichiers à surcharger
cp themes/ananke/layouts/partials/summary-with-image.html layouts/partials/ cp themes/ananke/layouts/_default/baseof.html layouts/_default/
-
Apporter les modifications souhaitées
layouts/partials/summary-with-image.html{{ $featured_image := .Params.featured_image }} <article class="{{ if .Draft }}draft {{ end }}bb b--black-10"> (1) ...
1 Notez qu’on applique juste une classe supplémentaire si l’article est en Draft layouts/_default/baseof.html1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
<!DOCTYPE html> <html lang="{{ $.Site.LanguageCode | default "en" }}"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> {{/* NOTE: the Site's title, and if there is a page title, that is set too */}} <title>{{ block "title" . }}{{ .Site.Title }} {{ with .Params.Title }} | {{ . }}{{ end }}{{ end }}</title> <meta name="viewport" content="width=device-width,minimum-scale=1"> {{ hugo.Generator }} {{/* NOTE: For Production make sure you add `HUGO_ENV="production"` before your build command */}} {{ if eq (getenv "HUGO_ENV") "production" | or (eq .Site.Params.env "production") }} <META NAME="ROBOTS" CONTENT="INDEX, FOLLOW"> {{ else }} <META NAME="ROBOTS" CONTENT="NOINDEX, NOFOLLOW"> {{ end }} {{ $stylesheet := .Site.Data.webpack_assets.app }} {{ with $stylesheet.css }} <link href="{{ relURL (printf "%s%s" "dist/" .) }}" rel="stylesheet"> {{ end }} {{ range .Site.Params.custom_css }} <link rel="stylesheet" href="{{ relURL ($.Site.BaseURL) }}{{ . }}"> {{ end }} {{ block "favicon" . }} {{ partialCached "site-favicon.html" . }} {{ end }} {{ if .OutputFormats.Get "RSS" }} {{ with .OutputFormats.Get "RSS" }} <link href="{{ .RelPermalink }}" rel="alternate" type="application/rss+xml" title="{{ $.Site.Title }}" /> <link href="{{ .RelPermalink }}" rel="feed" type="application/rss+xml" title="{{ $.Site.Title }}" /> {{ end }} {{ end }} {{/* NOTE: These Hugo Internal Templates can be found starting at https://github.com/spf13/hugo/blob/master/tpl/tplimpl/template_embedded.go#L158 */}} {{- template "_internal/opengraph.html" . -}} {{- template "_internal/schema.html" . -}} {{- template "_internal/twitter_cards.html" . -}} {{ if eq (getenv "HUGO_ENV") "production" | or (eq .Site.Params.env "production") }} {{ template "_internal/google_analytics_async.html" . }} {{ end }} </head> <body class="{{ if .Draft }}draft {{ end }}ma0 {{ $.Param "body_classes" | default "avenir bg-near-white"}}{{ with getenv "HUGO_ENV" }} {{ . }}{{ end }}"> (1) {{ block "header" . }}{{ partial "site-header.html" .}}{{ end }} <main class="pb7" role="main"> {{ block "main" . }}{{ end }} </main> {{ block "footer" . }}{{ partialCached "site-footer.html" . }}{{ end }} {{ block "scripts" . }}{{ partialCached "site-scripts.html" . }}{{ end }} </body> </html>
1 Idem ici -
Enfin déclarer la classe
draft
en ajoutant à la fin destatic/css/custom.css
:/* drafts */ .draft { opacity:0.5; }
Internationalisation
Enfin, puisque j’avais posté mon article sur les forums de Yunohost en français et en anglais, je me suis dit que ça serait bien d’avoir un site également bilingue.
Je me suis contenté de suivre encore une fois la documentation de Hugo.
J’ai choisi de séparer le contenu en deux sous répertoires english
et french
.
Résultat, cette petite mention en
ou fr
en haut à droite de chaque page du site (à condition que chaque article soit traduit!)
Pour aller encore plus loin
L’article étant déjà assez fourni, je discuterai des points suivants dans des posts séparés:
-
Génération à partir d’une image Docker et utilisation de docker-compose
-
Génération un site de préproduction
-
Génération automatique du site avec Gitlab-CI
-
Génération de l’image avec Giltab-CI et réutilisation pour le déploiement