Migration du blog sur Nanoc
Comme vous l’avez très certainement remarqué, le blog a changé d’apparence : c’est dû à la migration du blog sur Nanoc.
Pourquoi cette migration ?
Plusieurs raisons m’ont donné envie de quitter Octopress :
-
la façon dont fonctionne Octopress 2 est « perfectible™ » : pour utiliser
Octopress il faut cloner le dépôt Git, puis créer son blog dans ce dépôt (ce
qui peut générer des conflits lorsque l’on souhaite mettre à jour Octopress
avec un
git pull
). C’est d’ailleurs l’un des défauts qui a amené l’auteur à créer Octopress 3. - Octopress lui-même semble plus ou moins mort : Octopress 2 n’est plus développé et le développement d’Octopress 3 semble à l’arrêt. Je n’ai pas vraiment envie de construire mon blog sur un framework potentiellement non-maintenu.
- Octopress est livré avec beaucoup de fonctionnalités par défaut, c’est très pratique pour débuter et ça m’a bien aidé au début. Mais maintenant que je veux avoir un contrôle plus fin et plus de compréhension sur la tambouille interne ce côté « magique » deviens un désavantage.
Quand j’ai décidé de migrer, en août 2017 (oui, il y a eu un looooong
délai entre l’envie et la réalisation concrète), j’ai demandé à
kAworu ce qu’il utilisait et la réponse
fut Nanoc. Après avoir fait quelques
recherches dessus, j’ai décidé d’utiliser Nanoc moi aussi.
Voici quelques points qui ont fait pencher la balance en sa faveur :
- c’est du Ruby, et le Ruby c’est fun, puissant et simple à utiliser.
- pas de magie par défaut, il faut vraiment faire les choses soit-même.
- la documentation est bonne.
- et surtout c’est testé et approuvé par kAworu ! :p
Changements visibles
Le changement le plus visible est bien évidemment le thème du blog : j’utilise
maintenant une CSS faite maison. Cependant, étant donné mes connaissances
limitées dans ce domaine, je suis parti sur un thème très simple. Malgré cela,
j’ai essayé de rendre le résultat responsive (du moins en partie…).
Notez également que, pour épargner vos yeux (mon mauvais goût en matière
d’associations de couleurs étant légendaire), je me suis limité aux couleurs
de Solarized.
Au niveau de la structure, là aussi j’ai beaucoup simplifié :
- la page d’accueil liste maintenant tous les articles publiés (elle remplace donc la page d’archives, qui a été supprimée) : cela permet d’avoir une vue d’ensemble du contenu du blog au premier coup d’œil.
- pas de pagination (devenu inutile avec la nouvelle page d’accueil).
- utilisation d’une unique page pour lister les articles classés par catégories. Je reviendrais peut-être à une page par catégorie (comme c’était le cas avant) quand j’aurai plus d’articles.
- chaque article possède maintenant une courte description qui est affiché sur la page d’accueil et la page des catégories.
En ce qui concerne les changements moins visibles :
-
remplacement des liens
http
parhttps
quand c’est possible (de nos jours, la plupart des sites sont accessible en HTTPS, ce qui n’était pas forcément le cas lors de la rédaction de certains articles). - réparation des liens cassés, en utilisant archive.org si nécessaire.
- nouvelle favicon (faite maison, avec mes maigres capacités artistiques).
- suppression de tout le JavaScript (il n’y en avait pas beaucoup à la base, mais maintenant il n’y en a plus du tout : 100% JS free).
Sous le capot
Le langage source
Le premier changement interne est le passage de Markdown à eRuby pour la rédaction des articles.
Le principal problème de Markdown est que le format est limité et il faut souvent utiliser des dialectes (comme le GitHub Flavored Markdown) pour avoir plus de fonctionnalités. Par exemple, Octopress utilise kramdown (qui est déjà un Markdown enrichi) couplé avec Liquid.
En utilisant eRuby, j’ai toutes les fonctionnalités nécessaires avec un seul langage, sans avoir à recourir à des extensions ou dialectes. Et en bonus j’ai un contrôle beaucoup plus fin sur le HTML généré (au contraire de Markdown), ce qui est très pratique.
La coloration syntaxique
Pour la coloration syntaxique des blocs de code, il n’y a rien de prévu par défaut dans Nanoc (au contraire d’Octopress). Je suis donc allé faire un tour sur le blog de kAworu pour voir ce qu’il utilisait : Rouge via un filtre Nanoc fait maison.
Je me suis donc très fortement inspiré de son approche, avec toutefois quelques petites simplifications (étant donné que je n’avais pas besoin de certaines fonctionnalités).
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 | require 'json' require 'rouge' Nanoc::Filter.define(:highlight) do |content, _params| content.gsub(/```(?<options>\{[^}]*\})?\n(?<code>.+?)```$/m) do # Get options. options = JSON.parse(Regexp.last_match(:options) || '{}') lang = options.fetch('lang', 'text') title = options.fetch('title', '') linenos = options.fetch('linenos', true) # Highlight code. formatter = Rouge::Formatters::HTML.new formatter = Rouge::Formatters::HTMLTable.new(formatter) if linenos lexer = Rouge::Lexer.find(lang) code = formatter.format(lexer.lex(Regexp.last_match(:code))) # Wrap the result. title = "<figcaption>#{title}</figcaption>" unless title.empty? code = "<pre>#{code}</pre>" unless linenos <<-HTML <figure class="code"> #{title} <div class="highlight"> #{code} </div> </figure> HTML end end |
La gestion des catégories
Nanoc fourni déjà quelques fonctions pour gérer les catégories sur les articles. Malheureusement ces fonctions ne gèrent pas très bien les catégories qui contiennent des espaces (« Trucs et astuces » par exemple), et cela pose problème si l’on souhaite utiliser une catégorie en tant qu’ancre ou ID HTML.
J’ai donc réimplémenté moi-même certaines de ces fonctions :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # Return the articles tagged with `tag`, sorted by descending creation date. def articles_with_tag(tag) sorted_articles.select { |article| article.fetch(:tags, []).include?(tag) } end # Return a formatted list of tags for the given item as a string. def tags_for(item, separator: ', ') base_url = '/blog/tags/#' item[:tags].map do |tag| %(<a href="#{base_url}#{to_html_id(tag)}" rel="tag">#{tag}</a>) end.join(separator) end # Return a version of `str` that can be used as an HTML ID. def to_html_id(str) str.downcase.tr(' ', '-') end |
Au passage, j’ai également dû coder une fonction qui me renvoie la liste des
catégories triée par ordre alphabétique. Le fait que j’utilise le français
implique que je ne peux pas utiliser la fonction sort
de Ruby car
elle ne gère pas correctement l’ordre des caractères non ASCII.
On voit que Dé
est placé après Di
, ce qui est
incorrect : il devrait être placé avant Di
(mais bien
après De
). Pour pallier au problème, j’ai utilisé la gem
sort_alphabetical :
Cette fois, l’ordre est correct. Il suffit ensuite d’utiliser cette fonction sur la liste des catégories :
1 2 3 4 5 6 7 8 | require 'sort_alphabetical' # Return a list of existing tags, sorted alphabetically. def tags @items.inject(Set.new) do |tags, item| tags.merge(item.fetch(:tags, [])) end.to_a.sort_alphabetical end |
Les vérifications automatisées
Nanoc propose un système de validation qui permet d’exécuter (manuellement ou avant de déployer le site) des étapes de vérifications sur le contenu du site généré. C’est extrêmement pratique pour détecter des erreurs (telles que des liens cassés) lors du développement/avant de déployer le site.
J’ai donc mit à jour mon nanoc.yaml
pour activer les
vérifications suivantes :
css_local
html_local
atom
sitemap
external_links
internal_links
stale
external_links
, internal_links
et stale
sont fournis par Nanoc. Les deux premiers vérifient que les liens utilisés
sont valides et le dernier vérifie qu’il n’y a pas de fichiers inattendu dans
le répertoire qui est copié lors du déploiement.
HTML et CSS
css_local
et html_local
vérifient que le CSS et le
HTML sont bien valides. Nanoc propose déjà ce genre de vérification
via css
et html
, mais ils ne me convenaient pas pour
les raisons suivantes :
- ils ne remontent que les erreurs, or les avertissements sont parfois intéressants à prendre en compte (même si pas obligatoire).
- ils fonctionnent en faisant des requêtes sur les validateurs du W3C, ce qui requiert d’avoir un accès à Internet.
C’est pourquoi j’ai implémenté css_local
et html_local
qui utilisent un
validatornu
installé localement.
Un autre avantage à cela, en plus de fonctionner hors-ligne, est que je peux
maintenant avoir accès aux avertissements (et ignorer ceux qui me semblent non
pertinents).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | Nanoc::Check.define(:css_local) do require 'json' require 'open3' @output_filenames.each do |filename| next unless File.extname(filename) == '.css' command = %W[validatornu --css --format json #{filename}] output, _ = Open3.capture2e(*command) next if output.empty? JSON.parse(output)['messages'].each do |error| reason = error['message'].delete_prefix('CSS: ') errmsg = "l.#{error['lastLine']} - #{reason}" add_issue(errmsg, subject: filename) end end end |
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 | Nanoc::Check.define(:html_local) do require 'json' require 'open3' IGNORE_LIST = [ # Nope, that's not the HTML5 way. 'Consider using the “h1” element as a top-level heading only', # My <article> use h1, they have header… 'Article lacks heading. Consider using “h2”-“h6”' ].freeze @output_filenames.each do |filename| next unless File.extname(filename) == '.html' command = %W[validatornu --html --format json #{filename}] output, _ = Open3.capture2e(*command) next if output.empty? JSON.parse(output)['messages'].each do |error| errmsg = error['message'] next if IGNORE_LIST.any? { |str| errmsg.include?(str) } add_issue("l.#{error['lastLine']} - #{errmsg}", subject: filename) end end end |
Sitemap
sitemap
vérifie, à l’aide de l’excellente bibiothèque
Nokogiri, que le sitemap.xml
généré est un XML valide ET qu’il respecte bien le schéma
attendu (j’ai récupéré le sitemap.xsd
dans
les sources du blog de kAworu).
1 2 3 4 5 6 7 8 9 10 11 12 13 | Nanoc::Check.define(:sitemap) do require 'nokogiri' @output_filenames.each do |filename| next unless File.basename(filename) == 'sitemap.xml' xsd = Nokogiri::XML::Schema(File.read('misc/sitemap.xsd')) sitemap = Nokogiri::XML(File.read(filename)) xsd.validate(sitemap).each do |error| add_issue(error.message, subject: filename) end end end |
Atom
Pour atom
c’est le même principe que sitemap
: on
utilise Nokogiri pour valider le XML de atom.xml
, ainsi que la
validité du schéma.
1 2 3 4 5 6 7 8 9 10 11 12 13 | Nanoc::Check.define(:atom) do require 'nokogiri' @output_filenames.each do |filename| next unless File.basename(filename) == 'atom.xml' rng = Nokogiri::XML::RelaxNG(File.read('misc/atom.rng')) sitemap = Nokogiri::XML(File.read(filename)) rng.validate(sitemap).each do |error| add_issue(error.message, subject: filename) end end end |
On remarquera que cette fois le schéma n’est pas défini par un
fichier .xsd
. En effet, il n’y a pas de XSD
(XML Schema Definition) officielle pour le format Atom. La
RFC 4287
fourni une spécification Relax NG (Regular
Language for XML Next
Generation). Cependant il y a un problème : la
RFC utilise la
syntaxe compacte, et
cette syntaxe ne peut pas être utilisée par Nokogiri. Heureusement, il est
possible de la convertir en syntaxe standard (utilisable par Nokogiri)
avec trang.
Conclusion
La migration fut relativement simple, je n’ai pas eu trop de glue à faire moi-même. Seule la conversion des articles en Markdown vers eRuby a été un peu laborieuse, mais ça m’a permis de fixer deux ou trois trucs au passage, donc plutôt positif au final.