Migrer de Jekyll à Next.js

Jekyll versus Next.js

Après une première version de ce site développée il y a environ un an avec Jekyll, on vient de déployer une nouvelle version basée sur Next.js. Tout ne s’est pas forcément bien passé, mais l’expérience était assez intéressante. Ça mérite bien un article de blog !

Commençons donc par la base…

Les sites statiques

Qu’est-ce qu’un site statique

Pour faire simple, un site statique est un site où chaque page web disponible correspond à un fichier HTML sur un serveur HTTP. Cela s’oppose à un site dynamique, où les “fichiers” n’existent pas, et sont à la place générés sur demande par un logiciel.

Certaines fonctionnalités sont difficiles voire impossibles à implémenter dans un site statique. Ainsi, un moteur de recherche dans les pages du site par exemple nécessite forcément un logiciel pour chercher les pages qui contiennent la requête. Ou ajouter un système de commentaires à un blog demande un logiciel qui pourra ajouter de nouveaux commentaires à une base de données, et récupérer tous les commentaires liés à une publication.

Aussi, il est heureusement possible d’avoir un site statique sans écrire tous les fichiers HTML à la main ! C’est le but des générateurs de sites statiques. Le principe en gros est d’avoir un logiciel qui va générer un fichier HTML pour chaque page du site, plutôt que de le faire sur commande.

Comparaison sites statiques et dynamiques

Pourquoi faire des sites statiques en 2020

Un site statique peut être hébergé gratuitement et facilement sur GitLab Pages ou GitHub Pages. C’est un atout considérable, parce que ça évite d’avoir à maintenir et mettre à jour un serveur payant, qui peut aussi tomber. Je ne compte pas les fois où un lien d’Hacker News m’a renvoyé des 503 ou 504, parce qu’il y a trop de visites simultanées. Ce risque est pratiquement inexistant avec les sites statiques, car ils sont très peu coûteux à servir. C’est donc un choix économique et écologique.

Ensuite, il existe de nombreux générateurs de sites statiques, où les contenus, les templates et tout le code sont des fichiers, et donc versionnables. Le plus connu est sans doute Jekyll. Il a été développé par un des cofondateurs de GitHub, et a été popularisé vers 2008 - 2010, entre autres justement grâce à sa simplicité à déployer sur GitHub Pages.

Pour avoir eu à écrire dans plusieurs blogs sous Jekyll vers ces années-là, je me rappelle que c’était assez révolutionnaire : J’écrivais mes contenus en Markdown comme dans les tickets et fichiers README sur GitHub. Si je publiais du code, la coloration syntaxique était très simple à mettre en place, grâce à Pygments. Et on pouvait même avoir des commentaires dynamiques branchés dessus dans des iframes, grâce à Disqus - qui semble avoir pas mal changé depuis. Et tout ça gratuitement, et sans avoir à maintenir le moindre serveur.

Jekyll

Notre problème avec Jekyll

Jekyll a un moule de base très strict. Quand on veut un site personnel typique de développeur, avec des pages vitrines figées, puis un blog, c’est parfait. Dès qu’on sort de ça, ça devient vite compliqué. Internationaliser les contenus, avoir différents types de contenus à générer (plusieurs blogs par exemple), avec un routage sur mesure… Ces fonctionnalités ne sont pas supportées par défaut.

Alors certes, il existe pleins de plugins qui adressent ces points-ci, mais ils sont de qualités variables et j’ai personnellement beaucoup de mal à les évaluer, étant loin de la communauté Ruby. Aussi, on peut largement étendre les capacités de Jekyll en écrivant du Ruby, mais c’est trop loin de mes compétences et envies du moment.

Bref. On veut pouvoir redévelopper notre site web, potentiellement plusieurs fois, et on ne sait pas quelle forme cela prendra.

Notre besoin

Paul, Benoît et moi avons développé pratiquement tous nos projets cette année en TypeScript, et Angular, Angular.js ou React. C’est devenu l’environnement le plus confortable pour nous pour rendre des contenus web. Nous avons donc naturellement cherché un générateur de sites statiques en JavaScript.

Paradoxalement, on cherche aussi à conserver un site sans runtime JavaScript. En gros, on veut un site, pas une application, donc on ne voit pas le besoin de faire plus compliqué que ça. On pourrait vouloir une application web progressive, pour bénéficier de transitions modernes entre nos pages par exemple, mais on n’en voit pas vraiment l’utilité. Et puis ça a un côté low-tech qui nous plaît bien.

Je suis vite tombé sur Next.js et Gatsby.js. Mais l’expérience de nos ami·e·s du médialab de Sciences-Po m’a un peu refroidi sur Gatsby, et Next.js offrait un exemple ressemblant beaucoup à un site Jekyll, ce qui m’a convaincu d’essayer.

Next.js

Next.js

Next.js est un framework pour développer des sites web avec React. Parmis les avantages qu’il présente et qui nous intéressent :

Il se présente comme un système hybride entre génération statique (SSG : Static Sites Generation) et rendu statique (SSR : Server Side Rendering). Le côté SSG nous intéresse beaucoup, le côté SSR moins, vu qu’il consiste à pré-rendre un contenu dynamique côté serveur, et nécessite donc un serveur d’application.

Pas de configuration

Autre argument : Il vante du “Zéro Config”. Entre autres, cela veut dire qu’il supporte nativement le TypeScript (un bon point pour nous), et pleins d’autres langages et extensions. Par exemple, pour compiler nos feuilles de styles SASS, il nous a suffit d’installer npm install sass, sans aucun autre effort - et c’était bien documenté.

Pour appuyer ce point, ci-dessous la dernière version du next.config.js de notre site :

module.exports = {
  trailingSlash: true,
  url: "https://www.ouestware.com",
};

Promesse tenue.

Au passage, l’interface en ligne de commande de Next.js expose différentes actions pour lancer un serveur de développement par exemple. Celle qui nous intéresse ici, c’est next export, qui génère le site statique. Attention, cette commande doit être précédée de next build pour fonctionner correctement.

Gestion du routage

Pour les routes, c’est très simple. Le générateur conserve l’arborescence des fichiers dans le dossier pages du projet. Ainsi, on peut attendre qu’à partir de l’exemple ci-dessous…

mon-projet-next
├── ...
├── pages
│   ├── foo.tsx
│   └── bar
│       ├── toto.tsx
│       └── tutu.tsx
├── ...

…on obtienne un site avec les pages :

mon-projet-next/out
├── foo.html
├── bar/toto.html
└── bar/tutu.html

Il est aussi possible de passer des sortes de variables, en entourant le nom d’un fichier ou d’un dossier par des crochets. Supposons qu’on veuille générer toto.html et tutu.html à partir du même fichier source. On peut alors passer à l’arborescence suivante :

mon-projet-next
├── ...
├── pages
│   ├── foo.tsx
│   └── bar
│       └── [slug].jsx
├── ...

Mais comme on veut générer les pages, il faut donner une règle à Next.js pour qu’il connaisse la liste exhaustive des valeurs que peut prendre slug. Pour cela, depuis notre fichier pages/bar/[slug].jsx, il suffit d’exporter :

export async function getStaticPaths() {
  return {
    fallback: false,
    paths: [{ params: { slug: "toto" } }, { params: { slug: "tutu" } }],
  };
}

La fonction getStaticPaths est asynchrone, ce qui facilite le fait de générer les pages à partir de contenus stockés “ailleurs” que dans le code (base de données, fichiers…).

L’attribut fallback doit être égal à false dans notre cas, car il ne fonctionne que pour des sites servis directement par Next.js (et non exportés statiquement comme on fait ici).

Le contenu d’une page

Le plus simple c’est de montrer un exemple, en remplissant notre fichier pages/bar/[slug].jsx :

import React from "react";

export default ({ totoOrTutu }) => (
  <div>
    <h1>Vous êtes sur la page {totoOrTutu}.html</h1>
    <p>Vous avez bien de la chance.</p>
  </div>
);

export async function getStaticPaths() {
  return {
    fallback: false,
    paths: [{ params: { slug: "toto" } }, { params: { slug: "tutu" } }],
  };
}

export async function getStaticProps({ params }) {
  // Ici, on récupère la valeur effective de `slug` :
  return {
    props: {
      totoOrTutu: params.slug,
    },
  };
}

Les fichiers HTML générés ressemble alors à ceci (là, pour out/bar/toto.html) :

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <meta name="next-head-count" content="2" />
    <noscript data-n-css="true"></noscript>
    <!-- Pleins de balises link ajoutées par Next.js -->
  </head>
  <body>
    <div id="__next">
      <div>
        <h1>
          Vous êtes sur la page
          <!-- -->toto<!-- -->
          .html
        </h1>
        <p>Vous avez bien de la chance.</p>
      </div>
    </div>
    <!-- Pleins de balises script ajoutées par Next.js -->
  </body>
</html>

Dernier soucis à résoudre : les balises script ajoutées par Next.js permettent au JavaScript de reprendre la main sur le rendu et la navigation, à la manière des applications web progressives. Or, nous voulions un site sans runtime JavaScript. Next.js permet de ne pas charger ce runtime, grâce à une option bien planquée (je ne l’ai pas vue documentée, et je l’ai trouvée grâce à cette merge request) :

// À ajouter à la fin de chaque page, dont `pages/bar/[slug].jsx` :
export const config = {
  unstable_runtimeJS: false,
};

Et voilà !

coding

Notre expérience

Le code source de notre site web est publié sur notre GitLab. L’ancienne version en Jekyll est archivée dans la branche jekyll_old.

Un projet en cours de développement

La plupart des frictions que nous avons eues avec Next.js sont liées à son état de développement.

Aussi, le coup du unstable_runtimeJS non documenté représente assez bien l’état actuel du projet, à mon sens. On a eu pleins de petits moments où une fonctionnalité n’est pas documentée, ou alors elle l’est mais elle a été renommée depuis dans le code, ou encore elle ne marche pas comme prévu… Et comme Next.js est quand même mieux conçu pour faire des applications web progressive que des sites sans runtime, comme on le souhaitait, pleins de fonctionnalités bien stables et documentées ne s’appliquent en fait pas à notre usage.

On a aussi eu quelques problèmes liés à des choix de conceptions qui nous ont paru douteux.

Une des choses qui nous a pris le plus de temps a été d’avoir la bonne valeur pour l’attribut lang de la balise <html> dans chaque page. En effet, cette balise ne peut pas être écrite dans les pages elles-mêmes, et ne peut être personnalisée que via une classe Document sur mesure. Et on n’a jamais réussi à faire remonter les props de nos pages jusqu’à cette classe.

La solution de Benoît a finalement consisté à récupérer la balise meta Open Graph "og:locale" déjà présente dans chaque page, depuis notre Document sur mesure… Pas vraiment le genre de code qu’on a envie d’écrire ni de maintenir.

Autre pépin : les fichiers statiques (images, polices d’écriture…) doivent nécessairement être stockées dans le dossier public à la racine du projet, et ce dossier ne peut pas porter d’autre nom. Or, c’est aussi le nom du dossier que GitLab cherche pour déployer des sites statiques sur GitLab Pages. C’est vraiment pas grand chose mais c’est toujours un peu dommage…

Bref : Next.js est un projet jeune, en cours de développement, et utiliser un outil à ce stade a un coût. Cependant, c’est aussi un bon signe, car Next.js est bien maintenu, et sa communauté est réactive.

Une expérience de développement positive

J’ai beaucoup aimé travailler avec Next.js.

Le fait de calquer l’arborescence des fichiers pour générer celle du site me semble une assez bonne idée. Ça rend le développement et la navigation dans le code assez simples. Et ça nous a permis de facilement gérer l’internationalisation. Voici par exemple le code de la page en/index.tsx de notre site :

export { default } from "../index";

export const getStaticProps = async () => {
  return {
    props: {
      lang: "en",
    },
  };
};

export const config = {
  unstable_runtimeJS: false,
};

En gros : On importe le code de la page racine index.tsx, puis on écrase les props en remplaçant la langue par "en". Les traductions sont renseignées directement dans le code de la page racine, et ça fonctionne très bien.

Il s’avère que sur notre site, les pages en anglais sont préfixées par /en dans l’URL, sauf les articles de blog anglophones, à cause des contraintes de Jekyll… L’avantage avec cette méthode, c’est qu’on a pu choisir précisément la langue de chaque page, indépendamment entre autres de son chemin. Ainsi, on n’a eu aucun problème à porter la contrainte de Jekyll sur notre nouveau site.

Pareil, j’ai particulièrement apprécié de résoudre les problèmes que j’ai rencontrés avec des solutions de l’écosystème Node.js, et pas des plugins Next.js, comme c’était généralement le cas avec Jekyll. Par exemple, pour ajouter le support des articles de blog écrits en AsciiDoc, il nous a juste fallu trouver une solution pour parser des fichiers **.adoc et générer le HTML correspondant, *en JavaScript**.

Pour moi, Next.js a résolu bien plus de problèmes qu’il n’en a posés. On a maintenant un site développé avec le langage et les outils avec lesquels on est le plus efficaces, et on n’a plus peur d’ajouter de nouvelles pages ou fonctionnalités.

summer_landscape

Conclusion

Next.js semble très prometteur. J’espère qu’on aura bientôt l’occasion de l’utiliser pour des applications web progressives, voir comment il se comporte dans sa zone de confort.

Mais surtout, Jekyll a paradoxalement longtemps été à la fois la seule solution que j’envisageais pour faire des sites statiques, et un outil que je ne voulais vraiment pas utiliser. Je suis très content d’avoir enfin quitté Jekyll une bonne fois pour toutes !