Diil Docs
  1. Docs
  2. The widget

Live editing: the widget and data-crm markup

Updated:

The API gets content onto your site. This page gets editors off your back. Add one script tag, sprinkle a few data-crm-* attributes over your markup, and the people who own the words can click a heading on the real, live site, fix the typo and hit save — no ticket, no deploy, no “can you just change one comma on the homepage?” at 6 p.m. on a Friday.

Setting it up takes four steps, and only one of them involves thinking:

  1. Add the widget script

    One <script> tag on every page.
  2. Allow the CRM to frame your site

    One response header, so the CRM can open your site inside its Live mode.
  3. Mark up editable elements

    Tell the editor which element shows which block with data-crm-* attributes.
  4. Render fresh content in Live mode

    Skip your cache while an editor is looking, so changes show up instantly.

What editors get

From the editor's chair, live editing looks like this:

  1. They open your site in Live mode inside the CRM. It is your real site, not a mock-up.
  2. Every element you marked up gets a frame on hover. They click the one they want — a heading, a paragraph, a picture.
  3. They change the text or upload a new image and press save.
  4. The page refreshes with the new content. Done. Nobody opened a code editor.

If an element is marked up with a block marker that does not exist in the CRM yet, the editor can create the block right from the site. So you can ship the markup first and let the content team fill it in later.

How it works under the hood

Your page keeps rendering content from the Content API exactly as before. The data-crm-* attributes don't render anything — they just connect a DOM element with a block in the CRM, like a label on a drawer.

  • Live mode is an iframe. The CRM loads your site in a frame and adds ?crm_live=1 to the URL. The editor loads only inside that frame — when the referrer is the CRM, or the URL has ?crm_live and the referrer is empty or your own site — so if someone else embeds your site in their iframe, no editor shows up there.
  • One script loads what is needed, and only that. widget.js is the only tag you add. The editor itself (widget.editor.js) is loaded only when the site is opened inside the CRM Live mode. The support chat (widget.support.js) comes along only if chat is enabled in the CRM. Visitor sign-in (widget.auth.js) only if the page has data-crm-login or data-crm-auth elements or a data-crm-key attribute.
  • Visitors don't pay for it. Outside the CRM nothing editor-related is loaded — your visitors never download the editor.
  • Saving is a normal CRM edit. The CRM writes the block, the site's content version goes up, and the next API request returns fresh data.
  • Included the tag twice by accident? No harm done: the second copy is ignored.

Step 1. Add the widget script

Put the tag on every page, right before </body>. If your site has a shared layout, that is the one place to put it.

<!doctype html>
<html lang="en">
  <head>…</head>
  <body>
    …your page…

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

That is it for the script. No key, no init call, no config object — the widget figures out on its own whether it is running inside the CRM.

Step 2. Allow the CRM to frame your site

Live mode shows your site in an iframe on https://sitecog.com. Browsers only allow that if your site says so. Your responses need two things:

  • a Content-Security-Policy header with frame-ancestors 'self' https://sitecog.com;
  • no X-Frame-Options header with DENY or SAMEORIGIN — it overrules good intentions and blocks the frame.

Pick your server:

server {
    # …

    # Let the Diil CRM open the site in Live mode
    add_header Content-Security-Policy "frame-ancestors 'self' https://sitecog.com" always;

    # Delete any "add_header X-Frame-Options …" lines in this server block.
    # If the app behind proxy_pass sets X-Frame-Options itself, drop it here:
    proxy_hide_header X-Frame-Options;
}

Step 3. Mark up editable elements

Now tell the editor what is what. Each attribute says “this element shows that block”. The value is a path that starts with the block marker you set in the CRM.

Attribute reference

AttributePut it onWhat editors can do
data-crm-textAny element that shows text: h1, p, span, a button labelEdit the text of a text block or a text field
data-crm-imageThe <img> that shows an image block or fieldUpload or replace the picture
data-crm-videoThe <video> that shows a video block or fieldUpload or replace the video
data-crm-objectThe container that renders an object block (a section, a card)See the group of fields as one block
data-crm-arrayThe container that renders a list — an array block or an array fieldSee the list as one whole

Simple blocks need nothing more than their marker:

Top-level blockstsx
<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>

Path syntax: reaching inside objects and arrays

Object and array blocks have fields inside, so the path keeps going with dots. The first segment is always the block marker. After it come object field markers and numeric array indexes (starting at 0).

PathPoints at
hero_titleThe whole block hero_title
faq_section.titleField title of the object block faq_section
faq_section.itemsThe array field items
faq_section.items.0.questionField question of the first item
faq_section.items.2.answerField answer of the third item

The rules fit in four lines:

  • segments are separated by dots; each one is Latin letters, digits and underscores, 1–40 characters;
  • the first segment, the block marker, is at least 2 characters long (the usual marker rules);
  • a step into an object is a field marker, a step into an array is a number;
  • markers are case-sensitive: Hero_title and hero_title are two different blocks.

Full example: an FAQ section

Here is the data: one object block with a title and an array of questions.

faq_section from 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." }
      }
    ]
  }
}

And here is the markup. The object gets data-crm-object, the list gets data-crm-array, and every text inside gets a full path with the item index:

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>
  );
}

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

Arrays inside arrays work the same way — keep alternating field markers and indexes, e.g. pricing.plans.1.features.0.text.

Step 4. Render fresh content in Live mode

On our side every CRM edit reaches the API immediately. But your site may have a cache of its own: the browser may keep API responses for up to 60 seconds, a Next.js revalidate keeps them for its own window. Visitors won't notice. An editor who just pressed save and still sees the old text will.

The fix: when the page is open in Live mode, fetch with cache: 'no-store'. You can tell Live mode by the crm_live URL parameter or by the page running inside an iframe. Our own reference site does exactly this:

// crm.ts — is the page open in the CRM Live mode?
export function isCrmLive(): boolean {
  if (typeof window === 'undefined') return false;
  try {
    if (new URLSearchParams(window.location.search).has('crm_live')) return true;
    // The parameter can get lost after an internal link — the iframe check covers that
    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' },
    // Editors always get fresh content, visitors get the fast cached one
    cache: isCrmLive() ? 'no-store' : 'default',
  });
  if (!res.ok) throw new Error(`Content API: ${res.status}`);
  return res.json();
}

What is cached where, and for how long, is covered on the Caching & ETag page.

Checklist

  • The widget.js tag is on every page, before </body>.
  • Responses carry frame-ancestors 'self' https://sitecog.com.
  • No X-Frame-Options header anywhere — check the hosting panel, CDN and framework defaults too.
  • Editable elements have data-crm-* attributes, and the markers match the CRM letter for letter.
  • Objects and lists are wrapped in data-crm-object / data-crm-array, and inner paths use the right indexes.
  • In Live mode the site fetches content with cache: 'no-store'.
  • You opened the site in Live mode, clicked a heading, changed it and saw the change. 🎉

Troubleshooting

“The site forbids embedding”

The CRM tried to open your site in a frame and the browser said no. Usual suspects:

  • the frame-ancestors directive is missing, or https://sitecog.com is not in it;
  • something still sends X-Frame-Options: a hosting panel, a CDN, a security plugin, helmet in Express;
  • the CSP is set via <meta> instead of a header, so frame-ancestors is ignored;
  • the header is configured for one host, but the site opens on another (with or without www).

Check what your server actually sends:

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

An element is not clickable in Live mode

  • The attribute is missing from the rendered HTML. Inspect the page in DevTools, not the source — some components don't pass unknown props down to the DOM.
  • The path is malformed: a space, a hyphen, a non-Latin letter, a trailing dot. The widget writes a warning about the marker format to the browser console.
  • Only the container is marked. data-crm-object and data-crm-array group things; the clickable parts are the texts and images inside, and they need their own data-crm-text / data-crm-image.
  • The widget script isn't on this particular page — easy to miss when a site has several layouts.

Saved, but the change is not visible

  • Your fetch is cached. Use cache: 'no-store' in Live mode (Step 4).
  • The page is fully static — built once at deploy time — so it can't know about new content until the next build. Make it fetch at request time, at least in Live mode.
  • The element shows hardcoded text or a fallback instead of the value from the API: the attribute is there, the data isn't.
  • The path points somewhere other than what is rendered — e.g. the element shows item 1 but is marked items.0.
  • The site key belongs to another site, or the page renders a different language than the one being edited. See Languages & fallbacks.

The widget can do more

The same widget.js tag also counts page views, sends your own events with window.crmTrack(name, params), turns form[data-crm-lead] forms into CRM leads and shows a live support chat. No extra scripts — just pick what you need: