Les inscriptions sont fermées. Menestrel sert désormais aux sites réalisés par Agora Studio, agence web dans la Loire.

Le Content Layer d'Astro, expliqué par ses loaders

Le Content Layer a transformé les content collections d'un lecteur de dossier en une couche de données branchable. Ce qu'est réellement un loader, comment en écrire un, et pourquoi toutes les intégrations CMS pour Astro se ressemblent désormais.

Le Content Layer d’Astro est la chose la plus importante qui soit arrivée à l’intégration des CMS dans cet écosystème, et elle est mal expliquée. La plupart des articles montrent un extrait de configuration et passent à autre chose. Celui-ci va voir dessous, parce qu’une fois qu’on a compris ce qu’est un loader, toutes les intégrations CMS pour Astro cessent d’être magiques et deviennent évidentes.

Avant : une collection était un dossier

Dans les premières versions d’Astro, une content collection était un répertoire. Vous mettiez du markdown dans src/content/blog/, déclariez un schéma Zod, et getCollection('blog') vous rendait des entrées typées. Ça marchait bien, avec une limite dure : le contenu devait être des fichiers sur disque, dans votre projet.

Si votre contenu vivait dans un CMS, vous étiez seul. Chaque intégration inventait son approche : un await fetch() en haut d’une page, un script maison qui écrivait du markdown dans src/content/ avant la construction, une bibliothèque d’aide par éditeur. Aucune n’obtenait les deux choses que les collections offraient gratuitement : l’accès typé par getCollection(), et un cache de construction qui ne refait pas la requête à chaque page.

Après : une collection est ce que son loader dit qu’elle est

Le Content Layer renverse la logique. Une collection n’est plus un dossier ; une collection a un loader, dont le travail est de déposer des entrées dans un magasin. D’où viennent ces entrées ne regarde que le loader.

import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';

export const collections = {
  // Des fichiers sur disque : l'ancien comportement, devenu un loader parmi d'autres.
  docs: defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/docs' }) }),
};

Le loader glob n’est pas un cas particulier. C’est un loader ordinaire qui se trouve lire le système de fichiers. Remplacez-le par un loader qui lit une API, une base, un CSV ou un instantané sur un CDN, et tout ce qui suit reste identique : getCollection(), le typage, render().

C’est toute l’idée, et c’est pour ça que toutes les intégrations CMS publiées pour Astro depuis se ressemblent.

Ce qu’est réellement un loader

Au plus simple, un loader est un objet avec un name et une fonction load :

const myLoader = {
  name: 'my-loader',
  async load({ store, parseData, logger }) {
    const items = await fetchFromSomewhere();
    store.clear();
    for (const item of items) {
      const data = await parseData({ id: item.slug, data: item });
      store.set({ id: item.slug, data });
    }
  },
};

Trois points méritent qu’on s’y arrête.

store est la collection. Ce que vous y mettez est exactement ce que getCollection() rendra. Rien d’autre n’intervient.

parseData exécute votre schéma. C’est là que se fait la validation. Si la collection déclare un schéma Zod, parseData l’applique et lève une erreur utile désignant l’entrée fautive. L’ignorer revient à envoyer des données non validées dans du code typé, ce qui annule l’intérêt.

logger écrit dans la sortie de construction d’Astro. Servez-vous-en. Un loader qui échoue en silence pendant le déploiement d’un client, c’est un mauvais après-midi.

Il existe aussi un mécanisme de digest pour le travail incrémental : donnez une empreinte à store.set(), et Astro pourra sauter les entrées inchangées au passage suivant. Sur un petit site, c’est du bruit. Sur un site à plusieurs milliers d’entrées, c’est la différence entre une construction qu’on attend et une qu’on supporte.

La question du schéma

Un loader peut déclarer son propre schéma au lieu de vous en faire écrire un :

export const collections = {
  posts: defineCollection({ loader: menestrelLoader({ collection: 'posts' }) }),
};

Aucun schéma Zod en vue, et post.data.title est quand même typé. Le loader connaît la forme, parce que le CMS la connaît.

C’est un arrangement franchement meilleur que l’alternative, où vous déclarez le modèle dans le CMS puis à nouveau en Zod, et où les deux finissent par diverger. Quel que soit votre CMS, préférez le loader qui déduit le schéma à celui qui vous demande de le redire.

D’où vient le contenu au build, et pourquoi ça compte

Le Content Layer rend la source branchable, ce qui soulève une question à poser à toute intégration CMS : à quoi ma construction parle-t-elle exactement ?

Il y a trois réponses, avec des modes de panne très différents.

L’API vivante du CMS. Simple, et cela signifie que votre construction de production dépend d’un service tiers disponible et rapide au moment où vous déployez. Si l’API est lente, votre build est lent. Si elle est à terre, votre déploiement échoue. Un vendredi après-midi, c’est le problème de quelqu’un.

Un cache ou un export local. Rapide et robuste, mais quelqu’un doit le tenir à jour, et ce quelqu’un est un script que vous entretenez désormais.

Un instantané immuable sur un CDN. Le contenu d’une publication donnée est figé dans un fichier à l’URL stable, poussé sur un CDN. La construction va chercher ce fichier. Il ne peut pas changer sous vos pieds en cours de route, il est servi depuis un cache de bordure, et l’indisponibilité du CMS n’a aucune importance.

C’est ainsi que fonctionne Menestrel, et c’est un choix d’architecture assumé plutôt qu’un détail d’implémentation. La construction lit MENESTREL_CONTENT_URL, qui pointe sur l’instantané publié. Rien d’autre n’est nécessaire : aucun jeton dans votre environnement de déploiement, aucune dépendance à notre API. Nous le vérifions en coupant l’API et en confirmant que la construction d’un client aboutit quand même.

Écrire votre propre loader

Si vous avez une source de données que personne n’a intégrée, écrire un loader est un travail de deux heures, et le résultat est un citoyen de première classe dans Astro. La liste de contrôle :

  1. Récupérer et normaliser. Transformez votre source en une liste plate d’entrées avec des identifiants stables.
  2. Appeler parseData pour que les violations de schéma échouent bruyamment à la construction.
  3. Poser une empreinte si les entrées peuvent être nombreuses, pour que l’incrémental fonctionne.
  4. Vider avant un rafraîchissement complet, ou gérer explicitement les suppressions, sinon les entrées supprimées traînent dans le magasin d’une construction à l’autre. C’est le bug que tout le monde écrit une fois.
  5. Journaliser quelque chose. Nombre d’entrées chargées, URL source, durée.
  6. Échouer bruyamment. Un loader qui avale une erreur réseau et rend zéro entrée publiera un site vide, et le déploiement sera vert.

Ce dernier point mérite d’être souligné. Le pire comportement possible d’un loader est un résultat vide silencieux : votre construction passe, votre déploiement réussit, et le site de votre client n’a plus de page prestations. Préférez lever une erreur.

Deux pièges à connaître avant qu’ils ne mordent

Les entrées fantômes après une suppression. Le magasin persiste d’une construction à l’autre en développement. Si votre loader ajoute des entrées sans vider, et que quelque chose a été supprimé à la source, l’entrée supprimée continue d’exister localement et ne disparaît qu’en intégration continue, où le cache est froid. Ça produit le pire genre de bug : ça marche chez moi, c’est cassé en production, et il n’y a d’erreur nulle part.

La dérive de schéma entre environnements. Si le loader déduit le schéma d’une source distante, et que cette source change de forme pendant qu’un collègue est sur une branche plus ancienne, sa construction échoue avec une erreur de validation sur un champ dont il n’a jamais entendu parler. Le comportement est correct, et déroutant la première fois. La correction relève du processus : traitez un changement de modèle de contenu comme une migration de base, et livrez-le avant le code qui en dépend.

Ces deux pièges sont le prix d’un contenu distant. Ce ne sont pas des arguments contre le Content Layer, ce sont les choses à écrire dans le README de votre projet.

Ce que ça change au moment de choisir un CMS

Parce que le contrat est désormais petit et public, les différences intéressantes entre intégrations CMS pour Astro ne portent plus du tout sur l’intégration. Toutes les sérieuses sont des loaders, et toutes vous donnent des collections typées.

Ce qui diffère encore, et ce qu’il faut réellement évaluer :

Ce dernier point mérite qu’on s’y arrête. Le Content Layer a normalisé la sortie du contenu. Il n’a rien dit de la boucle qui commence quand une personne non technique clique sur un bouton et se termine quand sa page est réellement servie. Chaque CMS résout ça différemment, ou ne le résout pas.

Les limites, dites simplement

Le Content Layer est un mécanisme de construction. Il vous donne du contenu typé au build, rien à l’exécution. Si vous avez besoin de contenu qui change à la requête, ce n’est pas l’outil, et vous voulez du rendu serveur avec une récupération à la volée.

Il ne résout pas non plus la question de l’édition. Un loader fait sortir le contenu de quelque part ; comment il y est entré, et qui a le droit de l’y mettre, est un problème complètement séparé. C’est celui qu’un CMS résout, et les comparatifs détaillent comment les principaux s’y prennent.

Retour au blog