Pendant des années, nos landing pages se ressemblaient. Pas par manque d’idées : l’équipe eShop en avait plein. Mais un CMS à « slices » ne propose qu’un nombre fini de briques, et quand chaque page est un empilement des mêmes briques, on finit par renoncer aux pages qui sortent du cadre. Une forme d’autocensure créative, que personne n’avait décidée.

C’est le vrai point de départ de Kosmos, le CMS que nous avons construit cet été pour remplacer Prismic. Pas une question de licence ou de facture, même si ça compte : une question de liberté éditoriale, sur neuf storefronts, avec deux équipes qui publient de part et d’autre de l’Atlantique.

Fin septembre, le moteur de rendu de Kosmos est entré dans le site. Le 6 octobre, ses lots de modules et l’aperçu des brouillons l’ont rejoint en production, interrupteur fermé : aucune page Kosmos n’est encore servie aux clients. Voici ce qu’on a construit, et surtout les décisions qui le tiennent debout.

Le problème : neuf stores, deux équipes, un circuit éditorial

Zadig&Voltaire publie sur neuf storefronts : la France, la Belgique, la Suisse, l’Allemagne, l’Espagne, l’Italie, le Royaume-Uni, l’international et les États-Unis. Cinq d’entre eux sont anglophones, et pourtant commercialement distincts : une opération peut tourner au Royaume-Uni et pas en Suisse. L’équipe de Paris alimente huit stores, celle de New York le store américain.

Le circuit, lui, ne change pas : un brief de la communication, une création par l’eShop, des retours, une validation, une publication. Un bon outil épouse ce circuit au lieu d’imposer le sien.

Nous avons donc posé une règle simple : le contenu s’organise par store, pas par langue. L’unité de publication est le couple (store, contenu). Une page peut n’exister que sur certains marchés, et deux pages en anglais peuvent légitimement diverger.

Construire plutôt qu’acheter, cette fois

La première hypothèse était d’acheter la plomberie : Payload ou Strapi, qui offrent utilisateurs, brouillons, versions et médiathèque. En posant nos choix un par un (organisation par store, héritage et dérivation des contenus entre marchés, releases atomiques, éditeur découplé), on a vu ce qu’il en resterait : un workflow brouillon/publié et une médiathèque. Trop peu pour subir un framework étranger, ses montées de version et la contorsion de notre modèle dans le sien.

Ce qui a fait basculer la décision, c’est aussi notre façon de travailler. Le développement assisté par Claude Code déplace le curseur entre construire et acheter. Le critère n’est plus « combien ça coûte à écrire », mais « est-ce que l’outil colle à notre modèle ». Le dépôt de Kosmos a été ouvert le 10 août 2026 ; début octobre, il comptait plus de 1 500 commits.

Le livrable est volontairement sobre : une image Docker autosuffisante, configurée par variables d’environnement, à la manière de Meilisearch. Une API Fastify en TypeScript, PostgreSQL 18, une application Vue 3, une seule instance. Kosmos n’importe aucun code du reste de la plateforme : toutes ses relations passent par des contrats.

Une page est un document

Le principe directeur tient en une ligne : un contenu, c’est un chrome (fond, thème) et une liste ordonnée de blocs. Un document JSON, indépendant de tout framework, validé par le serveur à chaque écriture. Un document invalide est rejeté, jamais stocké.

Voici, raccourci, le document d’une des pages pilotes :

{
  "chrome": { "theme": "dark", "background": { "asset_id": "01a0aa7c-…" } },
  "blocks": [
    { "kind": "free_block", "block_id": "01M2NEM2J6…JG",
      "title": "Matière, manifeste", "icon": "sparkles",
      "ast": { "…": "…" }, "css": "&{color:var(--color-inverse)}…", "js": "…" },
    { "kind": "module", "block_id": "01M2NEM2J6…JH",
      "module": { "identity": "Products-row", "version": 1 },
      "fields": { "products": [{ "product": { "product_ref": "KWSW03037036" } }] } },
    { "kind": "module", "block_id": "01M2NEM2J6…JJ",
      "module": { "identity": "Actions", "version": 1 }, "variant": "row",
      "fields": { "actions": [{ "label": { "value": "Pulls et cardigans" } }] } }
  ]
}

Trois choses à remarquer. Chaque bloc porte un block_id immuable : réordonner une page ne casse ni les traductions ni l’audit, qui désignent le bloc, jamais sa position. Un produit est une référence, jamais un prix ni un stock : tout ce qui bouge se résout au rendu. Et il existe deux natures de blocs, à égalité.

Des modules issus du site, versionnés, jamais modifiés en place

Les modules sont des compositions des composants atomiques du site (typographies, boutons, grilles, briques produit). Ils sont écrits par les développeurs, portent le design system et leur comportement par thème et par viewport. Chacun déclare deux choses obligatoires : une fixture qui lui permet de se rendre seul, hors de toute page, et une icône qui décrit sa fonction plutôt que son apparence, pour qu’une page composée reste lisible d’un coup d’œil dans l’éditeur.

Un changement cassant ne modifie jamais un module : il crée Banner@v2, même identité, version incrémentée. Chaque document épingle la version qu’il utilise, et le moteur de rendu sait rendre les anciennes versions tant qu’un document dans la fenêtre de garantie de rendu (treize mois) les référence. Migrer des pages vers une nouvelle version est une opération délibérée, par lots, jamais l’effet de bord d’une édition.

La bibliothèque est produite par le build du site lui-même. La CI publie un paquet versionné : composants compilés en ESM, CSS, tokens du design system, icônes et fixtures. L’éditeur charge ce paquet et manipule les vrais composants. Il n’existe pas de maquette parallèle qui finirait par diverger du site : l’aperçu dans Kosmos, c’est le rendu réel.

Le bloc libre, pour sortir du cadre

Le bloc libre est la réponse directe à l’autocensure du début : du HTML, du CSS et du JS écrits sur la base du design system. Il n’est pas une soupape, c’est un matériau de composition à part entière, et il peut envelopper des modules.

Libre ne veut pas dire non gouverné. Un bloc libre est stocké comme un arbre, jamais comme une chaîne HTML : la normalisation à l’écriture produit un AST, filtré par une liste blanche de balises et d’attributs. Ses textes sont extraits en slots identifiés, donc traduisibles et auditables comme ceux d’un module. Ses valeurs passent par les tokens, ce qui préserve les contrastes validés. Et côté site, il est rendu par les fonctions de rendu de Vue, dans un seul arbre d’hydratation, sans v-html.

Page éditoriale « La maille selon Zadig&Voltaire » rendue par le moteur de rendu Kosmos : un grand titre, un texte d'introduction et un portrait, puis un bandeau de mots défilants au-dessus d'un gros plan de maille
Le haut d'une page éditoriale composée dans Kosmos, rendue en préproduction : uniquement des blocs libres, sur le design system du site.

Publier : une époque, pas une mise à jour

C’est la partie dont nous sommes le plus fiers, parce que c’est celle qui casse le plus souvent ailleurs.

Une opération commerciale, ce sont des dizaines de contenus (homepage, bandeau, landing pages) qui doivent basculer ensemble, à une heure précise, sur certains stores seulement. Si la moitié part et l’autre non, le site affiche des soldes à moitié commencés.

Kosmos ne sert jamais le site. Sa base PostgreSQL est l’outil des éditeurs ; la lecture passe par Elasticsearch et Helios, la couche qui sert déjà le catalogue. Publier se fait en deux temps :

  1. Les nouvelles versions sont pré-écrites dans les index de contenu, invisibles, porteuses d’un numéro d’époque.
  2. La bascule écrit un pointeur d’époque par store, et une release multi-store écrit ses N pointeurs dans une seule transaction. Tout part, ou rien ne part.
Chemin du contenu en deux bandes. En haut, l'édition : l'éditeur Kosmos écrit dans l'API, qui pré-écrit les versions dans les index de contenu puis bascule les pointeurs d'époque. En bas, la diffusion : Helios lit index et époques, le moteur de rendu produit la page pour le visiteur ; le build Nuxt du site produit le moteur de rendu et le paquet de manifeste chargé par l'éditeur. Édition (outil interne) Site Build (CI) Éditeur Kosmos Vue · vrais composants API Kosmos PostgreSQL · MCP Index de contenu versions pré-écrites Époques cms_epoch Helios cache clé = époque Moteur de rendu SSR · polaris Visiteur via CloudFront Manifeste paquet versionné Build Nuxt CI polaris 1 2
1 : la publication pré-écrit les versions. 2 : la bascule écrit un pointeur d'époque par store. Le build Nuxt du site compile les modules pour le moteur de rendu et publie le même paquet de manifeste sur S3 et CloudFront, que l'éditeur charge et dont l'API ingère les métadonnées. Le site ne lit jamais la base d'édition.

Le rollback suit la même logique, et c’est un point qu’on a dû corriger en cours de conception. Revenir en arrière en repointant l’ancienne époque laissait dans les index les documents de l’époque annulée, qu’une release programmée suivante pouvait réélire. Un rollback est donc une nouvelle publication vers l’avant : une époque neuve qui porte l’état d’avant. Même logique pour la dépublication, qui écrit une pierre tombale au lieu de supprimer, pour ne jamais ressusciter une version plus ancienne.

Trois stores dans le temps. La release des soldes fait passer France et Allemagne de l'époque 11 à 12 dans une seule transaction, l'Italie reste en 11. L'annulation ne revient pas en arrière : elle publie une époque 13 qui porte l'état d'avant. Bascule N pointeurs, 1 transaction Annulation re-publication vers l'avant fr_fr époque 11 époque 12 · soldes époque 13 = état de 11 de_de époque 11 époque 12 · soldes époque 13 = état de 11 it_it époque 11, hors release
Une release bascule plusieurs stores d'un coup ; un rollback est une nouvelle époque, jamais un retour de pointeur.

Côté Helios, l’époque fait partie de chaque clé de cache. Une bascule change d’un coup l’ensemble des clés lues pour un store, et rien n’est jamais purgé : les anciennes clés cessent simplement d’être demandées.

// The epoch is in every key, so a switch changes the whole read set of a store in one write and
// nothing is ever purged: old keys simply stop being asked for.
export const documentCacheKey = (
  output: ContentOutput,
  key: string,
  storeCode: string,
  epoch: number,
): string => `cms:${output}:${key}:${storeCode}:${epoch}`;

Le pointeur d’époque lui-même n’est gardé en cache que deux secondes : c’est la seule clé dont le sens change à une bascule. Et un cache qui tombe en panne n’est qu’un cache vide, Elasticsearch répond à sa place.

Conséquence directe de cette architecture : Kosmos peut tomber, le site continue. Les éditeurs perdent leur outil le temps de l’incident, les clients ne voient rien.

Une seule API, et MCP comme adaptateur

Kosmos est API-first. L’application des éditeurs n’a aucun privilège qui ne passe par l’API, et le serveur MCP de Kosmos n’est pas une API parallèle : c’est un adaptateur au-dessus d’elle. Même autorisation, même validation, même audit, seul le protocole change.

C’est ce qui permet la composition assistée par IA sans compromis : le modèle reçoit le vocabulaire complet (index des modules, design system, contraintes du store) et compose avec les modules qui existent, il n’en invente pas. Ce qu’il produit est un document comme un autre, validé par le même schéma épinglé que les écritures humaines. Une demande hors bibliothèque est journalisée, et ce journal nous dit si la bibliothèque bride vraiment les éditeurs.

Coexister avec Prismic, page par page

On ne coupe pas un CMS un vendredi soir. La livraison se fait par coexistence : sur les routes de contenu, le site demande d’abord la page à Helios. Si Kosmos la publie, il la rend ; sinon, Prismic prend le relais. Une seule source d’écriture par page, et chaque bascule est réversible.

La lecture est bornée : trois secondes de budget au rendu serveur, après quoi la page est servie par son autre source. Une passerelle qui répond 5xx, 429 ou rien du tout dégrade en « indisponible », jamais en page cassée, et chaque repli est journalisé : un repli que personne ne voit en production est un repli dont personne ne sait qu’il tourne.

L’ordre de bascule est fixé : pages de contenu, puis FAQ, puis blocs transverses, puis homepage. Trois pages Prismic ont été recomposées dans Kosmos et comparées en préproduction. En production, le moteur de rendu est déployé et l’interrupteur reste fermé : les premières pages vont passer une par une, chacune réversible.

Un chantier voisin a déjà franchi le pas. Le menu de navigation ne vit plus dans Prismic : depuis septembre, sur les neuf stores, il est piloté depuis le back-office Magento, avec programmation datée, aperçu et glisser-déposer. La navigation est de la configuration commerciale, elle avait sa place à côté du catalogue plutôt que dans un CMS éditorial.

Ce qu’on a appris

  • Construire est redevenu une option sérieuse. Avec un développement assisté par IA, un outil taillé pour notre modèle coûte moins cher qu’un framework qu’on contorsionne. À condition de documenter chaque décision : Kosmos a un registre d’ADR depuis son premier jour.
  • Ne jamais muter en place. Ni un module vivant, ni un document, ni une traduction publiée : on crée une version. Rollback, audit et diff deviennent triviaux.
  • Séparer l’édition de la diffusion. Le site ne lit jamais la base des éditeurs. C’est ce qui rend l’outil interne sacrifiable sans risque pour le chiffre d’affaires.
  • Penser la publication comme un problème distribué. L’atomicité d’une release multi-store ne s’ajoute pas après coup ; elle se conçoit et se teste dès la première tranche verticale.
  • Livrer par coexistence. Basculer page par page dérisque l’adoption en continu au lieu de tout concentrer sur un grand soir.

La prochaine étape se joue côté éditeurs : sortir les premières pages de Prismic en production, puis ouvrir la composition par prompt à l’équipe eShop. L’objectif reste celui du départ : que plus personne ne renonce à une page parce que l’outil ne sait pas la faire.