Hero slot3 en

Frontend headless pour tout CMS

Laioutr exploite des frontends sur plus de 50 stacks backend différentes. Sept des systèmes de CMS headless les plus utilisés dans le contexte enterprise DACH en font partie. Les patterns qui fonctionnent ne sont pas spécifiques à un CMS - et les anti-patterns qui posent problème se répètent d'un système à l'autre.

Cet article est une documentation directe issue de ces intégrations. Ce n'est pas un "comparatif des 7 meilleurs CMS" - tout le monde sait lire une grille tarifaire. Ce qui est documenté ici : quelles décisions de couche frontend sont les bonnes selon le CMS, et où les équipes reconstruisent systématiquement le même problème.

Le pattern transversal : l'abstraction par handler d'abord

Avant de passer aux patterns propres à chaque CMS, voici le principe transversal qui se vérifie sur l'ensemble des 50+ intégrations :

La couche frontend ne doit pas appeler un SDK de CMS directement. Elle doit appeler un content handler abstrait qui encapsule le SDK du CMS. C'est toute la différence entre un frontend marié en profondeur à un CMS et un frontend agnostique du CMS.

// Anti-pattern : SDK directement dans le code de la page
import { createClient } from 'contentful'
// ou
import { createClient } from '@sanity/client'
// ou
import { GraphQLClient } from 'graphql-request' // hygraph

// Les trois sont des anti-patterns au niveau de la page

// Pattern : interface abstraite au niveau de la plateforme
interface CMSHandler {
  fetchPage(slug: string, locale: string): Promise<PageData>
  fetchCollection(type: string, locale: string, params?: QueryParams): Promise<CollectionData>
  fetchAsset(id: string): Promise<AssetData>
  getPreviewData(slug: string): Promise<PreviewData | null>
}

Avec cette interface comme fondation, voici les sept systèmes de CMS.

Contentful : patterns et anti-patterns

Ce que la couche frontend absorbe :

La Content Delivery API de Contentful sait parler GraphQL (avec un CDN en frontal), mais elle livre le contenu dans un format propre à Contentful : wrappers `sys`, wrappers `fields`, références d'assets embarquées avec leur propre test `sys.type = 'Asset'`. Cette surcharge de format n'a rien à faire dans le code des composants.

// Handler Contentful : normalise le format Contentful
class ContentfulHandler implements CMSHandler {
  async fetchPage(slug: string, locale: string): Promise<PageData> {
    const response = await this.client.getEntries({
      content_type: 'page',
      'fields.slug': slug,
      locale: locale === 'de' ? 'de' : 'en-US', // mapping des locales Contentful !
      include: 3
    })

    if (!response.items.length) throw new ContentNotFoundError(slug)

    const entry = response.items[0]
    return {
      id: entry.sys.id,
      title: entry.fields.title as string,
      slug: entry.fields.slug as string,
      blocks: this.normaliseBlocks(entry.fields.blocks),
      meta: this.normaliseMeta(entry.fields.seo)
    }
  }

  private normaliseBlocks(rawBlocks: unknown[]): Block[] {
    return rawBlocks.map(block => {
      const b = block as ContentfulEntry
      if (b.sys?.type === 'Asset') return this.normaliseAsset(b)
      return this.normaliseEntry(b)
    })
  }
}

L'anti-pattern Contentful classique :

Rendre le rich text directement dans les composants, sans normaliseur. Le rich text de Contentful est un AST JSON (proche de Slate.js) mais avec des types de nœuds propres à Contentful pour les entrées embarquées. Appeler `documentToReactComponents(richText)` directement dans un composant inscrit une dépendance dure à Contentful dans la couche UI.

Le correctif : un renderer de rich text maison au niveau de la plateforme, qui convertit l'AST Contentful en AST de composants normalisé.

Storyblok : patterns et anti-patterns

Ce que la couche frontend absorbe :

Storyblok travaille avec des blocks imbriqués (components) plutôt qu'avec des content types. C'est conceptuellement différent de Contentful : Storyblok pense en "stories" composées de "blocks". Chaque block porte une clé `component` qui indique quel composant UI rendre.

Le pattern Storyblok qui fonctionne :

// Handler Storyblok
class StoryblokHandler implements CMSHandler {
  async fetchPage(slug: string, locale: string): Promise<PageData> {
    const { data } = await this.storyblokApi.get(`cdn/stories/${slug}`, {
      version: 'published',
      language: locale,
      resolve_relations: 'global_reference.reference'
    })

    return {
      id: data.story.uuid,
      title: data.story.content.title,
      slug: data.story.slug,
      blocks: this.mapStoryblokBlocks(data.story.content.body),
      meta: data.story.content.seo_metadata
    }
  }

  private mapStoryblokBlocks(blocks: StoryblokBlock[]): Block[] {
    return blocks.map(block => ({
      type: block.component, // Storyblok fournit directement le nom du composant
      data: this.normaliseBlockData(block),
      id: block._uid
    }))
  }
}

L'anti-pattern Storyblok classique :

Intégrer la directive `storyblokEditable` directement dans les composants UI finaux. `storyblokEditable(blok)` ajoute des attributs `data-` pour l'overlay du Visual Editor de Storyblok. Cela couple les composants UI à l'infrastructure d'édition de Storyblok. Quand l'éditeur passe sur une autre plateforme (ou qu'il est désactivé), des directives mortes subsistent dans la sortie de rendu.

Le correctif : détecter le mode édition au niveau de la plateforme, pas au niveau des composants. Quand `isEditorMode()` est vrai, les directives sont injectées par le handler - les composants, eux, restent propres.

Hygraph : patterns et anti-patterns

Ce que la couche frontend absorbe :

Hygraph est GraphQL-native. C'est un avantage - mais cela pousse les équipes à écrire des requêtes GraphQL directement dans les composants. Pour une architecture agnostique du CMS, c'est exactement le même anti-pattern que pour n'importe quel autre SDK.

La force de Hygraph, c'est sa couche GraphQL capable de fédération : un seul endpoint GraphQL peut agréger plusieurs sources backend. Pour des frontends à sources de données multiples (CMS + commerce + recherche), c'est un avantage réel - mais seulement si la couche frontend est proprement abstraite du schéma GraphQL.

// Handler Hygraph
class HygraphHandler implements CMSHandler {
  async fetchPage(slug: string, locale: string): Promise<PageData> {
    const { page } = await this.graphqlClient.request<HygraphPageResponse>(
      HYGRAPH_PAGE_QUERY,
      { slug, locale: locale === 'de' ? 'de' : 'en' }
    )

    if (!page) throw new ContentNotFoundError(slug)

    return {
      id: page.id,
      title: page.title,
      slug: page.slug,
      blocks: page.blocks.map(b => this.normaliseHygraphBlock(b)),
      meta: { title: page.seoTitle, description: page.seoDescription }
    }
  }
}

// La requête reste dans le handler, pas dans le composant
const HYGRAPH_PAGE_QUERY = gql`
  query GetPage($slug: String!, $locale: Locale!) {
    page(where: { slug: $slug }, locales: [$locale, en]) {
      id title slug seoTitle seoDescription
      blocks { ... on HeroBlock { __typename headline ... } }
    }
  }
`

L'anti-pattern Hygraph :

Mal configurer le fallback de locale de Hygraph. Hygraph dispose d'un système explicite de fallback de locale : si une entrée n'existe pas dans la locale demandée, elle peut retomber sur une locale de repli. Ne pas le configurer produit des champs vides au lieu du contenu de repli. Cela arrive surtout avec les entrées historiques dont la localisation est incomplète.

Le correctif : renseigner `locales: [$primaryLocale, en]` dans la requête (Hygraph accepte des tableaux de locales comme liste de priorité).

Sanity : patterns et anti-patterns

Ce que la couche frontend absorbe :

Sanity a GROQ - son propre langage de requête, plus puissant que GraphQL mais avec une courbe d'apprentissage plus raide. Pour des développeurs frontend sans expérience de GROQ, la tentation est d'écrire les requêtes GROQ directement dans les server components Next.js. C'est l'équivalent Sanity de l'anti-pattern du SDK Contentful.

// Handler Sanity
class SanityHandler implements CMSHandler {
  async fetchPage(slug: string, locale: string): Promise<PageData> {
    // GROQ reste dans le handler, pas dans le composant
    const query = groq`
      *[_type == "page" && slug.current == $slug && language == $locale][0] {
        _id,
        title,
        "slug": slug.current,
        blocks[] {
          _type,
          _key,
          ...
        },
        seo { title, description }
      }
    `
    const page = await this.sanityClient.fetch<SanityPage>(query, { slug, locale })

    if (!page) throw new ContentNotFoundError(slug)

    return this.normaliseSanityPage(page)
  }
}

L'anti-pattern Sanity :

Rendre le Portable Text de Sanity directement dans les composants, sans couche de rendu normalisée. Le Portable Text de Sanity est un format de contenu par blocs, avec des définitions de marks sur mesure pour les liens inline, des annotations personnalisées et des objets embarqués. Utiliser `<PortableText value={portableText} />` directement dans un composant rend ce composant spécifique à Sanity.

Le correctif : un renderer Portable Text maison au niveau de la plateforme, avec des types de blocs normalisés, qui convertit l'entrée propre à Sanity en sortie agnostique du CMS.

Strapi : patterns et anti-patterns

Ce que la couche frontend absorbe :

Strapi propose une API REST (principale) et une API GraphQL (optionnelle, via plugin). Pour des setups enterprise, l'API GraphQL est préférable - mais les installations Strapi varient beaucoup selon la version (v4 ou v5) et la configuration des plugins. La couche handler doit encapsuler cette variance.

Strapi v5 a introduit une nouvelle Document Service API avec `documentId` à la place de `id`. Appeler l'API directement depuis le frontend, sans couche handler, revient à encaisser des breaking changes à chaque upgrade de Strapi.

// Handler Strapi (v5)
class StrapiHandler implements CMSHandler {
  async fetchPage(slug: string, locale: string): Promise<PageData> {
    const response = await fetch(
      `${this.baseUrl}/api/pages?filters[slug][$eq]=${slug}&locale=${locale}&populate=deep`,
      { headers: { Authorization: `Bearer ${this.token}` } }
    )
    const { data } = await response.json() as StrapiV5Response

    if (!data?.length) throw new ContentNotFoundError(slug)

    const [page] = data
    return {
      id: page.documentId, // v5 : documentId à la place de id
      title: page.title,
      slug: page.slug,
      blocks: this.normaliseStrapiBlocks(page.blocks),
      meta: page.seo ?? {}
    }
  }
}

L'anti-pattern Strapi :

Découvrir les erreurs de permissions Strapi à l'exécution. Strapi a un système de permissions granulaire - les endpoints publics doivent être activés explicitement. Ne pas le tester pendant le développement produit des erreurs 403 en production, pour des content types visibles dans l'admin du CMS mais pas accessibles publiquement.

Le correctif : valider les permissions Strapi dans les tests de setup du handler (vérification de l'accessibilité des endpoints à l'initialisation du handler).

DatoCMS : patterns et anti-patterns

Ce que la couche frontend absorbe :

DatoCMS a une API GraphQL particulièrement propre et une bonne bibliothèque cliente TypeScript. Cela encourage un couplage fort - DatoCMS est si agréable à utiliser que l'anti-pattern s'installe subtilement : le handler devient trop mince, parce que l'API semble demander peu de normalisation.

Jusqu'au moment où le modèle de contenu change. DatoCMS permet de modifier le modèle de contenu sans migration de schéma (contrairement à Contentful). C'est une fonctionnalité - mais cela veut dire que le handler doit tester explicitement les changements de modèle, car le client TypeScript n'effectue aucune validation automatique du schéma.

// Handler DatoCMS avec garde de schéma explicite
class DatoCMSHandler implements CMSHandler {
  async fetchPage(slug: string, locale: string): Promise<PageData> {
    const { page } = await this.client.request<DatoCMSPageResponse>(
      PAGE_QUERY,
      { slug, locale: locale as SiteLocale }
    )

    if (!page) throw new ContentNotFoundError(slug)

    // Garde de schéma explicite : vérifier les champs critiques
    if (!page.title || !page.slug) {
      throw new SchemaValidationError(`Page ${slug}: missing required fields`)
    }

    return this.normaliseDatoCMSPage(page)
  }
}

L'anti-pattern DatoCMS :

Le mode preview de DatoCMS sans abstraction par handler. DatoCMS a son propre mode preview, qui fonctionne via un `previewSecret` et une route `/api/preview`. L'implémenter directement dans les pages Next.js, sans abstraction par handler, produit une logique de preview spécifique au CMS. Changer de CMS signifie alors réimplémenter la preview de zéro.

Le correctif : abstraire le mode preview au niveau du handler - `handler.getPreviewData(slug)` renvoie les données de preview, indépendamment du mécanisme de preview du CMS.

Prismic : patterns et anti-patterns

Ce que la couche frontend absorbe :

Prismic a son propre concept de slices pour la construction de pages - les pages sont faites de "slices" qui représentent différents types de composants. C'est conceptuellement proche de Storyblok, mais avec son propre workflow SliceMachine pour la génération de code.

// Handler Prismic
class PrismicHandler implements CMSHandler {
  async fetchPage(slug: string, locale: string): Promise<PageData> {
    const document = await this.client.getByUID('page', slug, {
      lang: locale === 'de' ? 'de-de' : 'en-us'
    })

    if (!document) throw new ContentNotFoundError(slug)

    return {
      id: document.id,
      title: document.data.title[0]?.text ?? '',
      slug: document.uid,
      blocks: document.data.slices.map(slice => this.normalisePrismicSlice(slice)),
      meta: {
        title: document.data.meta_title ?? '',
        description: document.data.meta_description ?? ''
      }
    }
  }

  private normalisePrismicSlice(slice: PrismicSlice): Block {
    return {
      type: slice.slice_type,
      data: {
        primary: slice.primary,
        items: slice.items
      },
      id: slice.id
    }
  }
}

L'anti-pattern Prismic :

Utiliser les composants générés par SliceMachine directement comme composants UI finaux. SliceMachine génère des composants React avec des types de props propres à Prismic. Intégrer ces composants directement dans le design system UI (au lieu de s'en servir comme adaptateurs qui transmettent des props normalisées aux composants du design system) inscrit les types Prismic dans la bibliothèque de composants.

Le correctif : traiter les composants SliceMachine comme une couche d'adaptation - ils traduisent les props Prismic en props du design system, puis appellent des composants génériques du design system.

Le tableau d'ensemble : ce qui rend l'agnosticisme CMS réel

Sur l'ensemble des sept systèmes de CMS, les patterns sont constants :

Ce qui fonctionne dans tous les cas :

  1. Abstraction par handler au niveau de la plateforme (pas au niveau de la page ni du composant)
  2. Interface ContentEntry normalisée, qui encapsule les formats propres à chaque CMS
  3. Mapping des locales dans le handler (chaque CMS a ses propres conventions de locale)
  4. Mode preview comme responsabilité du handler, pas de la page
  5. Gardes de schéma sur les champs critiques (protection contre la dérive du modèle)

Ce qui pose problème dans tous les cas :

  1. Appels de SDK directement dans les composants ou dans les pages
  2. Rendu du rich text sans renderer normalisé
  3. Logique de preview spécifique au CMS dans la couche UI
  4. Chaînes de locale sans logique de mapping (chaque CMS a des codes de locale différents)
  5. Directives de mode édition dans les composants UI finaux

Les 50+ intégrations de stacks sur la plateforme Composable Headless Frontend reposent sur ce pattern. Le CMS est une décision d'implémentation interchangeable - pas une décision d'architecture.

[Marcel section]

C'est le cœur de ce que nous entendons chez Laioutr par "headless cms agnostic". Pas : "nous prenons en charge beaucoup de systèmes de CMS." Mais : "la couche frontend est construite de telle sorte que le CMS soit un composant configurable, pas une dépendance structurelle."

Si vous voulez évaluer l'effort d'ingénierie concret pour votre stack - lequel des sept patterns de handler concerne votre setup, à quoi ressemble le travail d'abstraction sur du code frontend existant - une conversation technique avec Sebastian est le chemin le plus direct.

Le concept Agentic Frontend Management Platform constitue le cadre ; l'implémentation du handler, c'est le travail concret. Nous pouvons parcourir les deux avec vous.

[End Marcel section]

Plus sur cette capacité : Content Management

À lire aussi : Pas de changement de CMS, pas de réécriture du storefront · Quand votre couche CMS change de mains · CMS headless pour l'eCommerce Next.js 2026

D'autres articles intéressants

Un savoir-faire concret pour le développement frontend, les agents intelligents et le headless

App Shopify
Shopify
Shopify est une plateforme de commerce pour vendre en ligne et en magasin.
App shopware
Shopware
Shopware est une plateforme e-commerce européenne et flexible pour les catalogues produits et le commerce omnicanal.
App adobe commerce
Adobe Commerce
Adobe Commerce est une plateforme de commerce enterprise pour des scénarios B2C et B2B complexes et internationaux.
Planned
App B2B sellers suite
B2Bsellers
Suite B2B pour Shopware qui transforme la boutique en ligne en plateforme de commerce B2B professionnelle.
Planned
App commerce layer
Commerce Layer
Commerce Layer est une plateforme de commerce headless pour rendre stocks et catalogues disponibles en ligne.
App commercetools
Commercetools
Commercetools est une plateforme e-commerce headless en mode SaaS, utilisée dans le monde entier.
App emporix
Emporix
Emporix est une plateforme de commerce composable et API-first pour des scénarios B2B et B2C évolutifs.
Planned
App HCL Software
HCL Software
Suite enterprise pour le commerce et l'expérience digitale, hautement configurable.
Planned
App intershop
Intershop
Plateforme de commerce enterprise pour des modèles économiques B2B et B2C complexes.
Planned
App magento 2
Magento 2
Plateforme de commerce extensible et largement répandue pour les scénarios B2C et B2B.
App Oxid
OXID eShop
OXID eShop est une plateforme de commerce extensible pour les exigences B2B et B2C complexes.
Planned
App cover patchworks
Patchworks
Patchworks est une iPaaS low-code qui connecte e-commerce, ERP, WMS, 3PL et marketplaces.
Planned
App PRESTASHOP
Prestashop
Plateforme de commerce open source pour les petits et moyens commerçants en Europe et au-delà.
Planned
App saleor
Saleor
Plateforme de commerce open source et API-first basée sur GraphQL pour des storefronts sur mesure.
Planned
App Commercecloud
Salesforce Commerce Cloud
Salesforce Commerce Cloud est une plateforme de commerce cloud de niveau enterprise pour les entreprises de toutes tailles.
Planned
App SAP
SAP Commerce Cloud
Plateforme de commerce enterprise pour les catalogues complexes, les modèles de prix et les parcours omnicanaux.
Planned
App SCAYLE
Scayle
SCAYLE est un moteur de commerce qui permet aux marques et aux commerçants de développer leur activité à grande échelle.
Planned
App spryker
Spryker
Plateforme de commerce composable pour des modèles économiques B2B et B2C exigeants.
App Sylius
Sylius
Sylius est un framework e-commerce pensé pour les développeurs, dédié aux expériences d'achat B2C et B2B.
Planned
App vendure
Vendure
Vendure est une plateforme de commerce headless pour les entreprises aux exigences complexes.
Coming Soon
App VTEX
VTEX
Plateforme de commerce cloud-native et composable pour le B2B et le B2C à grande échelle.
Planned
App Websale
Websale
Backend de commerce stable et de niveau enterprise pour des environnements de vente complexes.
Book a demo mobile
Entretien stratégique

Prêt à faire de votre frontend une véritable couche de pilotage ?

Montrez-nous votre stack, votre roadmap, votre scénario de replatforming, et nous vous montrerons comment Laioutr s'intègre, ce que cela coûte et à quelle vitesse vous passez en production.

« Après 30 minutes, nous savions que Laioutr rendait notre replatforming réalisable. » - Daniel B., CEO, hygibox.de