Mot-clef « Migration »

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 WordPerss.

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.


Plus on enlève de code, mieux ça marche

Encore récemment, dans le cadre du chiffrage d'une migration d'un projet vers la dernière version d'RBS Change (CMS / e-commece dont j'ai déjà parlé plusieurs fois ici), l'un des développeurs disait en parlant de certaines fonctionnalités développées sur le projet en spécifique et qui entre temps ont été implémentées dans le produit que maintenant que le code spécifique était écrit, ça ne coûtait pas cher de le garder tel quel plutôt que de prendre le temps de le remplacer par des appels au code du produit.

À part si un besoin spécifique n'est pas compatible avec le code du produit, je suis intimement convaincu que c'est faux. Et ce pour un certain nombre de raisons.

Le point le plus évident pour moi c'est le coût en maintenance. S'il y a bien une chose que j'ai appris en travaillant sur un logiciel qui fait plusieurs centaines de milliers de lignes de code, c'est que plus on a de code, plus c'est couteux à maintenir. D'une part parce que chaque ligne ajoutée peut comporter des bugs ou s'avérer incompatible avec d'autres parties du logiciel et d'autre part parce que plus il y a de code, plus il est difficile de retrouver la source d'un problème. C'est d'autant plus vrai si l'équipe chargée du projet change. Quand le développeur est le même pendant des années, il peut connaitre assez bien son code pour s'y retrouver parmi les implémentations parallèles (et encore... que celui qui ne s'est jamais senti perdu en se replongeant dans du code qu'il avait écrit rien qu'un an plus tôt lève la main) mais un nouvel arrivant sur le projet mettra beaucoup plus de temps à s'y retrouver s'il doit apprendre à connaitre les implémentations parallèles en plus du produit lui-même.

On en arrive du coup à un second point connexe au précédent : l'évolutivité. Naïvement on se dit que vu que la fonctionnalité est codée spécifiquement, on peut faire ce qu'on veut avec et donc on est bien plus libre qu'en utilisant une fonctionnalité native du produit sur laquelle on n'aura pas autant la main. Ce n'est pas faux. Mais cela implique de se couper en partie des évolutions du produit et de devoir tout faire soi-même de son côté. De plus, se pose le même problème qu'évoqué précédemment où tout nouvel arrivant devra oublier ce qu'il sait déjà du produit pour apprendre ce que fait le projet.

Ensuite on a les coûts d'interface utilisateur et d'apprentissage. De deux choses l'une : soit on laisse les deux implémentations parallèles accessibles dans l'interface et là c'est l'utilisateur qui se sentira perdu, ne sachant quoi choisir (ce qui implique du coût de formation et de réparation de ce que l'utilisateur aura mal fait), soit on doit masquer de l'interface les éléments relatifs à l'implémentation standard pour les remplacer par l'implémentation spécifique (ce qui implique un coût initial plus un coût à chaque mise à jour pour revalider les choses et les réadapter si besoin).

Après on peut avoir du mal à jeter le produit de nombreuses heures de développement. C'est normal mais pour un développeur c'est une chose à laquelle il faut s'habituer. Un logiciel qui n'évolue pas, à part s'il est extrêmement ciblé sur un besoin très pointu qui n'évolue pas du tout (chose très rare), c'est un logiciel mort. Un logiciel vivant évolue continuellement au gré des nouveaux besoins, des nouvelles possibilités et des nouvelles idées. Seulement on ne peut pas se contenter d'empiler de nouvelles choses dessus, sous peine de voir l'ensemble s'effondrer sous sa complexité, de devenir inmaintenable et incompréhensible au nouvel arrivant.

Des choses qui semblaient - et potentiellement étaient réellement - pertinentes à un moment donné ne le seront plus un an plus tard parce que le besoin aura évolué ou simplement parce qu'à force d'y greffer des verrues, on aboutit à un ensemble qui ne ressemble plus à rien. Il ne faut donc pas hésiter à remplacer des fois des pans entiers du logiciel pour repartir sur des bases plus saines. Qui elles-mêmes dégénèreront plus ou moins rapidement (selon la qualité de l’implémentation et la vitesse à laquelle les besoins liés évoluent) avant d'être à leur tour remplacées à nouveau.

Ce principe vaut aussi bien au niveau macroscopique (une API entière finit par devenir trop lourde et doit être remplacée) qu'au niveau microscopique (une méthode donnée peut souvent être réécrite en cinq fois moins de code parce qu'elle prévoyait des cas qui n'existent plus ou bien parce qu'à force de refactoring, le code se répète trop). Il faut toutefois prendre garde à ne pas sauter trop vite aux conclusions et tout réécrire continuellement, sans quoi d'une part on n'avance plus et d'autre part, à aller trop vite, on passe à côté de subtilités qui ne sautent pas aux yeux sans examen approfondi (pour ce dernier point, des tests unitaires ou autres peuvent aider à éviter les régressions, encore faut-il avoir le temps ou simplement prendre le temps de les mettre en place).

Comme souvent il s'agit de trouver un juste milieu entre tout réécrire et conserver à tout prix l'existant. Mais quand la réécriture consiste à utiliser quelque chose qui existe par ailleurs et qu'on n'aura donc pas à maintenir soi-même, je pense qu'il n'y a pas à hésiter : si fonctionnellement c'est compatible, ça vaut le coup de jeter le spécifique pour utiliser du natif.

Voilà voilà, félicitations si vous avez tout lu jusqu'ici et n'hésitez pas à réagir dans les commentaires si vous avez quelque chose à ajouter sur le sujet ou des objections à formuler :)