For years, our landing pages looked alike. Not for lack of ideas: the eShop team had plenty. But a slice-based CMS offers a finite set of building blocks, and when every page is a stack of the same blocks, you end up giving up on the pages that don’t fit. A form of creative self-censorship that nobody had ever decided on.
That is the real starting point of Kosmos, the CMS we built this summer to replace Prismic. Not a question of licence or invoice, even if that counts: a question of editorial freedom, across nine storefronts, with two teams publishing on either side of the Atlantic.
In late September, the Kosmos rendering engine went into the site. On 6 October, its module batches and draft preview joined it in production, with the switch closed: no Kosmos page is served to customers yet. Here is what we built, and above all the decisions that hold it together.
The problem: nine stores, two teams, one editorial process
Zadig&Voltaire publishes on nine storefronts: France, Belgium, Switzerland, Germany, Spain, Italy, the United Kingdom, international and the United States. Five of them are in English, yet commercially distinct: a campaign can run in the UK and not in Switzerland. The Paris team feeds eight stores, the New York team the US store.
The process itself doesn’t change: a brief from communications, a page built by the eShop team, feedback, sign-off, publication. A good tool fits that process instead of imposing its own.
So we set a simple rule: content is organised by store, not by language. The unit of publication is the (store, content) pair. A page may exist only on some markets, and two English pages can legitimately diverge.
Building rather than buying, this time
The first idea was to buy the plumbing: Payload or Strapi, which come with users, drafts, versions and a media library. As we made our choices one by one (organisation by store, inheritance and derivation of content between markets, atomic releases, a decoupled editor), we saw what would be left: a draft/publish workflow and a media library. Too little to justify living with someone else’s framework, its upgrades and the contortion of our model into theirs.
What tipped the decision was also the way we work. AI-assisted development with Claude Code moves the line between build and buy. The question is no longer “how much will it cost to write” but “does the tool fit our model”. The Kosmos repository was opened on 10 August 2026; by early October it had more than 1,500 commits.
The deliverable is deliberately plain: a self-sufficient Docker image, configured through environment variables, in the spirit of Meilisearch. A Fastify API in TypeScript, PostgreSQL 18, a Vue 3 application, a single instance. Kosmos imports no code from the rest of the platform: every relationship goes through a contract.
A page is a document
The founding principle fits on one line: a piece of content is a chrome (background, theme) plus an ordered list of blocks. A JSON document, independent of any framework, validated by the server on every write. An invalid document is rejected, never stored.
Here is the document of one of the pilot pages, trimmed:
{
"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" } }] } }
]
}
Three things to notice. Each block carries an immutable block_id: reordering a page breaks neither translations nor audit, which point at the block, never at its position. A product is a reference, never a price or a stock level: anything that moves is resolved at render time. And there are two kinds of blocks, on equal footing.
Modules built from the site, versioned, never changed in place
Modules are compositions of the site’s atomic components (typography, buttons, grids, product bricks). They are written by developers and carry the design system along with their behaviour per theme and per viewport. Each one must declare two things: a fixture that lets it render on its own, outside any page, and an icon describing its function rather than its look, so that a composed page stays readable at a glance in the editor.
A breaking change never modifies a module: it creates Banner@v2, same identity, incremented version. Every document pins the version it uses, and the rendering engine can render older versions as long as a document within the rendering guarantee window (thirteen months) refers to them. Migrating pages to a new version is a deliberate, batched operation, never the side effect of an edit.
The library is produced by the site’s own build. CI publishes a versioned package: components compiled to ESM, CSS, design system tokens, icons and fixtures. The editor loads that package and handles the real components. There is no parallel mock-up that would eventually drift from the site: the preview in Kosmos is the actual render.
The free block, to step outside the frame
The free block is the direct answer to the self-censorship we started with: HTML, CSS and JS written on top of the design system. It is not an escape hatch but a first-class composition material, and it can wrap modules.
Free doesn’t mean ungoverned. A free block is stored as a tree, never as an HTML string: normalisation on write produces an AST, filtered through an allowlist of tags and attributes. Its texts are extracted into identified slots, so they can be translated and audited like a module’s. Its values go through the tokens, which preserves the validated contrast ratios. And on the site, it is rendered through Vue render functions, inside a single hydration tree, without v-html.
Publishing: an epoch, not an update
This is the part we are proudest of, because it is the one that breaks most often elsewhere.
A commercial campaign means dozens of pieces of content (homepage, banner, landing pages) that must switch together, at a precise time, on some stores only. If half goes live and the other half doesn’t, the site shows a sale that has half started.
Kosmos never serves the site. Its PostgreSQL database is the editors’ tool; reads go through Elasticsearch and Helios, the layer that already serves the catalogue. Publishing happens in two steps:
- New versions are pre-written into the content indices, invisible, carrying an epoch number.
- The switch writes one epoch pointer per store, and a multi-store release writes its N pointers in a single transaction. Everything goes, or nothing does.
Rollback follows the same logic, and it is something we had to correct during design. Rolling back by pointing at the previous epoch left the documents of the cancelled epoch in the indices, where a later scheduled release could elect them again. A rollback is therefore a forward re-publication: a new epoch carrying the previous state. Same idea for unpublishing, which writes a tombstone instead of deleting, so that an older version can never come back to life.
On the Helios side, the epoch is part of every cache key. A switch changes the whole set of keys read for a store at once, and nothing is ever purged: old keys simply stop being requested.
// 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}`;
The epoch pointer itself is only cached for two seconds: it is the one key whose meaning changes at a switch. And a cache that fails is just an empty cache: Elasticsearch answers instead.
A direct consequence of this architecture: Kosmos can go down and the site carries on. Editors lose their tool for the duration of the incident; customers see nothing.
One API, with MCP as an adapter
Kosmos is API-first. The editors’ application has no privilege that bypasses the API, and the Kosmos MCP server is not a parallel API: it is an adapter on top of it. Same authorisation, same validation, same audit; only the protocol differs.
That is what makes AI-assisted composition possible without compromise: the model receives the full vocabulary (module index, design system, store constraints) and composes with the modules that exist, it doesn’t invent new ones. What it produces is a document like any other, validated by the same pinned schema as human writes. A request outside the library is logged, and that log tells us whether the library is really holding editors back.
Coexisting with Prismic, page by page
You don’t switch off a CMS on a Friday night. Delivery happens by coexistence: on content routes, the site first asks Helios for the page. If Kosmos publishes it, the site renders it; otherwise Prismic takes over. One write source per page, and every switch is reversible.
The read is bounded: a three-second budget during server rendering, after which the page is served by its other source. A gateway that answers 5xx, 429 or nothing at all degrades to “unavailable”, never to a broken page, and every fallback is logged: a fallback nobody sees in production is a fallback nobody knows is running.
The switch order is set: content pages, then FAQ, then site-wide blocks, then the homepage. Three Prismic pages have been rebuilt in Kosmos and compared in pre-production. In production, the rendering engine is deployed and the switch stays closed: the first pages will move over one by one, each one reversible.
A neighbouring project has already made the jump. The navigation menu no longer lives in Prismic: since September, on all nine stores, it is managed from the Magento back office, with scheduled publication, preview and drag-and-drop. Navigation is commercial configuration; it belonged next to the catalog rather than in an editorial CMS.
What we learned
- Building is a serious option again. With AI-assisted development, a tool cut for our model costs less than a framework we have to bend. Provided every decision is documented: Kosmos has had an ADR log since day one.
- Never mutate in place. Not a live module, not a document, not a published translation: you create a version. Rollback, audit and diff become trivial.
- Separate editing from delivery. The site never reads the editors’ database. That is what makes the internal tool expendable without any risk to revenue.
- Treat publishing as a distributed problem. The atomicity of a multi-store release can’t be bolted on later; it is designed in and tested from the first vertical slice.
- Ship by coexistence. Switching page by page de-risks adoption continuously instead of betting everything on one big night.
The next step belongs to the editors: moving the first pages out of Prismic in production, then opening prompt-based composition to the eShop team. The goal is the one we started with: that nobody ever gives up on a page again because the tool can’t build it.