Catégorie « Jekyll »

Restauration des anciennes URL des articles

Un des derniers points qui manquaient depuis la résurrection de ce blog était de faire refonctionner les anciennes URL des articles.

En effet, lors du passage sous Jekyll, d’une part le nommage des articles a été modifié (par défaut Jekyll les met en .html là où ma réécriture d’URL sous WordPress me les faisait juste finir par un /) et d’autre part j’ai changé de domaine, passant de wp.darathor.com à blog.darathor.net.

Il manquait donc deux opérations :

  1. rediriger l’ancien domaine vers le nouveau
  2. rediriger les anciennes URL vers les nouvelles

La première étape n’a rien de compliqué et se fait assez rapidement dans la configuration des VHOST d’Apache.

La seconde est un peu moins immédiate puisqu’il m’a fallu construire la table de correspondance des anciennes URL vers les nouvelles.

J’ai choisi comme stratégie de stocker une liste d’alias dans l’en-tête de chaque articles et de générer un fichier .htaccess à partir de ça.

Déclaration des alias

La déclaration des alias est assez simple et se limite à une liste d’URL relatives :

aliases:
- /?p=3
- /2007/05/01/ouverture-du-site/

L’avantage de fonctionner avec une liste d’alias en URL relatives c’est que cette même mécanique peut être ré-exploitée si je veux changer l’URL d’un article par la suite (notamment pour corriger une coquille).

Reste à l’alimenter dans mon cas de migration depuis WordPress.

Le script de migration de WordPress vers Jekyll m’avait extrait une entrée de la forme wordpress_url: http://wp.darathor.com/?p=10 contenant une des formes d’URL proposée par WordPress. Par contre il ne s’agissait pas de la forme générée par la réécriture automatique de WordPress qu’il m’a fallu reconstruire pour aboutir aux deux alias ci-dessous. J’ai finalement choisi de garder les deux URL à chaque fois étant donné que les deux pointaient sur le contenu et que je n’aime pas les liens morts.

Génération du fichier .htaccess

Dans mon cas j’utilise Apache comme serveur web, donc je peux passer par un fichier .htaccess ce qui a un gros avantage : tout reste stocké dans le code versionné et déployable automatiquement sans aller toucher au VHOST.

Pour générer ce fichier j’ai procédé comme pour le robots.txt via une simple page nommée htaccess (sans extension sinon à la génération Jekyll la colle arbitrairement derrière le permalien) :

---
layout: null
permalink: .htaccess
---

ErrorDocument 404 /404.html

{% for page in site.pages -%}
{%- if page.aliases -%}
{%- for alias in page.aliases -%}
  Redirect 301 {{ alias }} {{ page.url }}
{% endfor -%}
{%- endif -%}
{%- endfor -%}

{% for post in site.posts -%}
{%- if post.aliases -%}
{%- for alias in post.aliases -%}
  Redirect 301 {{ alias }} {{ post.url }}
{% endfor -%}
{%- endif -%}
{%- endfor -%}

On retrouve donc l’en-tête avec un layout: null pour que le fichier généré ne soit pas habillé d’une structure HTML et un permalink: .htaccess indiquant le nom du fichier à générer.

Ensuite le contenu consiste en une simple boucle sur les articles avec à chaque fois une boucle sur les alias pour générer les redirections 301.

J’en ai également profité pour ajouter la ligne ErrorDocument 404 /404.html qui dit à Apache d’utiliser une page 404 personnalisée plutôt que la page par défaut.


Ajout du 07/10/2018 à 12h30

Dans mon .htaccess je ne gérais les alias que pour les articles et pas pour les pages. J’ai mis à jour le code ci-dessus pour en tenir compte également.


Moteur de recherche dans un site Jekyll

La problématique

Dernier des gros points noirs relevés lors de ma bascule sur Jekyll : l’absence de moteur de recherche.

Forcément quand le site est statique, c’est plus compliqué de mettre en place un moteur de recherche. Il y a plusieurs approches possibles :

  • faire une partie dynamique côté serveur pour la recherche : bof, le but c’est d’avoir maintenance côté serveur, donc non
  • faire appel à une API tierce qui ferait l’indexation puis la recherche : bof aussi, ça permet certes de rester statique côté serveur mais délocaliser la chose n’est pas mieux (en plus ça veut dire maintenance sur l’utilisation de l’API) donc non
  • déléguer complètement à un moteur externe type Google Search : ça existe sur certains sites (quoique ça fait un moment que je l’ai plus vu) sous la forme d’un formulaire de recherche redirigeant sur Google avec une recherche de type site:monsite.com ...… c’est mieux côté maintenance mais pire côté délocalisation puisqu’on quitte carrément le site, donc non

Reste la dernière : faire la recherche directement dans le navigateur en JS à partir d’un index fourni, solution finalement retenue.

Bon, c’est loin d’être parfait comme concept : ça ne passe évidemment pas à l’échelle notamment mais ce site reste un blog perso donc la volumétrie de devrait pas exploser…

L’index fait environ 500ko avec mes 156 articles actuels (représentant un peu plus de 10 ans) donc ça reste jouable, même si j’ai augmenté mon rythme ces derniers temps (notamment à cause des republications). Dans tous les cas on reste loin des pages d’accueil avec des gros carrousels d’images à 2Mo chacune qu’on trouve sur certains sites (notamment e-commerce) et ce n’est de toutes façons chargé que sur la page de recherche.

Donc c’est réaliste dans mon cas et vous pouvez le tester via le champ de recherche dans la colonne de droite.

Solution existante et adaptations

Je suis parti de cet article proposant une solution basée sur la bibliothèque Lunr mais comme souvent c’était loin d’être satisfaisant de base :

  • forcément c’est en anglais, donc fallait au moins traduire
  • ça ne permet pas de placer le champ de recherche sur toutes les pages (à moins d’inclure le code de recherche sur toutes les pages aussi, ce qui est exclus)
  • visuellement le rendu ne me convenait pas du tout
  • toutes les pages sont indexées, y compris les pages de catégories par exemple qui ne sont que des regroupements d’articles

Du coup j’ai réorganisé les choses différemment en :

  • séparant l’index dans un fichier JS à part
  • modifiant le formulaire pour qu’il fasse un simple GET vers une page dédiée au résultat de recherche
  • modifiant le JS pour que le terme à rechercher soit cherché dans l’URL
  • ajoutant dans l’en-tête des pages devant être indexées un paramètre searchable: true

La section suivante détaille la marche à suivre pour intégrer tout ça sur votre site.

Mise en œuvre

La bibliothèque Lunr

À récupérer ici et à ajouter dans le dossier assets/js.

Le code JS utilisant Lunr

Dans un fichier assets/js/search.js :

(function (window) {
	window.onload = function () {
		var idx = lunr(function () {
			this.ref('id');
			this.field('title');
			this.field('body');

			window.searchIndex.forEach(function (doc) {
				this.add(doc)
			}, this)
		});

		var resultsElement = document.getElementById('search-results');
		var HTML = '';
		var term = getQueryString('q');
		if (term) {
			document.getElementById('search-field').value = term;
			var results = idx.search(term);
			if (results.length > 0) {
				var plural = results.length > 1;
				HTML += '<p>' + results.length + ' résultat' + (plural ? 's' : '') + ' trouvé' + (plural ? 's' : '') + ' pour la recherche « ' + term + ' » :</p> <ul>';
				for (var i = 0; i < results.length; i++) {
					var result = window.searchIndex[results[i]['ref']];
					var url = result['url'];
					var title = result['title'];
					var date = result['date'];
					var body = result['body'].substring(0,160) + '...';
					var type = result.type === 'post' ? 'Article' : 'Page';

					HTML += '<li class="search-result"><p class="metadata"><a href="' + url + '" class="title">' + title + '</a>';
					HTML += ' <span class="type">' + type + '</span>';
					if (date) {
						HTML += ' <span class="date">publié le ' + date + '</span>';
					}
					HTML += '</p><p class="summary">' + body + '</p></li>';
				}
				HTML += '</ul>';
			}
			else {
				HTML += '<p>Aucun résultat trouvé poru la recherche « ' + term + ' ».</p>';
			}
		}
		else {
			HTML += '<p>Aucun terme à rechercher.</p>';
		}
		resultsElement.innerHTML = HTML;
	};

	/**
	 * Get the value of a query string.
	 * @param {string} field The field to get the value of
	 * @param {string=} url The URL to get the value from (optional)
	 * @return {string} The field value
	 */
	var getQueryString = function (field, url) {
		var href = url ? url : window.location.href;
		var reg = new RegExp( '[?&]' + field + '=([^&#]*)', 'i' );
		var string = reg.exec(href);
		return string ? string[1] : null;
	};
})(window);

Je n’ai pas cherché à extraire le rendu HTML du résultat de recherche, donc il faudra directement modifier ce fichier pour l’arranger.

Le fichier d’index : search-index.js

Dans un fichier search-index.html à la racine du projet :

---
layout: null
permalink: search-index.js
---

(function (window) {
  {%- assign counter = 0 %}
  window.searchIndex = [{% for page in site.pages %}{% if page.searchable %}{
    "id": {{ counter }},
    "url": "{{ site.url }}{{ page.url }}",
    "type": "page",
    "title": "{{ page.title | replace: '"', ' ' }}",
    "body": "{{ page.content | markdownify | replace: '.', '. ' | replace: '</h2>', ': ' | replace: '</h3>', ': ' | replace: '</h4>', ': ' | replace: '</p>', ' ' | strip_html | strip_newlines | replace: '  ', ' ' | replace: '"', ' ' }}"{% assign counter = counter | plus: 1 %}
  }, {% endif %}{% endfor %}{% for page in site.posts %}{
    "id": {{ counter }},
    "url": "{{ site.url }}{{ page.url }}",
    "type": "post",
    "date": "{{ page.date | date: '%d/%m/%Y à %R' }}",
    "title": "{{ page.title | replace: '"', ' ' }}",
    "body": "{{ page.content | markdownify | replace: '.', '. ' | replace: '</h2>', ': ' | replace: '</h3>', ': ' | replace: '</h4>', ': ' | replace: '</p>', ' ' | strip_html | strip_newlines | replace: '  ', ' ' | replace: '"', ' ' }}"{% assign counter = counter | plus: 1 %}
  }{% if forloop.last %}{% else %}, {% endif %}{% endfor %}];
})(window);

Il est possible de modifier ce fichier si vous voulez ajouter des informations à l’index (par exemple des données complémentaires pour l’affichage des résultats).

La page de résultat de recherche

Dans un fichier search.md à la racine du projet :

---
layout: page
title: Recherche
---

<script src="/assets/js/lunr.js?v={{ site.time | date: '%s' }}" type="application/javascript" async="async"></script>
<script src="/assets/js/search.js?v={{ site.time | date: '%s' }}" type="application/javascript" async="async"></script>
<script src="/search-index.js?v={{ site.time | date: '%s' }}" type="application/javascript" async="async"></script>

<div id="search-results">Recherche en cours...</div>

L’entrée layout est à adapter en fonction de ce que vous avez défini sur votre site.

Le formulaire de recherche

Il reste à intégrer le formulaire de recherche là où vous souhaitez de voir apparaitre :

<form action="/search.html" method="get" class="search-form">
  <p>
    <input id="search-field" type="text" name="q" maxlength="255" value="" />
    <button type="submit" title="Lancer la recherche"><img src="/assets/img/search.svg" alt="Lancer la recherche" /></button>
  </p>
</form>

Pages indexables

En l’état, seuls les articles seront indexés. Il reste donc à ajouter searchable: true dans l’en-tête de chaque page que vous souhaitez indexer.


Quelques trucs sur Jekyll #2

Lisibilité des erreurs

Par défaut, le serveur de développement de Jekyll remonte des erreurs assez sommaires en ne précisant que le template concerné. C’est inutilisable quand on débugue un template (même si un numéro de ligne ne serait pas du luxe) mais quand l’erreur vient d’un plugin c’est plutôt limité.

Pour un affichage un peu plus détaillé avec une trace d’exécution permettant de déterminer quelle ligne de quel plugin est en cause, il faut utiliser l’option --trace ou -t.

bundle exec jekyll serve --drafts -t

Fichiers sitemap.xml et robots.txt

Il existe un plugin pour générer le fichier sitemap.xml, par contre je n’ai rien trouvé pour gérer son intégration au fichier robots.txt. On peut certes faire un fichier robots.txt à la main mais le lien est censé être absolu, donc ce n’est pas super propre de le mettre en dur.

Finalement je m’en suis sorti en ajoutant un fichier robots.html avec le contenu suivant :

---
layout: null
permalink: robots.txt
---

Sitemap: {{ site.url }}/sitemap.xml

La directive permalink: robots.txt permet de forcer le bon nom de fichier, layout: null dit que le contenu ne doit pas du tout être habillé et comme on est dans une page normale on a accès aux variables globales, notamment site.url.

Éviter la profusion d’espaces dans les pages

Le moteur de template de Jekyll fonctionne en ajoutant des pseudo-balises dans le code HTML notamment pour indiquer les structures de contrôles, boucles, affectations de variables, etc.

Pour que le code reste lisible on fait des retours à la ligne entre ces différentes balises et on indente. Ceci aboutit à de très nombreuses lignes uniquement peuplées d’espaces (ceux de l’indentation).

Exemple :

<h2>Catégories</h2>
{% assign groupedCategories = site.categories | group_categories %}
{% for group in groupedCategories %}
  <h3>{{ group[0] }}</h3>
  <ul>
    {% assign categories = group[1] | sort_by_keys %}
    {% for category in categories %}
      <li><a href="/categories/{{ category[0]|slugify:'latin' }}/">{{ category[0] }}</a> ({{ category[1].size }})</li>
    {% endfor %}
  </ul>
{% endfor %}

Il est possible d’y remédier en utilisant un caractère - dans les balises. Chaque - suivant une ouverture de balise ({% devient {%-) indique que les espaces précédents doivent être ignorés et sur les fermetures ( (%} devient -%})) que les espaces suivants doivent être ignorés.

L’exemple précédent devient alors :

<h2>Catégories</h2>
{%- assign groupedCategories = site.categories | group_categories -%}
{% for group in groupedCategories %}
  <h3>{{ group[0] }}</h3>
  <ul>
    {%- assign categories = group[1] | sort_by_keys -%}
    {% for category in categories %}
      <li><a href="/categories/{{ category[0]|slugify:'latin' }}/">{{ category[0] }}</a> ({{ category[1].size }})</li>
    {%- endfor %}
  </ul>
{% endfor %}

C’est la même syntaxe que l’on retrouve dans d’autres moteurs de templates comme Twig pour PHP (la plupart des syntaxes sont assez proches en fait entre ces deux moteurs).

Remarque : cette syntaxe semble ne pas bien fonctionner avec le tag endraw, en effet j’ai des erreurs dès que je place un tiret un en début de ce tag (alors qu’à la fin et sur le tag raw ça marche)…


Plugin de groupement des catégories pour Jekyll

Voici mon second plugin pour Jekyll, toujours lié à mon menu de catégories.

Contexte

En plus de 10 ans d’existence de ce blog, j’ai beaucoup varié les thématiques de mes articles. Au début j’étais purement en mode bloc-notes sur des astuces techniques pour pouvoir les retrouver facilement. Puis j’ai pas mal parlé de l’iPhone, de mes développements perso, de mes créations “artistiques” (BD, cartes Magic, etc)… à force ça commence à faire beaucoup de catégories et ce n’est plus très lisible dans une liste en vrac.

Du coup j’ai voulu refaire ce que WordPress proposait via les sous-catégories : grouper les catégories par thématiques. Je n’ai pas trouvé de plugin qui le fasse bien. J’en avais testé un qui avait l’air de faire ce que je voulais mais je n’ai pas réussi à le faire marcher… peut-être que j’aurais dû insister mais je ne l’ai plus retrouvé donc j’ai fini par décider d’en développer un moi-même. Après tout le fonctionnel est simple.

Pour le coup effectivement, rien de bien compliqué, un connaisseur de Ruby y aurait sans doute passé moins d’un quart d’heure. Bon perso je n’y connais rien à Ruby, je l’ai découvert en débugant des plugins ces trois derniers mois, donc forcément ça m’a pris un peu plus de temps mais j’ai finalement ce que je voulais ^^

Le plugin

Ce plugin se résume donc à un nouveau filtre Liquid qui prend en entrée les catégories et renvoie en sortie une un tableau à double-entrée avec pour chaque thématique un sous-tableau contenant l’ensemble de ses catégories.

Les thématiques ou « groupes de catégories » sont définis dans le fichier de configuration _config.yml, avec la liste des groupes et pour chacune la liste des catégories qui la composent. Les catégories non-affectées étant regroupées dans un groupe « Default » (une seconde entrée de configuration permettant de définir son libellé).

Le plugin ainsi qu’une documentation plus détaillée sont accessibles ici : jekyll-group-categories-filter

Utilisation sur ce blog

J’ai donc appliqué ça à ce blog en découpant la liste des catégories en 4 thématiques :

  • jeux : vu que ces derniers temps j’ai posté pas mal de trucs sur Magic et Carcassonne ça ne me semble pas superflu d’en faire une thématique à part
  • réalisations perso : mes diverses réalisations perso que ce soit du dev ou des trucs plus « artistiques »
  • informatique : un peu fourre-tout… j’ai hésité à découper plus mais j’ai du mal à délimiter les choses, donc dans l’immédiat on va rester là dessus
  • autres : tout le reste

Dans chaque thématique j’ai gardé le tri alphabétique grâce à mon précédent plugin, ce qui me donne le code suivant dans la sidebar :

<h2>Catégories</h2>
{% assign groupedCategories = site.categories | group_categories %}
{% for group in groupedCategories %}
  <h3>{{ group[0] }}</h3>
  <ul>
    {% assign categories = group[1] | sort_by_keys %}
    {% for category in categories %}
      <li><a href="/categories/{{ category[0]|slugify:'latin' }}/">{{ category[0] }}</a> ({{ category[1].size }})</li>
    {% endfor %}
  </ul>
{% endfor %}

Voilà, n’hésitez pas à me faire un retour, soit ici, soit dans le bug tracker du plugin s’il s’agit d’un bug ou d’une idée d’amélioration ^^


Quelques trucs sur Jekyll #1

Histoire de pas spammer avec trop d’articles je vais reprendre mon habitude de faire des tirs groupés pour les petites astuces, remarques et autres ^^

Publication avec les bonnes URL dans les métas SEO

Une petite subtilité que je n’avais pas remarquée tout de suite : les URL générées dans les métas SEO sont des URL absolues et dans les pages générées par le mode serveur dédié au dev (via bundle exec jekyll serve) commencent par l’IP locale et non le domaine spécifié dans le _config.yml (c’est pas illogique mais comme ça ne touche que des métas SEO, ça ne saute pas aux yeux).

J’ai rapidement trouvé dans la doc qu’il faut utiliser jekyll build pour générer en mode production. Plus précisément jekyll build --destination _deploy qui génère les pages dans un autre dossier histoire que les deux ne se marchent pas sur les pieds.

En y repensant c’est effectivement plus propre de travailler comme ça avec une commande dédiée à la génération pour la prod et un dossier séparé, ça évite de déployer par erreur des brouillons par exemple.

Brouillons et gestion des dates

Jekyll gère les brouillons en les plaçant dans un dossier à part et en ajoutant une option à la génération si on veut les inclure (cf la documentation).

Tout ça fonctionne très bien à une exception près : la doc dit que la date de l’article sera la date de dernière modification du fichier. Jusque-là pourquoi pas, ça se tient assez bien. Mais elle ne précise pas plusieurs subtilités :

  • d’une part ça ne fonctionne que pour le fichier Markdown et pas pour les HTML (là, l’article n’est apparemment pas visible sans date)
  • d’autre part il ne s’agit que d’une valeur par défaut dans le cas où la date n’est pas présente dans l’en-tête de l’article
  • enfin, si la date spécifiée est dans le futur, l’article ne sera pas visible, même pour un brouillon (donc on ne peut pas planifier une date à l’avance)

Dans l’ensemble c’est assez bancal comme fonctionnement je trouve, mais une fois qu’on sait comme ça marche, on s’en sort.