API доставляє контент на ваш сайт. А ця сторінка знімає редакторів з вашої шиї. Додайте один тег <script>, розсипте по розмітці кілька атрибутів data-crm-* — і люди, які відповідають за тексти, зможуть клацнути заголовок просто на живому сайті, виправити одруківку й натиснути «Зберегти». Без тікетів, без деплою і без «а можна поміняти одну кому на головній?» о шостій вечора в п’ятницю.
Налаштування — це чотири кроки, і думати доведеться лише на одному з них:
Додайте скрипт віджета
Один тег<script>на кожній сторінці.Дозвольте CRM вбудовувати ваш сайт
Один заголовок відповіді, щоб CRM могла відкрити ваш сайт у режимі Live.Розмітьте редаговані елементи
Підкажіть редактору атрибутамиdata-crm-*, який елемент показує який блок.Віддавайте свіжий контент у режимі Live
Обходьте свій кеш, поки на сторінку дивиться редактор, — і зміни з’являтимуться миттєво.
Що отримують редактори
З крісла редактора редагування на сайті виглядає так:
- Він відкриває ваш сайт у режимі Live усередині CRM. Це ваш справжній сайт, а не макет.
- Кожен розмічений елемент при наведенні отримує рамку. Редактор клацає потрібний — заголовок, абзац, картинку.
- Змінює текст або завантажує нове зображення й натискає «Зберегти».
- Сторінка оновлюється з новим контентом. Готово. Редактор коду ніхто навіть не відкривав.
Якщо елемент розмічено маркером блока, якого в CRM ще немає, редактор може створити цей блок просто із сайту. Тож можна спершу викотити розмітку, а контент-команда заповнить її пізніше.
Як це працює під капотом
Ваша сторінка й далі виводить контент із Content API точнісінько як раніше. Атрибути data-crm-* нічого не рендерять — вони лише пов’язують DOM-елемент із блоком у CRM, як наліпка на шухляді.
- Режим Live — це iframe. CRM завантажує ваш сайт у фреймі й додає до URL
?crm_live=1. Редактор вмикається лише в цій рамці: коли referrer веде на адресу CRM або в URL є?crm_live, а referrer порожній чи з вашого ж сайту. Якщо ваш сайт вбудує у свій iframe хтось інший, редактор там не завантажиться. - Один скрипт підвантажує лише те, що потрібно.
widget.js— єдиний тег, який ви додаєте. Сам редактор (widget.editor.js) завантажується лише тоді, коли сайт відкрито в режимі Live усередині CRM. Чат підтримки (widget.support.js) приїжджає, тільки якщо чат увімкнено в CRM. Вхід відвідувачів (widget.auth.js) — тільки якщо на сторінці є елементиdata-crm-loginчиdata-crm-authабо атрибутdata-crm-key. - Відвідувачі за це не платять. Поза CRM нічого редакторського не завантажується — ваші відвідувачі ніколи не качають редактор.
- Збереження — це звичайна правка в CRM. CRM записує блок, версія контенту сайту зростає, і наступний запит до API повертає свіжі дані.
- Випадково підключили тег двічі? Нічого страшного: другу копію буде проігноровано.
Крок 1. Додайте скрипт віджета
Поставте тег на кожну сторінку, просто перед </body>. Якщо на сайті є спільний layout, то саме там йому й місце.
<!doctype html>
<html lang="en">
<head>…</head>
<body>
…ваша сторінка…
<script src="https://widget.sitecog.com/widget.js" defer></script>
</body>
</html>// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script src="https://widget.sitecog.com/widget.js" strategy="afterInteractive" />
</body>
</html>
);
}<!-- index.html у корені проєкту -->
<!doctype html>
<html lang="en">
<head>…</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="https://widget.sitecog.com/widget.js" defer></script>
</body>
</html>Зі скриптом усе. Ні ключа, ні виклику init, ні об’єкта конфігурації — віджет сам розбереться, чи працює він усередині CRM.
Крок 2. Дозвольте CRM вбудовувати ваш сайт
Режим Live показує ваш сайт в iframe на https://sitecog.com. Браузери дозволяють це лише тоді, коли ваш сайт не проти. У ваших відповідях мають бути дві речі:
- заголовок
Content-Security-Policyзframe-ancestors 'self' https://sitecog.com; - жодного заголовка
X-Frame-Optionsзі значеннямDENYчиSAMEORIGIN— він переважує добрі наміри й блокує фрейм.
Оберіть свій сервер:
server {
# …
# Дозволяємо CRM Diil відкривати сайт у режимі Live
add_header Content-Security-Policy "frame-ancestors 'self' https://sitecog.com" always;
# Видаліть у цьому блоці server усі рядки "add_header X-Frame-Options …".
# Якщо застосунок за proxy_pass сам ставить X-Frame-Options, приберіть його тут:
proxy_hide_header X-Frame-Options;
}# .htaccess або конфігурація VirtualHost (потрібен mod_headers)
<IfModule mod_headers.c>
Header always set Content-Security-Policy "frame-ancestors 'self' https://sitecog.com"
Header always unset X-Frame-Options
</IfModule>// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: "frame-ancestors 'self' https://sitecog.com",
},
],
},
];
},
};
export default nextConfig;// Перед вашими маршрутами
app.use((req, res, next) => {
res.removeHeader('X-Frame-Options');
res.setHeader('Content-Security-Policy', "frame-ancestors 'self' https://sitecog.com");
next();
});
// Використовуєте helmet? За замовчуванням він надсилає X-Frame-Options: SAMEORIGIN. Налаштуйте його так:
// app.use(helmet({
// xFrameOptions: false,
// contentSecurityPolicy: {
// directives: { frameAncestors: ["'self'", 'https://sitecog.com'] },
// },
// }));Крок 3. Розмітьте редаговані елементи
Тепер підкажіть редактору, що де. Кожен атрибут каже: «цей елемент показує отой блок». Значення — це шлях, який починається з маркера блока, заданого в CRM.
Довідник атрибутів
| Атрибут | Куди ставити | Що можуть редактори |
|---|---|---|
data-crm-text | На будь-який елемент із текстом: h1, p, span, підпис кнопки | Редагувати текст текстового блока чи текстового поля |
data-crm-image | На <img>, що показує блок або поле із зображенням | Завантажити або замінити картинку |
data-crm-video | На <video>, що показує блок або поле з відео | Завантажити або замінити відео |
data-crm-object | На контейнер, що рендерить блок object (секцію, картку) | Бачити групу полів як один блок |
data-crm-array | На контейнер, що рендерить список — блок array або поле-масив | Бачити список як одне ціле |
Простим блокам не потрібно нічого, крім маркера:
<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>Синтаксис шляхів: усередину об’єктів і масивів
У блоках object і array всередині є поля, тож шлях продовжується через крапку. Перший сегмент — завжди маркер блока. Далі йдуть маркери полів об’єкта й числові індекси масиву (від 0).
| Шлях | На що вказує |
|---|---|
hero_title | Увесь блок hero_title |
faq_section.title | Поле title блока-об’єкта faq_section |
faq_section.items | Поле-масив items |
faq_section.items.0.question | Поле question першого елемента |
faq_section.items.2.answer | Поле answer третього елемента |
Правила вміщаються в чотири рядки:
- сегменти розділяються крапками; кожен складається з латинських літер, цифр і підкреслень, від 1 до 40 символів;
- перший сегмент, маркер блока, має щонайменше 2 символи (звичайні правила для маркерів);
- крок усередину об’єкта — це маркер поля, крок усередину масиву — число;
- маркери чутливі до регістру:
Hero_titleіhero_title— два різні блоки.
Повний приклад: секція FAQ
Ось дані: один блок object із заголовком і масивом питань.
"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." }
}
]
}
}А ось розмітка. Об’єкт отримує data-crm-object, список — data-crm-array, а кожен текст усередині — повний шлях з індексом елемента:
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>
);
}
// Використання: <Faq block={page.content.faq.content.faq_section} lang="en" /><section data-crm-object="faq_section">
<h2 data-crm-text="faq_section.title">FAQ</h2>
<div data-crm-array="faq_section.items">
<details>
<summary data-crm-text="faq_section.items.0.question">How long is delivery?</summary>
<p data-crm-text="faq_section.items.0.answer">1–3 days.</p>
</details>
<details>
<summary data-crm-text="faq_section.items.1.question">Can I return the earbuds?</summary>
<p data-crm-text="faq_section.items.1.answer">Yes, within 14 days.</p>
</details>
</div>
</section>Масиви всередині масивів працюють так само — просто чергуйте маркери полів та індекси, наприклад pricing.plans.1.features.0.text.
Крок 4. Віддавайте свіжий контент у режимі Live
З нашого боку кожна правка в CRM потрапляє в API миттєво. Але у вашого сайту може бути власний кеш: браузер може тримати відповіді API до 60 секунд, а revalidate у Next.js — протягом свого вікна. Відвідувачі цього не помітять. А от редактор, який щойно натиснув «Зберегти» й досі бачить старий текст, — помітить.
Рішення: коли сторінку відкрито в режимі Live, робіть запит із cache: 'no-store'. Режим Live можна впізнати за параметром URL crm_live або за тим, що сторінка працює всередині iframe. Наш власний еталонний сайт робить саме так:
// crm.ts — чи відкрито сторінку в режимі Live у CRM?
export function isCrmLive(): boolean {
if (typeof window === 'undefined') return false;
try {
if (new URLSearchParams(window.location.search).has('crm_live')) return true;
// Параметр може загубитися після внутрішнього посилання — перевірка на iframe це покриває
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' },
// Редактори завжди отримують свіжий контент, відвідувачі — швидкий із кешу
cache: isCrmLive() ? 'no-store' : 'default',
});
if (!res.ok) throw new Error(`Content API: ${res.status}`);
return res.json();
}// app/page.tsx — Server Component бачить лише URL, тож перевіряє crm_live
type Props = { searchParams: Promise<Record<string, string | string[] | undefined>> };
export default async function Home({ searchParams }: Props) {
const live = 'crm_live' in (await searchParams);
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': process.env.CRM_KEY! },
// Режим Live: напряму з API. Для всіх інших: кеш на 60 секунд
...(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>;
}Що, де й скільки кешується, розказано на сторінці «Кеш і ETag».
Чекліст
- Тег
widget.jsє на кожній сторінці, перед</body>. - Відповіді містять
frame-ancestors 'self' https://sitecog.com. - Заголовка
X-Frame-Optionsнемає ніде — перевірте також панель хостингу, CDN і налаштування фреймворку за замовчуванням. - Редаговані елементи мають атрибути
data-crm-*, а маркери збігаються з CRM літера в літеру. - Об’єкти й списки обгорнуто в
data-crm-object/data-crm-array, а внутрішні шляхи мають правильні індекси. - У режимі Live сайт запитує контент із
cache: 'no-store'. - Ви відкрили сайт у режимі Live, клацнули заголовок, змінили його й побачили зміну. 🎉
Якщо щось не так
«Сайт забороняє вбудовування»
CRM спробувала відкрити ваш сайт у фреймі, а браузер сказав «ні». Звичайні підозрювані:
- директиви
frame-ancestorsнемає або в ній немаєhttps://sitecog.com; - щось і далі надсилає
X-Frame-Options: панель хостингу, CDN, плагін безпеки, helmet в Express; - CSP задано через
<meta>, а не заголовком, тожframe-ancestorsігнорується; - заголовок налаштовано для одного хоста, а сайт відкривається на іншому (з
wwwчи без).
Перевірте, що насправді надсилає ваш сервер:
curl -sI https://your-site.com | grep -iE "content-security-policy|x-frame-options"Елемент не клікається в режимі Live
- Атрибута немає у відрендереному HTML. Дивіться сторінку в DevTools, а не вихідний код — деякі компоненти не передають невідомі пропси в DOM.
- Шлях зіпсований: пробіл, дефіс, не латинська літера, крапка в кінці. Віджет пише попередження про формат маркера в консоль браузера.
- Розмічено лише контейнер.
data-crm-objectіdata-crm-arrayгрупують, а клікабельні частини — це тексти й картинки всередині, і їм потрібні власніdata-crm-text/data-crm-image. - Скрипта віджета немає саме на цій сторінці — легко прогавити, коли на сайті кілька layout-ів.
Зберегли, але зміни не видно
- Ваш запит кешується. У режимі Live використовуйте
cache: 'no-store'(крок 4). - Сторінка повністю статична — зібрана один раз під час деплою, — тож про новий контент вона дізнається лише після наступної збірки. Нехай вона робить запит під час кожного звернення, бодай у режимі Live.
- Елемент показує захардкоджений текст або запасне значення замість даних з API: атрибут є, а даних немає.
- Шлях указує не на те, що відрендерено, — наприклад, елемент показує елемент
1, а розмічений якitems.0. - Ключ сайту належить іншому сайту, або сторінка рендерить іншу мову, ніж ту, яку редагують. Див. «Мови та запасна мова».
Віджет уміє більше
Той самий тег widget.js рахує перегляди, надсилає ваші події через window.crmTrack(name, params), перетворює форми form[data-crm-lead] на заявки в CRM і показує онлайн-чат підтримки. Жодних зайвих скриптів — беріть, що потрібно: