Coffee Beans

EmDash en production : notre retour d'expérience, du déploiement au serveur MCP

Retour au blog

Début octobre 2026, nous avons déplacé l'édition du blog de Coffee Beans, puis celle des réalisations, des services et des pages d'expertise, dans EmDash. Voici ce que fait ce CMS, comment nous l'avons branché sur un site existant sans le réécrire, ce qui a coincé en production, et pourquoi son serveur MCP change la façon de produire du contenu.

Cet article a été rédigé avec un agent IA, puis créé en brouillon dans notre CMS par ce même agent via le serveur MCP d'EmDash. Il a été relu avant publication. C'est la démonstration la plus directe de ce dont il parle.

EmDash en bref

EmDash est un CMS open source, sous licence MIT, présenté par Cloudflare comme le « successeur spirituel de WordPress ». Il est écrit en TypeScript et repose entièrement sur Astro : le CMS est une intégration Astro, l'administration est servie par le même processus que le site, et le contenu est lu avec les Live Collections d'Astro. Le paquet principal est publié par Matt Kane, qui cosigne l'annonce avec Matt Taylor.

Quatre choix le distinguent de WordPress :

  • Le contenu est structuré. Le texte riche est stocké en Portable Text, un format JSON, et non en HTML. Le même article peut être rendu en page web, en e-mail ou en réponse d'API sans analyser du HTML.
  • Les plugins sont isolés. Sur Cloudflare, chaque plugin tourne dans son propre bac à sable (un Dynamic Worker) et n'accède qu'aux capacités déclarées dans son manifeste. Un plugin qui déclare content:read et email:send ne peut rien faire d'autre.
  • Le CMS est pilotable par des agents. Chaque instance expose un serveur MCP, un CLI et des « agent skills » qui documentent ses API pour les assistants de code.
  • La migration est prévue. EmDash importe un export WordPress (WXR), l'API REST ou WordPress.com, et convertit les blocs Gutenberg en Portable Text.

Nous utilisons la version 1.1. Le dépôt GitHub décrit toujours le projet comme une « beta preview » : c'est un logiciel jeune, et nous l'avons traité comme tel.

Cloudflare ou Node.js : le choix qui conditionne le reste

EmDash tourne sur Cloudflare Workers ou sur n'importe quel serveur Node.js. La documentation propose cinq bases de données :

BaseQuand la choisirEnvironnement
SQLiteUn seul processus Node.js avec un disque persistantNode.js, développement local
D1Le site tourne sur Cloudflare WorkersCloudflare Workers
HyperdriveLe site tourne sur Workers mais doit utiliser un PostgreSQL existantCloudflare Workers
PostgreSQLPlusieurs processus Node.js partagent une même baseNode.js
libSQLUne base distante compatible SQLite pour Node.jsNode.js

Nous avons choisi Node.js et PostgreSQL. Le site de Coffee Beans lisait déjà ses contenus dans une base PostgreSQL hébergée en France, et nous avions un serveur qui fait tourner des conteneurs derrière un reverse proxy avec TLS automatique. Ajouter un conteneur coûtait moins cher que d'ouvrir une nouvelle plateforme.

Ce choix a deux conséquences qu'il faut connaître avant de s'engager :

  • La recherche plein texte n'existe que sur SQLite (elle repose sur FTS5). Sur PostgreSQL, elle n'est pas disponible à ce jour.
  • L'isolation des plugins n'est pas automatique hors de Cloudflare. Les plugins « sandboxés » ont besoin d'un exécuteur de bac à sable configuré, faute de quoi EmDash les ignore. Les plugins « natifs », eux, sont des paquets npm chargés dans le même processus que le site : ils ont accès à tout, comme un plugin WordPress. Nos deux plugins sont natifs. C'est un choix assumé pour du code que nous écrivons nous-mêmes. Pour installer des plugins tiers, nous configurerions un exécuteur de bac à sable.

Notre architecture : un CMS à côté du site, pas à sa place

La voie la plus simple aurait été de laisser EmDash servir tout le site. Nous ne l'avons pas prise. Le site public reste un site Astro statique, généré au déploiement et servi par un CDN. Ses performances, son référencement et ses URL ne bougent pas. EmDash devient l'outil d'édition et la source de vérité, sans migration d'un seul bloc.

Le flux d'une publication est le suivant :

  1. Un rédacteur (ou un agent) publie un contenu dans EmDash.
  2. Notre plugin de synchronisation écoute le hook content:afterPublish. Il convertit le Portable Text en HTML et écrit le contenu dans les tables que lit le site, avec un rôle PostgreSQL qui n'a le droit d'écrire que dans ces tables.
  3. Le plugin appelle le hook de build de l'hébergeur du site, qui régénère les pages.
  4. Au build, le site lit tout le contenu publié en un seul appel à un endpoint du CMS, protégé par un jeton porteur.

La dépublication suit le même chemin dans l'autre sens : le contenu repasse en brouillon dans la table et disparaît du site au build suivant.

Le point 4 a une histoire. Au départ, le build du site interrogeait directement PostgreSQL, ce qui obligeait à ouvrir la base à Internet. En faisant passer la lecture par le CMS, nous avons pu fermer la base à toute connexion extérieure : seuls le serveur du CMS et un poste d'administration y accèdent. En contrepartie, si le CMS est en panne, les builds échouent. Nous l'avons voulu ainsi : un build qui échoue laisse en ligne la dernière version du site, alors qu'un build sans contenu publierait un site vide.

La configuration qui compte

Voici, simplifiée, la configuration Astro de notre instance :

import node from "@astrojs/node";
import { defineConfig, sessionDrivers } from "astro/config";
import emdash, { local } from "emdash/astro";
import { postgres } from "emdash/db";

export default defineConfig({
  output: "server",
  adapter: node({ mode: "standalone" }),
  session: { driver: sessionDrivers.fsLite({ base: "/data/sessions" }) },
  integrations: [
    emdash({
      // Aucun identifiant ici : le pilote pg lit les variables PG* au démarrage.
      database: postgres({ ssl: { ca: rdsCa, rejectUnauthorized: true } }),
      storage: local({ directory: "/data/uploads", baseUrl: "/_emdash/api/media/file" }),
      plugins: [emailTransport(), siteSync()],
    }),
  ],
});

Trois lignes de ce fichier nous ont coûté du temps.

La configuration est figée au build. Tout ce qui est écrit dans astro.config.mjs est sérialisé dans le serveur compilé. Y mettre un mot de passe, c'est l'embarquer dans l'image Docker. Nous ne passons donc aucun identifiant à postgres() : le pilote pg lit les variables d'environnement standard de libpq au moment où le conteneur démarre. Seule l'autorité de certification de la base, qui est publique, est incluse, pour vérifier le certificat TLS au lieu de l'accepter aveuglément.

Le chemin des sessions doit être explicite. EmDash authentifie ses utilisateurs par passkey, avec un lien magique par e-mail en secours. Le défi WebAuthn est conservé dans la session Astro. Par défaut, l'adaptateur Node fige au build un chemin absolu de la machine qui compile, introuvable dans le conteneur : l'enregistrement des passkeys échouait sans message clair. Un pilote de session déclaré explicitement, sur un volume, règle le problème.

Les médias vont sur un volume. Le stockage local suffit pour un blog. Pour plusieurs instances, EmDash accepte un stockage compatible S3.

Enfin, l'image Docker ne compile rien. Le build Astro est fait sur la machine de déploiement, et l'image n'installe que les dépendances de production pour Linux. Le build complet dépassait la mémoire allouée à Docker sur un poste de développement.

Écrire un plugin

Un plugin natif est un module qui exporte une définition : un identifiant, les capacités qu'il demande et les hooks qu'il écoute. Le cœur de notre plugin de synchronisation tient en quelques lignes :

import { definePlugin } from "emdash";

export function createPlugin() {
  return definePlugin({
    id: "site-sync",
    version: "0.4.0",
    capabilities: ["content:read"],
    hooks: {
      "content:afterPublish": async ({ collection, content }, ctx) => {
        const handler = handlers[collection];
        if (!handler) return;
        await handler.publish(content, content.data ?? {});
        await triggerBuild(ctx);
      },
    },
  });
}

Chaque collection a son propre gestionnaire : articles, réalisations, services, pages d'expertise et réglages du site. Le deuxième plugin branche notre fournisseur d'e-mails transactionnels sur le hook email:deliver, ce qui permet à EmDash d'envoyer les liens magiques, les invitations et les récupérations de compte.

Un détail de comportement compte pour les rédacteurs : enregistrer un contenu déjà publié ne modifie que son brouillon. Seul le bouton « Publier les modifications » déclenche le hook, donc la synchronisation et le build. C'est le comportement attendu d'un CMS éditorial, mais il surprend quand on vient d'un outil où chaque enregistrement part en ligne.

Le serveur MCP : le CMS devient pilotable par un agent

C'est la partie qui a le plus changé notre façon de travailler. Chaque instance EmDash expose un serveur Model Context Protocol à l'adresse /_emdash/api/mcp. Un client comme Claude ou ChatGPT s'y connecte par OAuth, avec le compte et le rôle d'un utilisateur du CMS. Il peut alors lire et modifier les contenus, les schémas, les médias, les taxonomies, les menus, les révisions et les réglages, ou exporter le site entier.

L'accès se règle par des scopes, vérifiés en plus du rôle de l'utilisateur : un scope n'accorde jamais une permission que l'utilisateur n'a pas.

ScopeCe qu'il permet
content:readLire et rechercher les contenus, taxonomies, menus et révisions
content:writeCréer et modifier des contenus et des révisions
schema:writeCréer, modifier et supprimer des collections et des champs
media:writeTéléverser et gérer des médias
settings:manageModifier les réglages du site
adminAppeler tous les outils

Ce que nous en avons fait

Nous n'avons pas seulement utilisé le MCP pour écrire des articles. Les collections « Réalisations », « Services », « Pages d'expertise » et « Réglages du site » ont été créées avec lui, en production, sans toucher au code du CMS. Les pages d'expertise reposent sur dix types de blocs (en-tête, grille d'enjeux, étapes, offres, FAQ, appel à l'action, etc.), eux aussi définis par l'agent. Ajouter une page d'expertise consiste désormais à créer un contenu, tant que les blocs existants suffisent.

Pour un article, le travail se répartit ainsi : l'agent lit le schéma de la collection, rédige le texte en Markdown, le crée en brouillon avec tous les champs (titre, chapô, titre et description SEO, temps de lecture), puis relit ce qui a été enregistré. Le Markdown est converti en Portable Text par le serveur. Un humain relit dans l'administration et clique sur « Publier ».

Les garde-fous intégrés

Plusieurs mécanismes rendent ce fonctionnement sûr, et ils sont prévus par l'outil, pas ajoutés par-dessus :

  • Brouillon par défaut. Un contenu créé par MCP n'est pas en ligne tant que personne ne le publie.
  • Contrôle de concurrence. Toute modification exige le jeton de révision obtenu à la lecture. Si quelqu'un a modifié le contenu entre-temps, l'écriture échoue au lieu d'écraser son travail.
  • Verrouillage d'édition. Si un rédacteur a le contenu ouvert dans l'administration, l'agent est bloqué et le message dit qui édite.
  • Révisions. Chaque version est conservée et peut être restaurée.

Notre règle : l'agent écrit, un humain publie. Pour la rédaction courante, un jeton limité à content:read et content:write suffit. schema:write reste réservé aux sessions où l'on fait évoluer le modèle de contenu.

Les limites rencontrées

  • L'outil de création de champ ne propose pas le type « répéteur ». Les types de blocs, eux, l'acceptent : nous avons modélisé les listes répétées sous forme de blocs.
  • Les collections créées par MCP existent en base, pas dans le fichier de seed du projet. Un nouvel environnement ne les recrée pas tout seul : il faut exporter le site ou maintenir le seed à la main.
  • La suppression d'un contenu n'est pas synchronisée vers le site. Il faut d'abord le dépublier.

Ce qu'il faut retenir avant de se lancer

  • EmDash est jeune. Il est utilisable en production, mais il faut lire le code quand la documentation ne répond pas, et suivre les versions de près.
  • Le choix de l'hébergement n'est pas neutre. Sur Cloudflare, on obtient l'isolation des plugins sans effort. Sur Node.js, on garde la main sur l'infrastructure et la localisation des données, mais l'isolation et la recherche plein texte demandent du travail ou des compromis.
  • On n'est pas obligé de tout migrer. Brancher EmDash à côté d'un site existant, avec un hook de publication et un hook de build, donne l'édition et l'agent sans réécrire le site.
  • Le MCP est la vraie nouveauté. Il fait du modèle de contenu et des contenus eux-mêmes quelque chose qu'un agent peut manipuler, avec les garde-fous d'un CMS éditorial.

EmDash convient bien aux équipes qui ont déjà un site Astro, ou qui veulent mettre des agents dans leur chaîne éditoriale sans leur donner les clés de la production. Pour un site qui dépend d'un large catalogue de plugins WordPress, il est encore tôt.

Vous envisagez de quitter WordPress ou de brancher un CMS sur votre site existant ? Parlons-en.