Diil Docs
  1. Documentación
  2. El widget

Edición en vivo: el widget y el marcado data-crm

Actualizado:

La API lleva el contenido a tu web. Esta página te quita a los editores de encima. Añade una etiqueta script, espolvorea unos cuantos atributos data-crm-* por tu marcado y quienes mandan sobre los textos podrán hacer clic en un título de la web real, en producción, corregir la errata y guardar: sin ticket, sin deploy y sin el “¿me cambias solo una coma de la home?” un viernes a las seis de la tarde.

Montarlo lleva cuatro pasos, y solo uno requiere pensar:

  1. Añade el script del widget

    Una etiqueta <script> en cada página.
  2. Deja que el CRM meta tu web en un iframe

    Una cabecera de respuesta para que el CRM pueda abrir tu web dentro de su modo Live.
  3. Marca los elementos editables

    Dile al editor qué elemento muestra qué bloque con los atributos data-crm-*.
  4. Sirve contenido fresco en el modo Live

    Sáltate tu caché mientras un editor está mirando, para que los cambios aparezcan al instante.

Qué obtienen los editores

Desde la silla del editor, la edición en vivo se ve así:

  1. Abren tu web en el modo Live dentro del CRM. Es tu web real, no una maqueta.
  2. Cada elemento que hayas marcado se enmarca al pasar el ratón. Hacen clic en el que quieren: un título, un párrafo, una imagen.
  3. Cambian el texto o suben una imagen nueva y le dan a guardar.
  4. La página se recarga con el contenido nuevo. Listo. Nadie ha abierto un editor de código.

Si un elemento está marcado con un marcador de bloque que todavía no existe en el CRM, el editor puede crear el bloque directamente desde la web. Así que puedes subir primero el marcado y dejar que el equipo de contenido lo rellene después.

Cómo funciona por dentro

Tu página sigue renderizando el contenido de la Content API exactamente igual que antes. Los atributos data-crm-* no renderizan nada: solo conectan un elemento del DOM con un bloque del CRM, como la etiqueta de un cajón.

  • El modo Live es un iframe. El CRM carga tu web en un frame y añade ?crm_live=1 a la URL. El widget solo carga el editor dentro de ese frame del CRM (cuando el referrer viene del CRM, o la URL lleva ?crm_live y el referrer está vacío o es de tu propio sitio); si otra web mete la tuya en su propio iframe, allí no aparece ningún editor.
  • Un script carga lo que hace falta, y nada más. widget.js es la única etiqueta que añades. El editor en sí (widget.editor.js) solo se carga cuando la web se abre dentro del modo Live del CRM. El chat de soporte (widget.support.js) solo viene si el chat está activado en el CRM. El inicio de sesión de visitantes (widget.auth.js), solo si la página tiene elementos data-crm-login o data-crm-auth o un atributo data-crm-key.
  • Los visitantes no lo pagan. Fuera del CRM no se carga nada del editor: tus visitantes nunca se lo descargan.
  • Guardar es una edición normal del CRM. El CRM escribe el bloque, la versión del contenido del sitio sube y la siguiente petición a la API devuelve datos frescos.
  • ¿Has puesto la etiqueta dos veces sin querer? No pasa nada: la segunda copia se ignora.

Paso 1. Añade el script del widget

Pon la etiqueta en todas las páginas, justo antes de </body>. Si tu web tiene un layout compartido, ese es el sitio.

<!doctype html>
<html lang="en">
  <head>…</head>
  <body>
    …tu página…

    <script src="https://widget.sitecog.com/widget.js" defer></script>
  </body>
</html>

Y eso es todo en cuanto al script. Ni clave, ni llamada de init, ni objeto de configuración: el widget averigua solo si está corriendo dentro del CRM.

Paso 2. Deja que el CRM meta tu web en un iframe

El modo Live muestra tu web en un iframe en https://sitecog.com. Los navegadores solo lo permiten si tu web lo autoriza. Tus respuestas necesitan dos cosas:

  • una cabecera Content-Security-Policy con frame-ancestors 'self' https://sitecog.com;
  • ninguna cabecera X-Frame-Options con DENY o SAMEORIGIN: se impone a las buenas intenciones y bloquea el frame.

Elige tu servidor:

server {
    # …

    # Permite que el CRM de Diil abra la web en modo Live
    add_header Content-Security-Policy "frame-ancestors 'self' https://sitecog.com" always;

    # Borra cualquier línea "add_header X-Frame-Options …" de este bloque server.
    # Si la app detrás de proxy_pass pone X-Frame-Options por su cuenta, quítala aquí:
    proxy_hide_header X-Frame-Options;
}

Paso 3. Marca los elementos editables

Ahora dile al editor qué es cada cosa. Cada atributo dice “este elemento muestra aquel bloque”. El valor es una ruta que empieza por el marcador del bloque que pusiste en el CRM.

Referencia de atributos

AtributoDónde ponerloQué pueden hacer los editores
data-crm-textCualquier elemento que muestre texto: h1, p, span, el texto de un botónEditar el texto de un bloque de texto o de un campo de texto
data-crm-imageEl <img> que muestra un bloque o campo de imagenSubir o cambiar la imagen
data-crm-videoEl <video> que muestra un bloque o campo de vídeoSubir o cambiar el vídeo
data-crm-objectEl contenedor que renderiza un bloque object (una sección, una tarjeta)Ver el grupo de campos como un solo bloque
data-crm-arrayEl contenedor que renderiza una lista: un bloque array o un campo de tipo arrayVer la lista como un todo

Los bloques simples no necesitan más que su marcador:

Bloques de primer niveltsx
<section>
  <h1 data-crm-text="hero_title">{hero.hero_title.content.en}</h1>
  <p data-crm-text="hero_subtitle">{hero.hero_subtitle.content.en}</p>
  <img data-crm-image="hero_image" src={hero.hero_image.content.file} alt="" />
  <video data-crm-video="hero_video" src={hero.hero_video.content.file} autoPlay muted loop />
</section>

Sintaxis de rutas: entrar en objetos y arrays

Los bloques object y array tienen campos dentro, así que la ruta sigue con puntos. El primer segmento siempre es el marcador del bloque. Después vienen los marcadores de los campos del objeto y los índices numéricos del array (empezando por 0).

RutaApunta a
hero_titleEl bloque hero_title entero
faq_section.titleEl campo title del bloque object faq_section
faq_section.itemsEl campo array items
faq_section.items.0.questionEl campo question del primer elemento
faq_section.items.2.answerEl campo answer del tercer elemento

Las reglas caben en cuatro líneas:

  • los segmentos se separan con puntos; cada uno lleva letras latinas, dígitos y guiones bajos, de 1 a 40 caracteres;
  • el primer segmento, el marcador del bloque, tiene al menos 2 caracteres (las reglas de siempre de los marcadores);
  • un paso dentro de un objeto es un marcador de campo; un paso dentro de un array es un número;
  • los marcadores distinguen mayúsculas: Hero_title y hero_title son dos bloques distintos.

Ejemplo completo: una sección de FAQ

Estos son los datos: un bloque object con un título y un array de preguntas.

faq_section de GET /v1/pages/home?lang=enjson
"faq_section": {
  "type": "object",
  "content": {
    "title": { "en": "FAQ" },
    "items": [
      {
        "question": { "en": "How long is delivery?" },
        "answer": { "en": "1–3 days." }
      },
      {
        "question": { "en": "Can I return the earbuds?" },
        "answer": { "en": "Yes, within 14 days." }
      }
    ]
  }
}

Y este es el marcado. El objeto lleva data-crm-object, la lista lleva data-crm-array y cada texto de dentro lleva la ruta completa con el índice del elemento:

type LangMap = Record<string, string>;
type FaqItem = { question: LangMap; answer: LangMap };
type FaqBlock = { content: { title: LangMap; items: FaqItem[] } };

export function Faq({ block, lang }: { block: FaqBlock; lang: string }) {
  const { title, items } = block.content;

  return (
    <section data-crm-object="faq_section">
      <h2 data-crm-text="faq_section.title">{title[lang]}</h2>

      <div data-crm-array="faq_section.items">
        {items.map((item, i) => (
          <details key={i}>
            <summary data-crm-text={`faq_section.items.${i}.question`}>
              {item.question[lang]}
            </summary>
            <p data-crm-text={`faq_section.items.${i}.answer`}>
              {item.answer[lang]}
            </p>
          </details>
        ))}
      </div>
    </section>
  );
}

// Uso: <Faq block={page.content.faq.content.faq_section} lang="en" />

Los arrays dentro de arrays funcionan igual: sigue alternando marcadores de campo e índices, p. ej. pricing.plans.1.features.0.text.

Paso 4. Sirve contenido fresco en el modo Live

