Hero slot3 en

Frontend headless que sobrevive a cualquier CMS, patrones de más de 50 integraciones de stacks

Laioutr opera frontends sobre más de 50 stacks de backend diferentes. Siete de los sistemas headless CMS más utilizados en el contexto enterprise de la región DACH forman parte de ese conjunto. Los patrones que funcionan no son específicos de un CMS, y los anti-patrones que generan problemas se repiten de un sistema a otro.

Este artículo es documentación directa de esas integraciones. No es una "comparativa de los 7 mejores CMS", las tablas de precios las puede leer cualquiera. Lo que aquí se documenta es otra cosa: qué decisiones de la capa de frontend son correctas en cada CMS, y dónde los equipos construyen sistemáticamente el mismo problema.

El patrón general: primero la abstracción de handlers

Antes de recorrer los patrones específicos de cada CMS, el principio general que se sostiene en las más de 50 integraciones:

La capa de frontend no debería llamar directamente a un SDK de CMS. Debería llamar a un content handler abstracto que encapsule ese SDK. Esa es la diferencia entre un frontend profundamente casado con un CMS y otro que es agnóstico al CMS.

// Anti-patrón: SDK directamente en el código de la página
import { createClient } from 'contentful'
// o
import { createClient } from '@sanity/client'
// o
import { GraphQLClient } from 'graphql-request' // hygraph

// Los tres son anti-patrones a nivel de página

// Patrón: interfaz abstracta a nivel de plataforma
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>
}

Con esta interfaz como base, estos son los siete sistemas CMS.

Contentful: patrones y anti-patrones

Lo que absorbe la capa de frontend:

La Content Delivery API de Contentful admite GraphQL (respaldada por CDN), pero entrega el contenido en un formato propio de Contentful: wrappers `sys`, wrappers `fields`, referencias a assets embebidas con su propia comprobación `sys.type = 'Asset'`. Esa sobrecarga de formato no pertenece al código de los componentes.

// Contentful handler: normaliza el formato de 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', // ¡mapeo de locales de 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)
    })
  }
}

El anti-patrón clásico de Contentful:

Renderizar rich text directamente en los componentes, sin normalizador. El rich text de Contentful es un AST en JSON (similar a Slate.js), pero con tipos de nodo propios de Contentful para las entradas embebidas. Llamar a `documentToReactComponents(richText)` directamente en un componente incorpora una dependencia dura de Contentful en la capa de UI.

La solución: un renderizador de rich text propio a nivel de plataforma que convierta el AST de Contentful en un AST de componentes normalizado.

Storyblok: patrones y anti-patrones

Lo que absorbe la capa de frontend:

Storyblok trabaja con bloques anidados (componentes) en lugar de tipos de contenido. Eso es conceptualmente distinto de Contentful: Storyblok piensa en "stories" compuestas de "blocks". Cada block tiene una clave `component` que indica qué componente de UI hay que renderizar.

El patrón de Storyblok que funciona:

// Storyblok handler
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 da directamente el nombre del componente
      data: this.normaliseBlockData(block),
      id: block._uid
    }))
  }
}

El anti-patrón clásico de Storyblok:

Integrar la directiva `storyblokEditable` directamente en los componentes de UI finales. `storyblokEditable(blok)` añade atributos `data-` para el overlay del Visual Editor de Storyblok. Eso acopla los componentes de UI a la infraestructura de edición de Storyblok. Cuando el editor se traslada a otra plataforma (o se desactiva), quedan directivas muertas en el output del render.

La solución: detectar el modo editor a nivel de plataforma, no a nivel de componente. Cuando se cumple `isEditorMode()`, las directivas las inyecta el handler, y los componentes se mantienen limpios.

Hygraph: patrones y anti-patrones

Lo que absorbe la capa de frontend:

Hygraph es GraphQL nativo. Eso es una ventaja, pero tienta a los equipos a escribir queries de GraphQL directamente en los componentes. Para una arquitectura agnóstica al CMS, ese es el mismo anti-patrón que con cualquier otro SDK.

La fortaleza de Hygraph es su capa de GraphQL con capacidad de federación: un único endpoint de GraphQL puede agregar varias fuentes de backend. Para frontends con varias fuentes de datos (CMS + commerce + búsqueda), eso es una ventaja real, pero solo cuando la capa de frontend está limpiamente abstraída del esquema de GraphQL.

// Hygraph handler
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 query se queda en el handler, no en el componente
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 ... } }
    }
  }
`

El anti-patrón de Hygraph:

Configurar mal el fallback de locales de Hygraph. Hygraph tiene un sistema explícito de fallback de locales: si una entrada no existe en el locale solicitado, puede recurrir a un locale de reserva. No configurarlo da como resultado campos vacíos en lugar de contenido de fallback. Esto ocurre sobre todo con entradas heredadas que no tienen la localización completa.

La solución: definir `locales: [$primaryLocale, en]` en la query (Hygraph admite arrays de locales como lista de prioridad).

Sanity: patrones y anti-patrones

Lo que absorbe la capa de frontend:

Sanity tiene GROQ, su propio lenguaje de consulta, más potente que GraphQL pero con una curva de aprendizaje más pronunciada. Para las personas de frontend sin experiencia en GROQ, la tentación es escribir queries GROQ directamente en los server components de Next.js. Ese es el equivalente en Sanity del anti-patrón del SDK de Contentful.

// Sanity handler
class SanityHandler implements CMSHandler {
  async fetchPage(slug: string, locale: string): Promise<PageData> {
    // GROQ se queda en el handler, no en el componente
    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)
  }
}

El anti-patrón de Sanity:

Renderizar el Portable Text de Sanity directamente en los componentes, sin una capa de render normalizada. El Portable Text de Sanity es un formato de contenido por bloques con definiciones de marca propias para enlaces inline, anotaciones personalizadas y objetos embebidos. Usar `<PortableText value={portableText} />` directamente en un componente convierte ese componente en específico de Sanity.

La solución: un renderizador propio de Portable Text a nivel de plataforma, con tipos de bloque normalizados, que convierta la entrada específica de Sanity en un output agnóstico al CMS.

Strapi: patrones y anti-patrones

Lo que absorbe la capa de frontend:

Strapi tiene una API REST (la principal) y una API GraphQL (opcional, vía plugin). Para los setups enterprise, la API GraphQL es preferible, pero los setups de Strapi varían bastante según la versión (v4 frente a v5) y la configuración de plugins. La capa de handler tiene que encapsular esa variación.

Strapi v5 introdujo una nueva Document Service API con `documentId` en lugar de `id`. Llamar a la API directamente desde el frontend, sin capa de handler, significa breaking changes en cada actualización de Strapi.

// Strapi handler (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 en lugar de id
      title: page.title,
      slug: page.slug,
      blocks: this.normaliseStrapiBlocks(page.blocks),
      meta: page.seo ?? {}
    }
  }
}

El anti-patrón de Strapi:

Descubrir los errores de permisos de Strapi en runtime. Strapi tiene un sistema de permisos granular, los endpoints públicos deben habilitarse de forma explícita. No probarlo durante el desarrollo se traduce en errores 403 en producción para tipos de contenido que sí son visibles en el admin del CMS pero no son accesibles públicamente.

La solución: validar los permisos de Strapi como parte de los tests de setup del handler (comprobación de accesibilidad del endpoint durante la inicialización del handler).

DatoCMS: patrones y anti-patrones

Lo que absorbe la capa de frontend:

DatoCMS tiene una API GraphQL especialmente limpia y una buena librería cliente de TypeScript. Eso favorece el acoplamiento estrecho: trabajar con DatoCMS resulta tan agradable que el anti-patrón aparece de forma sutil, el handler se queda demasiado fino porque parece que la API necesita poca normalización.

Hasta que cambia el modelo de contenido. DatoCMS permite cambios en el modelo de contenido sin migraciones de esquema (a diferencia de Contentful). Eso es una feature, pero implica que el handler tiene que comprobar de forma explícita los cambios de modelo, porque el cliente de TypeScript no valida el esquema automáticamente.

// DatoCMS handler con schema guard explícito
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)

    // Schema guard explícito: comprobar los campos críticos
    if (!page.title || !page.slug) {
      throw new SchemaValidationError(`Page ${slug}: missing required fields`)
    }

    return this.normaliseDatoCMSPage(page)
  }
}

El anti-patrón de DatoCMS:

El modo preview de DatoCMS sin abstracción de handler. DatoCMS tiene su propio modo preview, que funciona con `previewSecret` y una ruta `/api/preview`. Implementarlo directamente en las páginas de Next.js, sin abstracción de handler, da como resultado una lógica de preview específica del CMS. Cambiar de CMS significa reimplementar el preview desde cero.

La solución: abstraer el modo preview a nivel de handler, `handler.getPreviewData(slug)` devuelve los datos de preview con independencia del mecanismo de preview del CMS.

Prismic: patrones y anti-patrones

Lo que absorbe la capa de frontend:

Prismic tiene su propio concepto de slices para construir páginas, las páginas se componen de "slices" que representan distintos tipos de componente. Eso es conceptualmente similar a Storyblok, pero con su propio flujo de trabajo SliceMachine para la generación de código.

// Prismic handler
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
    }
  }
}

El anti-patrón de Prismic:

Usar los componentes generados por SliceMachine directamente como componentes de UI finales. SliceMachine genera componentes React con prop types propios de Prismic. Integrar esos componentes directamente en el design system de UI (en lugar de usarlos como adaptadores que pasan props normalizadas a los componentes del design system) incorpora los tipos de Prismic a la librería de componentes.

La solución: tratar los componentes de SliceMachine como una capa de adaptación, traducen las props de Prismic a props del design system y después llaman a componentes genéricos del design system.

El cuadro común: qué hace real el agnosticismo respecto al CMS

En los siete sistemas CMS los patrones son consistentes:

Lo que funciona en todos los casos:

  1. Abstracción de handlers a nivel de plataforma (no a nivel de página ni de componente)
  2. Interfaz ContentEntry normalizada que encapsula los formatos propios de cada CMS
  3. Mapeo de locales en el handler (cada CMS tiene sus propias convenciones de locales)
  4. Modo preview como responsabilidad del handler, no de la página
  5. Schema guards para los campos críticos (protegen frente al desvío del modelo)

Lo que causa problemas en todos los casos:

  1. Llamadas al SDK directamente en componentes o páginas
  2. Renderizado de rich text sin un renderizador normalizado
  3. Lógica de preview específica del CMS en la capa de UI
  4. Strings de locale sin lógica de mapeo (cada CMS usa códigos de locale distintos)
  5. Directivas de modo editor en los componentes de UI finales

Las más de 50 integraciones de stacks sobre la plataforma Composable Headless Frontend se construyen con este patrón. El CMS es una decisión de implementación intercambiable, no una decisión de arquitectura.

[Marcel section]

Este es el núcleo de lo que en Laioutr entendemos por "headless cms agnostic". No: "damos soporte a muchos sistemas CMS". Sino: "la capa de frontend está construida de modo que el CMS es un componente configurable, no una dependencia estructural".

Si quieres evaluar el esfuerzo concreto de ingeniería para tu stack, cuál de los siete patrones de handler es relevante para tu setup, cómo sería el trabajo de abstracción sobre el código de frontend existente, una conversación técnica con Sebastian es el camino más directo.

El concepto Agentic Frontend Management Platform es el marco de referencia; la implementación del handler es el trabajo concreto. Podemos recorrer ambas cosas contigo.

[End Marcel section]

Más sobre esta capacidad: Content Management

Relacionado: Sin cambio de CMS, sin reescritura del storefront · Cuando tu capa de CMS cambia de manos · Headless CMS para eCommerce con Next.js en 2026

Más artículos interesantes

Conocimiento práctico sobre desarrollo frontend, agentes inteligentes y headless

App Shopify
Shopify
Shopify es una plataforma de comercio para vender online y en tienda física.
App shopware
Shopware
Shopware es una plataforma de e-commerce flexible de origen europeo para catálogos de productos y comercio omnicanal.
App adobe commerce
Adobe Commerce
Adobe Commerce es una plataforma de comercio empresarial para escenarios B2C y B2B complejos y globales.
Planned
App B2B sellers suite
B2Bsellers
Suite B2B para Shopware que convierte la tienda online en una plataforma profesional de comercio B2B.
Planned
App commerce layer
Commerce Layer
Commerce Layer es una plataforma de headless commerce para que inventarios y catálogos estén disponibles online.
App commercetools
Commercetools
Commercetools es una plataforma de e-commerce headless basada en SaaS y utilizada en todo el mundo.
App emporix
Emporix
Emporix es una plataforma de composable commerce API-first para escenarios B2B y B2C escalables.
Planned
App HCL Software
HCL Software
Suite empresarial de comercio y experiencia digital con un alto grado de configurabilidad.
Planned
App intershop
Intershop
Plataforma de comercio empresarial para modelos de negocio B2B y B2C complejos.
Planned
App magento 2
Magento 2
Plataforma de comercio ampliable y muy extendida para escenarios B2C y B2B.
App Oxid
OXID eShop
OXID eShop es una plataforma de comercio ampliable para requisitos B2B y B2C complejos.
Planned
App cover patchworks
Patchworks
Patchworks es un iPaaS low-code que conecta e-commerce, ERP, WMS, 3PL y marketplaces.
Planned
App PRESTASHOP
Prestashop
Plataforma de comercio open source para pequeños y medianos comerciantes en Europa y más allá.
Planned
App saleor
Saleor
Plataforma de comercio open source y API-first basada en GraphQL para storefronts a medida.
Planned
App Commercecloud
Salesforce Commerce Cloud
Salesforce Commerce Cloud es una plataforma de comercio empresarial en la nube para empresas de cualquier tamaño.
Planned
App SAP
SAP Commerce Cloud
Plataforma de comercio empresarial para catálogos complejos, modelos de precios y recorridos omnicanal.
Planned
App SCAYLE
Scayle
SCAYLE es un motor de comercio con el que marcas y comerciantes escalan su negocio.
Planned
App spryker
Spryker
Plataforma de composable commerce para modelos de negocio B2B y B2C exigentes.
App Sylius
Sylius
Sylius es un framework de e-commerce pensado para desarrolladores y para experiencias de compra B2C y B2B.
Planned
App vendure
Vendure
Vendure es una plataforma de headless commerce para empresas con requisitos complejos.
Coming Soon
App VTEX
VTEX
Plataforma de composable commerce cloud native para B2B y B2C a gran escala.
Planned
App Websale
Websale
Backend de comercio estable y apto para grandes empresas en entornos comerciales complejos.
Book a demo mobile
Llamada estratégica

¿Listos para convertir su frontend en una capa de control?

Muéstranos tu stack, tu roadmap, tu escenario de replatforming, y te mostraremos cómo encaja Laioutr, cuánto cuesta y qué tan rápido puedes estar en producción.

"Después de 30 minutos supimos que Laioutr hace viable nuestro replatforming." - Daniel B., CEO, hygibox.de

SEO / GEO / AEO Ready
Rendimiento y Core Web Vitals
WCAG 3.0 Ready
Seguimiento & Analytics
Consistencia de marca