Cinco minutos, una página, cero paneles de administración. Al final de esta guía, un titular de tu web vendrá del CRM de Diil, y tus editores podrán cambiarlo con un clic encima. Prepárate un café; igual no te da tiempo a terminártelo.
Vas a necesitar:
- acceso a un sitio en el CRM de Diil (puedes crear uno nuevo para trastear);
- una terminal con
curlo cualquier herramienta que sepa enviar una petición HTTP; - cualquier frontend: un HTML a pelo, React, Next.js, Nuxt… tú decides.
Prepara el contenido y una clave
Crea algo de contenido en el CRM
Abre tu sitio en el CRM y comprueba que tiene los idiomas que necesitas (por ejemplo, inglés). Después crea:
- una página con el marcador
home; - dentro, una sección con el marcador
hero; - y dentro de esta, un bloque de texto con el marcador
hero_title; escribe un titular en él.
Los marcadores son los nombres con los que tu código encuentra el contenido: letras latinas, dígitos y guiones bajos, de 2 a 40 caracteres, distinguiendo mayúsculas. Elígelos como si fueran nombres de variables: van a vivir mucho tiempo en tu código. Aquí tienes la foto completa de páginas, secciones y bloques.
- una página con el marcador
Consigue una clave de sitio
Ve a Ajustes → Claves de Content API y crea una clave. Tiene la forma
pk_seguido de 32 caracteres hexadecimales y está ligada a este sitio en concreto.Una clave nueva suele funcionar al momento; en el peor de los casos, dale unos minutos. Revocar funciona igual: las claves revocadas reciben
401 invalid_key.Haz tu primera petición
Pide la página
homeen inglés:curl "https://back.sitecog.com/content/v1/pages/home?lang=en" \ -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', { headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' }, }); console.log(await res.json());200 OKjson { "id": 26, "marker": "home", "name": "Home", "href": "/", "index": 0, "params": {}, "content": { "hero": { "id": 41, "marker": "hero", "name": "Hero", "index": 0, "show": true, "content": { "hero_title": { "id": 95, "marker": "hero_title", "name": "Hero title", "type": "text", "multilang": true, "updatedAt": "2026-09-20T16:33:23.000Z", "content": { "en": "Earbuds that mute the city" } } } } } }¿Ves el camino hasta tu titular?
content.hero.content.hero_title.content.en: página → sección → bloque → idioma. Todas las páginas tienen exactamente esta forma; los detalles, en Páginas.
Muéstralo en tu página
La misma petición en cuatro sabores. Elige el tuyo: todos hacen lo mismo, traer la página y meter el titular en un <h1>.
<h1 id="hero-title"></h1>
<script type="module">
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
});
const page = await res.json();
const hero = page.content.hero.content;
document.getElementById('hero-title').textContent = hero.hero_title.content.en;
</script>// Hero.jsx — un componente de cliente (Vite, CRA, lo que sea)
import { useEffect, useState } from 'react';
const API = 'https://back.sitecog.com/content';
const KEY = import.meta.env.VITE_CRM_KEY; // clave pública, sin problema en el navegador
export function Hero() {
const [hero, setHero] = useState(null);
useEffect(() => {
fetch(API + '/v1/pages/home?lang=en', { headers: { 'x-crm-key': KEY } })
.then((res) => res.json())
.then((page) => setHero(page.content.hero.content));
}, []);
if (!hero) return null;
return <h1>{hero.hero_title.content.en}</h1>;
}// app/page.tsx — un Server Component: se ejecuta en el servidor y la clave se queda allí
const API = 'https://back.sitecog.com/content';
export default async function Home() {
const res = await fetch(API + '/v1/pages/home?lang=en', {
headers: { 'x-crm-key': process.env.CRM_KEY! },
next: { revalidate: 60 }, // pregunta a la API como mucho una vez por minuto
});
if (!res.ok) throw new Error('Content API: ' + res.status);
const page = await res.json();
const hero = page.content.hero.content;
return <h1>{hero.hero_title.content.en}</h1>;
}<!-- pages/index.vue -->
<script setup lang="ts">
const config = useRuntimeConfig(); // NUXT_PUBLIC_CRM_KEY → config.public.crmKey
const { data: page } = await useFetch('https://back.sitecog.com/content/v1/pages/home', {
query: { lang: 'en' },
headers: { 'x-crm-key': config.public.crmKey },
});
const hero = computed(() => page.value?.content.hero.content);
</script>
<template>
<h1>{{ hero?.hero_title.content.en }}</h1>
</template>Dónde guardar la clave
Pon la clave en una variable de entorno en lugar de en el código; no porque sea secreta, sino porque así cambiar de sitio o rotar claves se queda en un cambio de una línea.
# .env.local (Next.js) — solo en el servidor, nunca llega al navegador
CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# El código del navegador necesita una variable pública. No pasa nada: la clave es de solo lectura.
NEXT_PUBLIC_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Vite: VITE_CRM_KEY=… Nuxt: NUXT_PUBLIC_CRM_KEY=…En un componente de servidor (como en la pestaña de Next.js de arriba) usa la variable solo de servidor: la clave nunca sale de tu servidor. Para el código del navegador, una variable pública está perfectamente bien.
Añade un helper de idiomas
Los bloques de texto llegan como mapas de idiomas: { "en": "…", "de": "…" }. La API nunca cambia un idioma por otro: si falta una traducción, sencillamente no está (o es una cadena vacía si el editor dejó el campo en blanco). Elegir un respaldo es cosa tuya, y este helper diminuto lo deja en una sola línea:
// lib/t.js
// Elige una traducción: idioma pedido → idioma de respaldo → la primera no vacía → ''
export function t(map, lang, fallback = 'en') {
if (!map) return '';
return map[lang] || map[fallback] || Object.values(map).find(Boolean) || '';
}// lib/t.ts
export type LangMap = Record<string, string>;
// Elige una traducción: idioma pedido → idioma de respaldo → la primera no vacía → ''
export function t(map: LangMap | null | undefined, lang: string, fallback = 'en'): string {
if (!map) return '';
return map[lang] || map[fallback] || Object.values(map).find(Boolean) || '';
}// Pide el idioma del visitante y el de respaldo en una sola petición
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=de,en', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
});
const page = await res.json();
const hero = page.content.hero.content;
t(hero.hero_title.content, 'de'); // "Kopfhörer, die die Stadt stummschalten"
t(hero.hero_title.content, 'de', 'en'); // ¿el alemán aún no está rellenado? → el texto en inglésCada código en ?lang= tiene que ser un idioma activo del sitio; si no, recibes 400 unknown_lang con la lista de los disponibles. Si omites lang, te llegan todos los idiomas activos de golpe. La historia completa, en Idiomas y respaldo.
Activa la edición en vivo
Ahora viene lo divertido. Tu página ya muestra contenido del CRM; vamos a dejar que los editores lo cambien directamente en la página, sin buscar el campo correcto en un formulario.
Añade el widget
Un script, una vez por página, justo antes de
</body>. Solo carga el editor cuando tu sitio se abre dentro del CRM, así que los visitantes normales no descargan ni un byte.<script src="https://widget.sitecog.com/widget.js" defer></script>Dile al editor qué elemento muestra qué bloque
Añade
data-crm-textcon el marcador del bloque al elemento que lo pinta. Sigue renderizando el texto de la API como hasta ahora: el atributo solo conecta el elemento con el bloque.<body> <h1 data-crm-text="hero_title">Earbuds that mute the city</h1> <!-- una vez por página, justo antes de </body> --> <script src="https://widget.sitecog.com/widget.js" defer></script> </body>// app/layout.tsx export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <script src="https://widget.sitecog.com/widget.js" defer /> </body> </html> ); } // app/page.tsx — el mismo render de antes, más un atributo return <h1 data-crm-text="hero_title">{hero.hero_title.content.en}</h1>;Las imágenes, los vídeos, los objetos y los arrays tienen sus propios atributos (
data-crm-image,data-crm-video,data-crm-object,data-crm-array): mira Widget y marcado. Los atributos no molestan a los visitantes, así que déjalos en producción.Deja que el CRM meta tu sitio en un iframe
El modo Live abre tu sitio dentro del CRM, en un iframe. Tu servidor tiene que permitirlo con
frame-ancestorsy no debe enviarX-Frame-Options: DENYniSAMEORIGIN. Si no, el CRM te dirá que el sitio prohíbe que lo incrusten.// next.config.js module.exports = { async headers() { return [{ source: '/:path*', headers: [ { key: 'Content-Security-Policy', value: "frame-ancestors 'self' https://sitecog.com" }, ], }]; }, };# nginx — y asegúrate de que nada envía X-Frame-Options: DENY o SAMEORIGIN add_header Content-Security-Policy "frame-ancestors 'self' https://sitecog.com";Clic, escribe, guarda
Abre tu sitio en el CRM en modo Live, haz clic en el titular, cámbialo y guarda. El CRM escribe el bloque, la versión del contenido sube, tu página trae el contenido fresco… y ahí está el titular nuevo. 🎉
Extra: si pones
data-crm-text="promo_note"en un elemento antes de que exista ese bloque, el editor puede crear el bloque directamente desde el sitio.
Sáltate la caché para los editores
Nuestro lado nunca sirve contenido viejo en modo Live. Pero tu caché sí puede: con revalidate: 60 un editor podría guardar y seguir viendo el texto antiguo hasta un minuto. Dentro del marco del CRM la URL lleva ?crm_live=1; úsalo para hacer la petición con cache: 'no-store':
// app/page.tsx — sáltate cualquier caché mientras un editor está mirando
const API = 'https://back.sitecog.com/content';
export default async function Home({ searchParams }: { searchParams: Promise<{ crm_live?: string }> }) {
const live = (await searchParams).crm_live === '1';
const res = await fetch(API + '/v1/pages/home?lang=en', {
headers: { 'x-crm-key': process.env.CRM_KEY! },
...(live ? { cache: 'no-store' as const } : { next: { revalidate: 60 } }),
});
const page = await res.json();
const hero = page.content.hero.content;
return <h1 data-crm-text="hero_title">{hero.hero_title.content.en}</h1>;
}// Dentro del marco del CRM (o con ?crm_live=1) pide siempre contenido fresco
const live = window.self !== window.top || new URLSearchParams(location.search).has('crm_live');
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
cache: live ? 'no-store' : 'default',
});Y ahora qué
Ya tienes el contenido fluyendo y a los editores haciendo clic. Por aquí puedes seguir profundizando: