Migration : de Dotclear à Wordpress à Jekyll

Ce blog utilise désormais Jekyll. J'ai pioché à droite à gauche pour lui faire faire ce que je voulais, je documente donc ici pour celleux que ça intéresse.

Au tout début, ce blog utilisait Dotclear. Une architecture logicielle magnifique, bien au-dessus de ce qui existait à l’époque. Mais le choix malheureux de gabarits en pseudo-code, donc : moins puissant que du code, une documentation bien plus réduite, une courbe d’apprentissage inutile, et la nécessité d’invalider le cache quand on développe. De plus, Dotclear n’avait qu’une petite communauté donc peu de plugins et de thèmes, et l’API était bien conçue mais difficile à manipuler pour développer un petit plugin vite fait.

Je suis donc ensuite passé à Wordpress pour son API hyper intuitive et très très bien documentée, et pour les thèmes et plugins par milliers. Mais à force, j’ai eu marre de vivre dans la crainte qu’une faille de sécurité ne soit utilisée sur mon blog, du coup il fallait mettre à jour sans cesse. Par ailleurs les milliers de plugins pas maintenus, en doublon et/ou mal écrit ont rendu le moindre choix pénible. Enfin, les mises à jour et sauvegardes sont rendues compliquées par la présence d’une base de donnée. Ça rend compliqué de modifier le site en local avant de le publier, aussi.

Pour réduire le temps et la complexité de maintenance, j’ai testé plusieurs CMS sans base de données :

  • Pico, qui ne gérait pas l’internationalisation quand j’ai testé, je ne sais pas s’ils le font maintenant.
  • Grav, pas convainquant, je ne sais plus pourquoi.
  • Kirby, vraiment très bien ! Je recommande 5/5. Des tas de thèmes, de plugins, une syntaxe facile à apprendre, et un code bien architecturé.
  • Et enfin Jekyll, très très bien mais en Ruby, donc il faut découvrir et installer l’éco-système sur sa machine avant de pouvoir l’installer, ce qui m’avait bloqué jusque-là.

Voici donc comment j’ai configuré mon blog Jekyll pour avoir un site qui se charge vite, avec un formulaire de recherche, un formulaire de contact, le tout hébergé gratuitement (mais pas sur Github).

Hébergement

J’étais chez Gandi dont le service est très bien, mais un poil complexe mine de rien pour un simple blog. Quand j’ai vu que Alwaysdata proposait une offre gratuite pour les moins de 100Mo j’ai sauté dessus pour faire des économies. Il faut un peu s’habituer à leur interface, qui est bien conçue mais diffère de Gandi et de Dreamhost où j’avais mes habitudes.

J’apprécie au passage de pouvoir facilement mettre un certificat Let’s Encrypt, ce n’est pas hyper intuitif au premier abord (il faut en créer un pour chaque sous-domaine dans un écran séparé) mais ça marche bien. Je vais attendre de voir s’ils se renouvellent bien tout seuls, ça se serait chouette :-)

Un CMS sans base de données

Niveau sécurité, on ne peut pas faire plus simple : ce site est construit en local et le serveur ne reçoit que des fichiers HTML à afficher. Ça veut dire aussi que les backups sont ultra-simples à faire, et qu’il devient possible de stocker et versionner tout son site via Git, ce que j’apprécie beaucoup.

Par contre je perds la possibilité de bloguer sans avoir accès à mon ordinateur (ou alors il faut télécharger tout le dépôt, configurer l’environnement de travail, etc…). Tant pis, de toute façon je ne blogue pas souvent et je peux toujours écrire une note et la publier ensuite, j’ai le temps.

Formulaire de contact

L’envers de la médaille c’est que les formulaires, sur des sites statiques, c’est tout-de-suite une autre affaire. Pour les commentaires j’ai lâché l’affaire, j’en reçois peu de toute façon, et je n’aime pas les solutions externes telle que Disqus.

Je voulais qu’on puisse au moins me contacter —même si les 2/3 des messages reçus par ce biais sont inutiles, je trouve que c’est plus poli. J’ai donc regardé différentes solutions et fini par retenir la plus simple (même si ça repose sur un service externe) : FormSpree. Il suffit de mettre <form action="https://formspree.io/[votre email]"> et d’autoriser l’adresse où sera affiché le formulaire, et voilà ! Ça marche avec n’importe quelle site, statique ou non, d’ailleurs.

Formulaire de recherche

J’étais parti pour utiliser Algolia, qui fournit un plugin Jekyll gratuit en échange d’un placement de leur logo. Mais entre la création du compte, de clé d’interrogation, la synchronisation de l’index, et tout, j’ai fini par jeter l’éponge.

Du coup je suis passé par un script basé sur Lunr. La page de recherche contient un formulaire get basique qui renvoie sur elle-même, une balise <script> dans laquelle je génère un JSON de tous les billets, et deux scripts. Le premier c’est une version minifiée de Lunr, que j’auto-héberge pour être indépendant et réduire le nombre de requêtes DNS. Le deuxième c’est une fonction anonyme (IIFE) qui sert à isoler et regrouper les différentes actions à faire :

  1. Transformer le JSON en index utilisable par Lunr.
  2. Intercepte l’événement submit du formulaire.
  3. Récupère les paramètres de la requête via l’URL de la page (c’est pour ça qu’on est en get), s’il y en a.
  4. Si on envoie le formulaire, ou que l’URL contient une recherche, ça lance la suite :
  5. On interroge l’index Lunr.
  6. On formate les résultats : récupération du fragment pertinent, mise en évidence des termes, génération du HTML.
  7. Et on n’oublie pas de déplacer le focus, pour l’accessibilité.

Ça surligne la phrase recherchée plutôt que les termes individuellement, je n’ai pas voulu y passer toute la nuit vu le peu d’usage qui sera fait de cette page :-)

Coloration syntaxique des exemples de code

Ça faisait longtemps que je voulais le faire, même si je n’écris plus très souvent de billet à propos du code. Au passage, c’est tellement plus facile de saisir du code en markdown qu’en HTML ! Je me rappelle d’avoir des fois été obligé de modifier mes billets à la main dans la base de données parce que Wordpress avait mangé les balises…

Ici j’utilise l’incontournable gem Rouge de Jeanine Adkisson et le thème Solarized Dark. C’est intégré dans ma CSS principale pour n’avoir qu’un seul fichier à télécharger (12ko minifié).

Gestion du flux de syndication (Atom)

gem 'jekyll-feed', group: jekyll_plugins

J’utiliser le plugin jekyll-feed et c’est super ! Il suffit d’inclure <link type="application/atom+xml" rel="alternate" href="https://userland.fr/feed.xml" title="Chez Goulven" /> dans _includes/head.html pour que les liens soient automatiquement insérés avec la bonne syntaxe. C’est ultra-simple, on aurait tort de s’en priver :-)

Regénération à la volée (live-reload)

Pareil, Jekyll rend ça super simple : gem 'jekyll-livereload', group: jekyll_plugins dans le Gemfile et livereload: true dans _config.yml et on n’en parle plus ! Attention cependant, si on l’utilise il faut générer une version basique du site avant d’uploader sinon le script essaie de se charger sur le serveur et on a une erreur 404. Ce n’est pas critique mais c’est pas compliqué à corriger alors on y pense.

Localisation

gem 'i18n'

Ça m’enquiquinait d’avoir des dates en anglais dans un blog en français, alors j’ai mis le code suivant dans _plugins/i18n_filter.rb :

# Originally from https://github.com/nelsonsar/jekyll-i18n-filter/blob/master/i18n_filter.rb
require 'i18n'

LOCALE = 'fr'

module Jekyll
  module I18nFilter
    def localize(input, format=nil)
      load_translations
      format = (format =~ /^:(\w+)/) ? $1.to_sym : format
      # Prevent Object must be a Date, DateTime or Time object errors
      # Liquid does it
      input = Time.parse(input) if input.is_a?(String)
      I18n.l input, :format => format, :locale => LOCALE
    end

    def load_translations
      unless I18n::backend.instance_variable_get(:@translations) and locale_file_exists
        I18n.backend.load_translations Dir[File.join(File.dirname(__FILE__),'../_locales/*.yml')]
      end
    end

    def locale_file_exists
      file_name = File.join(LOCALE,'.yml')
      return (locale_file_exists_in_current_dir(file_name) and locale_exists_in_locales_dir(file_name))
    end

    def locale_file_exists_in_current_dir(file_name)
        return File.exists?(File.join(File.dirname(__FILE__),file_name))
    end

    def locale_exists_in_locales_dir(file_name)
        return File.exists?(File.join('../_locales/',file_name))
    end
  end
end

Liquid::Template.register_filter(Jekyll::I18nFilter)

Pour les traductions, on ne se casse pas la nénette, on copie le contenu du fichier de localisation de Rails et on le colle dans _locales/fr.yml.

Grâce à tout ça dans mes templates je peux désormais écrire {{ page.date | localize: '%d %B %Y' }} et ça s’affiche en français.

Attention j’ai modifié le fichier pour ne pas avoir à convertir les dates, c’est commenté. J’ai proposé la modification à l’auteur du plugin pour que tout le monde en profite.

Images responsive

gem 'jekyll-responsive-image', group: jekyll_plugins

J’utilise le plugin jekyll-responsive-image qui redimensionne les images et permet d’utiliser l’attribut src-set afin que les navigateurs chargent l’image la plus adaptée sur mobile. J’ai juste supprimé les espaces inutiles générés par _includes/responsive-image.html, il suffit de remplacer les balises {% capture/if/else/endif/etc %} par {%- capture/if/else/endif/etc -%}. C’est pas compliqué et ça rend le code source plus léger et plus facile à lire. Voici le code :

{%- capture srcset -%}
  {%- for i in resized -%}
      /{{ i.path }} {{ i.width }}w,
  {%- endfor -%}
{%- endcapture -%}
{%- assign largest = resized | sort: 'width' | last -%}
<img src="/{{ largest.path }}" alt="{{ alt }}" srcset="{{ srcset | strip_newlines }}" class="{{ classes }}">

J’aurai juste préféré que le plugin filtre les images plutôt que d’obliger à utiliser une nouvelle syntaxe, mais il y a peut-être une bonne raison pour ça.

Contenus externes

J’aimais bien pouvoir coller une URL Twitter et la voir transformée automatiquement dans Wordpress. Comme j’ai quelques tweets qui trainent j’ai ajouté gem 'jekyll-twitter-plugin', group: jekyll_plugins au Gemfile et ça tourne tout seul, c’est pratique.

Pour les conférences j’ai intégré les diaporamas stockés sur Speakerdeck en copiant-collant leur script embed à la main. J’ai regardé le plugin OEmbed de Tammo van Lessen mais il ne semblait pas gérer Speakerdeck (ou alors c’est juste la documentation qui n’est pas à jour, parce que la gem oembed intègre ce fournisseur. Vu le temps que ça prend, j’ai fait au plus simple : un champ embed en haut des pages de conférences et on n’en parle plus !

Performances Web

Les pages sont plutôt légères : un seul fichier CSS, pas de JS, pas de cookies, pas de polices non-natives, quasiment pas d’images. On descend à moins de 100ko, c’est rare ! Les pages les plus lourdes sont celles où il y a des photos ou un support de conférence inclus par JavaScript.

Configuration Apache

  1. Je génère des ETags indépendants du serveur. Le site n’est pas sensé être servi par une armée de serveurs, mais bon…
  2. Je spécifie les dates d’expiration pour les différents types de fichiers.
  3. Je dis au serveur d’afficher le contenu de ma page d’erreur customisée s’il ne trouve pas l’adresse demandée.

Voici le contenu de mon fichier .htaccess :

# Create the ETag (entity tag) response header field
FileETag MTime Size

<IfModule mod_expires.c>
 ExpiresActive On
 ExpiresDefault "access plus 7200 seconds"
 ExpiresByType image/jpg "access plus 2592000 seconds"
 ExpiresByType image/jpeg "access plus 2592000 seconds"
 ExpiresByType image/png "access plus 2592000 seconds"
 ExpiresByType image/gif "access plus 2592000 seconds"
 AddType image/x-icon .ico
 ExpiresByType image/ico "access plus 2592000 seconds"
 ExpiresByType image/icon "access plus 2592000 seconds"
 ExpiresByType image/x-icon "access plus 2592000 seconds"
 ExpiresByType text/css "access plus 2592000 seconds"
 ExpiresByType text/javascript "access plus 2592000 seconds"
 ExpiresByType text/html "access plus 7200 seconds"
 ExpiresByType application/xhtml+xml "access plus 7200 seconds"
 ExpiresByType application/javascript A2592000
 ExpiresByType application/x-javascript "access plus 2592000 seconds"
</IfModule>

ErrorDocument 404 /404/index.html

Optimisation des images

Quand j’ai vu la complexité de jekyll-assets je me suis dit que j’allais faire plus simple. Vu que les pages sont légères et la CSS déjà compressée, il n’y avait besoin que de compresser les images. Au final vu le temps que j’y ai passé j’aurai mieux été inspiré d’utiliser jekyll-assets je pense.

Dans mon Gemfile j’ai ajouté gem 'image_optim', group: :jekyll_plugins et gem 'image_optim_pack', group: :jekyll_plugins. Puis j’ai écrit un plugin de filtre Jekyll dont voici le code complet :

# _plugins/optimize_images.rb
Jekyll::Hooks.register :site, :post_render do |site|
  # Skip when live-serving because it makes generating the pages much slower
  next if site.config.fetch('serving')
  path = Dir["#{site.dest}/assets/**/*.*"]
  optim = ::ImageOptim.new
  optim.optimize_images! path do |original, optimized|
    if optimized
      puts "Optimized #{optimized}, (#{optimized.original_size - optimized.size} bytes shaved)"
    end
  end
end
  1. J’attache le filtre après l’écriture des fichiers pour être sûr que tout soit présent, je gagnerai peut-être en l’attachant à :post_render plutôt que :post_write, je n’ai pas trop saisi la nuance.
  2. Je récupère ensuite tous les fichiers du répertoire _site/assets et une instance d’optimiseur.
  3. Je lance ensuite l’optimisation des fichiers (le ! signifie qu’on les modifie, sinon ça ne fait de renvoyer une version optimisée, c’est une convention pratique en Ruby)
  4. Je termine en affichant le gain obtenu pour chaque fichier optimisé.

Ce que j’aimerai bien faire un jour ou l’autre

  • Rendre la recherche instantanée plutôt que de forcer un rechargement de page.
  • Tester l’intégration OEmbed des diaporamas Speakerdeck
  • Optimiser les images du répertoire assets pour stocker uniquement des fichiers optimisés dans le dépôt de code.
  • Faire en sorte de n’optimiser que les nouvelles images pour accélérer la phase de génération du site.
  • Utiliser les extraits automatiques pour les billets
  • Lister les tags et autres catégories.
  • Je pourrais demander à Lunr de me fournir directement l’extrait centré sur les mots trouvés, et retourner les résultats approchants s’il y en a peu (pour autoriser les formes singulières/plurielles, etc).
  • Automatiser la transformation des abréviations et du texte en langue étrangère, pour l’accessibilité.
  • J’aimerai bien que les smileys soient automatiquement convertis en emoji (pas en images, mais bien en emoji).
  • Faciliter la localisation de dates dans Jekyll, j’aimerai que ça soit aussi simple que d’indiquer lang: fr dans _config.yml.

Publié le 27 décembre 2017