Пять минут, одна страница и ни одной самописной админки. К концу этого руководства заголовок на вашем сайте будет приходить из CRM Diil, а редакторы смогут поменять его, просто кликнув по нему. Наливайте кофе — допить, возможно, не успеете.
Понадобится:
- доступ к сайту в CRM Diil (для опытов можно завести отдельный);
- терминал с
curlили любой другой способ отправить HTTP-запрос; - любой фронтенд: обычный HTML-файл, React, Next.js, Nuxt — как вам удобнее.
Контент и ключ
Заведите контент в CRM
Откройте сайт в CRM и проверьте, что нужные языки добавлены (например, английский). Затем создайте:
- страницу с маркером
home; - в ней — секцию с маркером
hero; - в секции — текстовый блок с маркером
hero_title, и впишите в него заголовок.
Маркер — это имя, по которому ваш код находит контент: латиница, цифры и подчёркивание, от 2 до 40 символов, регистр важен. Выбирайте их как имена переменных — жить в коде им долго. Как всё устроено целиком — в разделе «Как устроен контент».
- страницу с маркером
Получите ключ сайта
Откройте Настройки → Ключи контентного API и создайте ключ. Он выглядит как
pk_и 32 шестнадцатеричных символа и привязан к этому сайту.Обычно новый ключ работает сразу, в худшем случае — через несколько минут. С отзывом так же: на отозванный ключ API отвечает
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": "Главная", "href": "/", "index": 0, "params": {}, "content": { "hero": { "id": 41, "marker": "hero", "name": "Первый экран", "index": 0, "show": true, "content": { "hero_title": { "id": 95, "marker": "hero_title", "name": "Заголовок первого экрана", "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 — серверный компонент: выполняется на сервере, ключ остаётся там же
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="ru"> <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"на элемент раньше, чем такой блок появился в CRM, редактор сможет создать блок прямо с сайта.
Без кэша для редакторов
С нашей стороны в режиме Live устаревшего контента не бывает. А вот ваш кэш может подвести: с revalidate: 60 редактор сохранит правку и ещё до минуты будет видеть старый текст. Внутри рамки CRM к адресу добавляется ?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',
});Что дальше
Контент приходит, редакторы кликают. Вот куда копать дальше: