Le rendu statique personnalisé avec Next.js et le Megaparam

|

Vous préférez les vidéos ? Vous apprécierez ma présentation "Architecture licorne pour le rendu arc-en-ciel" à la conférence Sunny Tech qui résume le contenu de cet article.

Le rendu statique n’a aucune limite

J'ai publié il y a quelques semaines une nouvelle traduction de mon article "Le rendu arc-en-ciel dans Next.js". Ce rendu arc-en-ciel est une extension du rendu statique à plusieurs versions d'une même page.

Les cas d'usage sont nombreux, par exemple implémenter efficacement l'internationalisation, mettre en place des tests A/B, des pages payantes… sans pour autant renoncer au rendu statique (côté serveur, au moment de la compilation), qui le plus performant et le moins cher.

Je l'appelle aussi "rendu segmenté", mais je trouve l'image de l'arc-en-ciel plus poétique : cette technique permet de générer une même page en autant de couleurs que l'on veut, sans impact sur les performances !

Quand j'ai conçu ce pattern il y a quelques années, il était assez difficile à implémenter.

Le rendu arc-en-ciel repose sur l'idée de mettre en place un serveur en amont d'un framework que Next.js. Ce micro-serveur permet de rediriger l'utilisateur vers la bonne version du site et éventuellement de vérifier ses autorisations.

J'appelle l'architecture sous-jacente au rendu arc-en-ciel l'architecture "licorne", car ce petit serveur en amont est comme une corne de licorne : quand la requête le traverse, c'est là que la magie opère !

Schéma architecture licorne

L'architecture licorne pour le rendu arc-en-ciel : une architecture qui permet de personnaliser des rendus statiques.

Les choses ont changé en octobre 2021, quand Next.js 12 a introduit le concept de Edge Middleware, ce qui a enfin permis de mettre en place le rendu arc-en-ciel facilement.

Les middlewares, première étape pour le rendu arc-en-ciel

Dans Next.js, un middleware est une petite fonction qui vous permet de traiter une requête avant de renvoyer une page ou une réponse d'API.

Un cas d'usage courant est d'authentifier/autoriser l'utilisateur, et le rediriger vers une page de connexion si besoin.

Dans un middleware, on peut réécrire l'URL, c'est-à-dire faire une sorte de redirection, mais non visible pour l'utilisateur final. Cela permet de découpler l'URL vue par l'utilisateur, et le chemin affiché.

Par exemple, une requête vers l'url /home avec un cookie theme=sombre peut être réécrite pour pointer vers le chemin /sombre/home. L'utilisateur ne verra pas le paramètre "sombre", mais votre application Next.js aura accès à ce paramètre au niveau des pages.

En détail, dans cet exemple :

  • la structure de fichier est app/[theme]/page.tsx , "theme" est un paramètre de route
  • la fonction generateStaticParams nous permet de générer une version sombre et une version claire
  • l'utilisateur ouvre l'URL /my-page dans son navigateur. Imaginons qu'il avait précédemment choisi le thème sombre, sa requête contient donc un cookie theme=sombre
  • le middleware réécrit l'URL en /sombre/my-page
  • l'utilisateur voit la page sombre

Le chargement est très rapide grâce au rendu statique, mais il est quand même personnalisé ! Voici le code final au niveau de la page:

// app/[theme]/page.tsx
export default function HomePage({params}) {
    // grâce au middleware, pas besoin d'accéder aux cookies ici
    // les informations utiles ont été injectés un paramètre de route
    // => la page peut rester statique !
    return <div>Le thème courant est : {params.theme}</div>
}

Sans la réécriture d'URL, il aurait fallu accéder au cookie "theme" dans la page, ce qui déclenche soit un rendu serveur à la requête (dynamique), soit un rendu côté client. On perd alors la possibilité de faire un rendu statique, ce qui est dommage car il est plus efficace.

On résout donc ce problème en déportant la lecture du cookie dans un middleware, qui est en amont de l'application Next à proprement parler.

C'est un pattern très puissant, mais on peut faire encore mieux.

Le problème de l'URL longue

Que se passe-t-il si en plus du thème clair/sombre, on souhaite mettre en place des tests A/B ? Et un fonctionnement multi-organisation ?

On obtiendrait une structure de ce type : app/[theme]/[tenant]/[bucket]/page.tsx

La fonction generateStaticParams pour générer ces différentes combinaisons ressemble alors à ceci :

export async function generateStaticParams() {
 return [
      { theme: "sombre", tenant: "Machin SARL", bucket: "A"},
      { theme: "sombre", tenant: "Machin SARL", bucket: "B"},
      { theme: "clair", tenant: "Machin SARL", bucket: "A"},
      { theme: "clair", tenant: "SAS Truc", bucket: "B"},
 ]
}

Cela fonctionne, l'utilisateur ne voit pas les paramètres injectés dans l'URL, mais l'expérience développeur n'est pas terrible, la page se retrouve imbriquée dans trois dossiers.

Pas besoin d'aller sur des cas d'usage avancés pour rencontrer ce problème. Il est très courant pour les sites multi-domaines avec traduction, car on peut se demander s'il vaut mieux placer le tenant (organisation à laquelle appartient l'utilisateur) avant le langage, ou l'inverse, ce qui impacte ensuite la revalidation des pages : app/[tenant]/[locale] ou app/[locale]/[tenant] ?

Et si l'URL devient trop longue, on pourrait aller jusqu'à dépasser la limite de 255 caractères.

La solution à ce problème ? [M], le Megaparam.

M

[M] le Megaparam

Pour reprendre l'exemple du nom de l'organisation et de la langue de l'utilisateur, on peut se demander : lequel d'entre eux deux / speede l'autre 🎸 lequel d'entre eux deux / inspire l'autre 🎶

Je précise que dans la version anglaise initiale de cet article, je n'ai pas pu faire de blague sur M, le chanteur. C'était très frustrant.

Bref, qui a dit qu'il fallait un paramètre d'URL pour chaque paramètre d'URL ? Une structure de fichier plus efficace et plus symétrique pourrait être la suivante : app/[M]/page.tsx

"M" le "Megaparam" est simplement une combinaison de plusieurs paramètres, avec un petit calcul d'encodage/décodage pour qu'ils prennent moins de place.

Des exemples possibles:

  • séparer les valeurs avec des tirets : params.M === "light-truc_sarl-B" signifie afficher le thème clair pour l'entreprise Truc SARL et montrer la variante B d'un test A/B
  • encoder les valeurs : params.M === "l42B" signifie afficher le thème clair "l" pour l'entreprise d'identifiant 42 (Truc SARL) et la version B du site
  • passer au binaire : params.M === "1101" signifierait encore la même chose

L'encodage peut être aussi simple ou aussi complexe que vous le souhaitez, selon votre besoin. Ce pattern fonctionne bien à l'échelle et peut vous permettre de gérer une bonne dizaine de paramètres de personnalisation statique sans aucun soucis : organisation de l'utilisateur, groupe d'un test A/B, langage, segment marketing, contenu freemium ou payant, thème clair ou sombre…

L'encodage se fait dans le middleware au moment où l'URL est réécrite pour rediriger l'utilisateur vers la variante qui le concerne. Le décodage se fait lors de l'accès à l'objet params dans la page. Dans le cas d'un encodage avec tirets, il s'agit simplement d'appels a join("-") et split("-") .

Tout l'interêt est que nous pouvons personaliser la page, sans pour autant récourir au rendu serveur à la requête ou au rendu client dans le navigateur, qui sont beaucoup plus coûteux que le rendu statique.

Le mégaparam permet de garder une application propre quand on a beaucoup de paramètres de personnalisation.

Demo

Un "tiens voici une bonne démo" vaut mieux que deux "tu l'auras", alors voici une implémentation type du Megaparam : https://github.dev/eric-burel/megaparam-demo

Vous pouvez tester le résultat final sur cette adresse: https://megaparam-demo.vercel.app

Démonstration montrant deux cookies theme et company qui permettent une personalisation statique alors que l'URL n'a pas changée

Et voilà, en deux temps trois mouvements, vous pouvez maintenant utiliser le rendu statique pour a peu près n'importe quel type de personnalisation d'un site.

Si vous avez un doute sur le temps de compilation, qui augmente exponentiellement avec le nombre de paramètres : vous pouvez jouer sur la fonction generateStaticParams pour choisir quelles combinaisons sont pré-générées, et lesquelles ne seront générées que si un utilisateur se connecte réellement sur cette variante.

Si vous prenez en compte les capacités de revalidation et de rendu incrémental de Next, on peut dire que vous venez de découvrir la plus puissante architecture de rendu statique au monde, rien que ça ¯\_(ツ)_/¯

🇬🇧 Speak English? This article is a translation of my blog post "Render anything statically with Next.js and the Megaparam"


Quelques ressources pour approfondir

Vous avez aimé cet article? Vous aimerez peut-être mes formations Next.js. Je suis un ingénieur indépendant avec plusieurs années d'expérience dans la conception d'architectures frontend scalables, et j'aime par dessus tout partager les connaissances acquises au cours de mes expériences et mes recherches.

Découvrez ma formation Next.js en 3 jours Une introduction à Next.js pour bien saisir les enjeux du développement web moderne hybridant les logiques client et serveur.

Formation Next.js en 3 jours

https://www.formationnextjs.fr/

Mes masterclass en 1 journée sur des sujets spécifiques

Vous codez déjà avec Next.js ? Allons plus loin avec des cours avancés dédiés à des problématiques techniques complexes.

Une playlist Youtube où l'on suit le tutoriel Next.js Learn, en français

Le tuto Next.js Learn en français sur YoutTube

Vous êtes plutôt vidéo ? Voici une petite playlist où l'on suit le tutoriel officiel Next.js Learn (contenu gratuit et en français):

Mes cours vidéos (en anglais)

Newline cover

Next.js Patterns

Si vous avez aimé cet article, vous aimerez aussi...