Por nuestra parte, cada edición en el CRM llega a la API al momento. Pero tu web puede tener su propia caché: el navegador puede guardar las respuestas de la API hasta 60 segundos, y un revalidate de Next.js las guarda durante su propia ventana. Los visitantes no lo notarán. Un editor que acaba de guardar y sigue viendo el texto viejo, sí.

La solución: cuando la página está abierta en el modo Live, haz el fetch con cache: 'no-store'. El modo Live se reconoce por el parámetro crm_live de la URL o porque la página se ejecuta dentro de un iframe. Nuestra propia web de referencia hace exactamente esto:

// crm.ts — ¿está la página abierta en el modo Live del CRM?
export function isCrmLive(): boolean {
  if (typeof window === 'undefined') return false;
  try {
    if (new URLSearchParams(window.location.search).has('crm_live')) return true;
    // El parámetro se puede perder tras un enlace interno: la comprobación del iframe lo cubre
    return window.parent !== window;
  } catch {
    return false;
  }
}

export async function getPage(marker: string) {
  const res = await fetch(`https://back.sitecog.com/content/v1/pages/${marker}`, {
    headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
    // Los editores siempre reciben contenido fresco; los visitantes, el rápido de la caché
    cache: isCrmLive() ? 'no-store' : 'default',
  });
  if (!res.ok) throw new Error(`Content API: ${res.status}`);
  return res.json();
}

Qué se cachea dónde y durante cuánto tiempo lo tienes en la página Caché y ETag.

Checklist

  • La etiqueta widget.js está en todas las páginas, antes de </body>.
  • Las respuestas llevan frame-ancestors 'self' https://sitecog.com.
  • No hay ninguna cabecera X-Frame-Options en ningún sitio: revisa también el panel del hosting, el CDN y los valores por defecto del framework.
  • Los elementos editables tienen atributos data-crm-* y los marcadores coinciden con el CRM letra por letra.
  • Los objetos y las listas están envueltos en data-crm-object / data-crm-array y las rutas internas usan los índices correctos.
  • En el modo Live la web pide el contenido con cache: 'no-store'.
  • Has abierto la web en el modo Live, has hecho clic en un título, lo has cambiado y has visto el cambio. 🎉

Solución de problemas

“El sitio no permite incrustarlo”

El CRM intentó abrir tu web en un frame y el navegador dijo que no. Los sospechosos habituales:

  • falta la directiva frame-ancestors, o https://sitecog.com no está en ella;
  • algo sigue enviando X-Frame-Options: un panel de hosting, un CDN, un plugin de seguridad, helmet en Express;
  • la CSP se pone con <meta> en vez de con una cabecera, así que frame-ancestors se ignora;
  • la cabecera está configurada para un host, pero la web se abre en otro (con o sin www).

Comprueba qué envía tu servidor de verdad:

curl -sI https://your-site.com | grep -iE "content-security-policy|x-frame-options"

Un elemento no se puede pulsar en el modo Live

  • El atributo no está en el HTML renderizado. Inspecciona la página en DevTools, no el código fuente: algunos componentes no pasan las props desconocidas al DOM.
  • La ruta está mal formada: un espacio, un guion, una letra no latina, un punto al final. El widget escribe un aviso sobre el formato del marcador en la consola del navegador.
  • Solo está marcado el contenedor. data-crm-object y data-crm-array agrupan cosas; las partes pulsables son los textos y las imágenes de dentro, y necesitan su propio data-crm-text / data-crm-image.
  • El script del widget no está en esta página en concreto: es fácil que se escape cuando la web tiene varios layouts.

Guardado, pero el cambio no se ve

  • Tu fetch está en caché. Usa cache: 'no-store' en el modo Live (paso 4).
  • La página es totalmente estática (se genera una vez en el deploy), así que no puede enterarse del contenido nuevo hasta el siguiente build. Haz que pida los datos en cada petición, al menos en el modo Live.
  • El elemento muestra un texto hardcodeado o un fallback en vez del valor de la API: el atributo está, los datos no.
  • La ruta apunta a un sitio distinto de lo que se renderiza; p. ej., la etiqueta muestra el elemento 1 pero está marcado como items.0.
  • La clave del sitio es de otro sitio, o la página renderiza un idioma distinto del que se está editando. Mira Idiomas y fallbacks.

El widget sabe hacer más

La misma etiqueta widget.js cuenta visitas, envía tus propios eventos con window.crmTrack(name, params), convierte los formularios form[data-crm-lead] en leads del CRM y muestra un chat de soporte en vivo. Sin scripts extra — elige lo que necesites: