П’ять хвилин, одна сторінка, нуль адмінок. Наприкінці цього посібника заголовок на вашому сайті надходитиме з CRM Diil — а редактори зможуть змінити його, просто клацнувши по ньому. Наливайте каву; є шанс, що допити не встигнете.
Вам знадобиться:
- доступ до сайту в CRM Diil (для експериментів можна створити новий);
- термінал із
curlабо будь-який інструмент, що вміє надіслати HTTP-запит; - будь-який фронтенд: звичайний HTML-файл, React, Next.js, Nuxt — на ваш смак.
Налаштуйте вміст і ключ
Створіть трохи вмісту в CRM
Відкрийте свій сайт у CRM і переконайтеся, що потрібні мови додано (скажімо, англійську). Потім створіть:
- сторінку з маркером
home; - у ній — секцію з маркером
hero; - у ній — текстовий блок із маркером
hero_titleі впишіть туди заголовок.
Маркери — це імена, за якими ваш код знаходить вміст: латинські літери, цифри та підкреслення, 2–40 символів, з урахуванням регістру. Обирайте їх, як назви змінних, — вони житимуть у вашому коді довго. Ось повна картина сторінок, секцій і блоків.
- сторінку з маркером
Отримайте ключ сайту
Перейдіть у Налаштування → Ключі Content API і створіть ключ. Він має вигляд
pk_плюс 32 шістнадцяткові символи й прив’язаний саме до цього сайту.Новий ключ зазвичай працює одразу; у гіршому разі дайте йому кілька хвилин. Відкликання працює так само — відкликані ключі отримують
401 invalid_key.Зробіть перший запит
Запитайте сторінку
homeанглійською: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" } } } } } }Бачите шлях до заголовка?
content.hero.content.hero_title.content.en— сторінка → секція → блок → мова. Кожна сторінка має саме таку форму; подробиці — у розділі Сторінки.
Виведіть на сторінку
Той самий запит у чотирьох варіантах. Оберіть свій — усі роблять одне й те саме: отримують сторінку й вставляють заголовок в <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 — клієнтський компонент (Vite, CRA, будь-що)
import { useEffect, useState } from 'react';
const API = 'https://back.sitecog.com/content';
const KEY = import.meta.env.VITE_CRM_KEY; // публічний ключ, у браузері йому саме місце
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 — Server Component: виконується на сервері, ключ там і лишається
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 }, // звертаємося до API не частіше разу на хвилину
});
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>Де тримати ключ
Покладіть ключ у змінну оточення, а не в код, — не тому, що він секретний, а тому, що тоді зміна сайту чи ротація ключа — це правка одного рядка.
# .env.local (Next.js) — лише для сервера, у браузер не потрапляє
CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Коду в браузері потрібна публічна змінна. І це нормально: ключ лише для читання.
NEXT_PUBLIC_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Vite: VITE_CRM_KEY=… Nuxt: NUXT_PUBLIC_CRM_KEY=…У серверному компоненті (як у вкладці Next.js вище) використовуйте серверну змінну: ключ узагалі не покидає ваш сервер. Для коду в браузері публічна змінна цілком годиться.
Додайте хелпер для мов
Текстові блоки приходять як карти мов: { "en": "…", "de": "…" }. API ніколи не підміняє одну мову іншою: якщо перекладу немає, його просто немає (або це порожній рядок, якщо редактор залишив поле порожнім). Обирати запасну мову — вам, і цей крихітний хелпер робить це одним рядком:
// lib/t.js
// Обираємо переклад: запитана мова → запасна мова → перший непорожній → ''
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>;
// Обираємо переклад: запитана мова → запасна мова → перший непорожній → ''
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) || '';
}// Мова відвідувача й запасна мова — одним запитом
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'); // німецький ще не заповнили? → англійський текстКожен код у ?lang= має бути активною мовою сайту, інакше отримаєте 400 unknown_lang зі списком доступних. Не передасте lang — отримаєте всі активні мови одразу. Уся історія — у розділі Мови та запасна мова.
Увімкніть редагування на сайті
А тепер найцікавіше. Ваша сторінка вже показує вміст із CRM; дозвольмо редакторам змінювати його просто на сторінці, не шукаючи потрібне поле у формі.
Додайте віджет
Один скрипт, один раз на сторінку, просто перед
</body>. Він завантажує редактор лише тоді, коли сайт відкрито всередині CRM, тож звичайні відвідувачі не завантажують із нього ані байта.<script src="https://widget.sitecog.com/widget.js" defer></script>Підкажіть редактору, який елемент показує який блок
Додайте
data-crm-textіз маркером блоку до елемента, що його виводить. Текст і далі рендерте з API, як раніше, — атрибут лише пов’язує елемент із блоком.<body> <h1 data-crm-text="hero_title">Earbuds that mute the city</h1> <!-- один раз на сторінку, просто перед </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 — той самий рендер, що й раніше, плюс один атрибут return <h1 data-crm-text="hero_title">{hero.hero_title.content.en}</h1>;У зображень, відео, об’єктів і масивів свої атрибути (
data-crm-image,data-crm-video,data-crm-object,data-crm-array) — див. Віджет і розмітка. Для відвідувачів атрибути нешкідливі, тож лишайте їх і в продакшені.Дозвольте CRM вбудовувати ваш сайт у фрейм
Режим Live відкриває ваш сайт усередині CRM, в iframe. Ваш сервер має дозволити це через
frame-ancestorsі не надсилатиX-Frame-Options: DENYчиSAMEORIGIN. Інакше CRM повідомить, що сайт забороняє вбудовування.// next.config.js module.exports = { async headers() { return [{ source: '/:path*', headers: [ { key: 'Content-Security-Policy', value: "frame-ancestors 'self' https://sitecog.com" }, ], }]; }, };# nginx — і переконайтеся, що ніщо не надсилає X-Frame-Options: DENY чи SAMEORIGIN add_header Content-Security-Policy "frame-ancestors 'self' https://sitecog.com";Клацніть, введіть, збережіть
Відкрийте сайт у CRM у режимі Live, клацніть по заголовку, змініть його й збережіть. CRM записує блок, версія вмісту зростає, ваша сторінка отримує свіжий вміст — і новий заголовок уже на місці. 🎉
Бонус: якщо додати
data-crm-text="promo_note"на елемент ще до того, як такий блок з’явиться, редактор зможе створити блок просто із сайту.
Оминайте кеш для редакторів
Наш бік ніколи не віддає застарілий вміст у режимі Live. А от ваш кеш може: з revalidate: 60 редактор може зберегти зміни й ще до хвилини бачити старий текст. Усередині фрейму CRM до URL додається ?crm_live=1 — використайте його, щоб робити запит із cache: 'no-store':
// app/page.tsx — оминаємо будь-який кеш, поки дивиться редактор
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>;
}// Усередині фрейму CRM (або з ?crm_live=1) завжди запитуємо свіжий вміст
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',
});Що далі
Вміст надходить, редактори клацають. Ось куди копати глибше: