Playbook: фабрика страниц
Рецепт собирает любую страницу из готовых блоков: лендинг, статью, рубрику каталога, корзину, FAQ. Тип страницы определяется набором выбранных блоков — сам рецепт универсален.
Годится и человеку со curl, и ИИ-агенту. Агент, поставивший
скиллы, делает всё это сам; здесь — что именно он делает.
Подготовка
Заголовок раздела «Подготовка»export API='https://page-craft.4partners.io/api/v1'export KEY='pb_a1b2c3d4e5f6...'Нужны curl и jq. Ключ создаётся по инструкции
API-ключи.
Контракт POST /pages — прочитать до первого запроса
Заголовок раздела «Контракт POST /pages — прочитать до первого запроса»Половина ошибок здесь — не в содержимом, а в именах полей.
| Поле | Тип | Обяз. | Заметка |
|---|---|---|---|
name |
string | да | не title — именно name |
site_id |
integer | да | число, не строка |
kind |
string | нет | вид документа, по умолчанию article; список типов — GET /page-types. Неизменяем после создания |
slug |
string | нет | уникален в пределах сайта; только у kind=article |
folder_id |
integer | нет | ID папки внутри сайта |
meta_title |
string | нет | плоское поле верхнего уровня, не meta: { title } |
meta_description |
string | нет | так же плоское |
meta_keywords |
string | нет | так же плоское |
meta_robots |
string | нет | так же плоское |
blocks |
array | нет | элементы — см. шаг 4 |
Минимальный валидный payload:
{ "site_id": 1, "name": "Заголовок", "slug": "page-slug", "blocks": [] }Шаг 1. Выбрать сайт
Заголовок раздела «Шаг 1. Выбрать сайт»curl -s -H "Authorization: Bearer $KEY" "$API/sites?limit=50" | jq '.data[] | {id, name}'Идентификатор нужного сайта — в $SITE.
Шаг 2. Найти блоки
Заголовок раздела «Шаг 2. Найти блоки»# поиском по названию и описаниюcurl -s -H "Authorization: Bearer $KEY" "$API/blocks?search=hero&limit=20" \ | jq '.data[] | {id, name, description}'
# или по тегамcurl -s -H "Authorization: Bearer $KEY" "$API/tags?limit=50" | jq '.data[] | {id, name}'curl -s -H "Authorization: Bearer $KEY" "$API/blocks?tags=1,3&limit=20" | jq '.data[] | {id, name}'Полнотекстовый поиск с ранжированием — GET /search/blocks?q=… (запрос
от двух символов).
Шаг 3. Взять контракт каждого блока
Заголовок раздела «Шаг 3. Взять контракт каждого блока»Критичный шаг, пропускать нельзя. Контракт говорит, какие поля у блока есть и какого они типа.
for BID in 3 7 12; do echo "=== Block $BID ===" curl -s -H "Authorization: Bearer $KEY" "$API/blocks/$BID/placeholder-schema" \ | jq '{block_type, schema, example, binding_slots}'done| Поле ответа | Что означает |
|---|---|
block_type |
schema — обычный блок; markdown — WYSIWYG-блок |
schema |
JSON Schema (Draft-07) для placeholder_values |
example |
минимальный валидный пример из значений по умолчанию — можно отправить как есть |
binding_slots |
слоты живых данных с полями name, allowed_sources, default, result_type; пустой массив у статических блоков |
Шаг 4. Заполнить placeholder_values
Заголовок раздела «Шаг 4. Заполнить placeholder_values»Структура зависит от block_type.
block_type: "schema" — обычный блок
Заголовок раздела «block_type: "schema" — обычный блок»Поля заполняются строго по schema:
{ "block_id": 3, "placeholder_values": { "title": "Современный сайт для вашего бизнеса", "subtitle": "Запустим лендинг за 3 дня", "cta_label": "Оставить заявку", "cta_url": "#contact" }}Соответствие типов полей формату значения разобрано на странице Схема полей. Поля, которых нет в схеме, но которые использует Liquid-шаблон, тоже допустимы — валидация их не отклоняет.
block_type: "markdown" — WYSIWYG-блок
Заголовок раздела «block_type: "markdown" — WYSIWYG-блок»Основной контент кладётся в зарезервированный ключ _markdown; поля схемы
остаются опциональными и отвечают за оформление.
{ "block_id": 7, "placeholder_values": { "_markdown": "## О компании\n\nМы делаем сайты с 2018 года.\n\n- Лендинги за 72 часа\n- 50+ проектов" }}Формат — GFM (GitHub Flavored Markdown): заголовки #, списки, ссылки,
**bold**, ~~strike~~, картинки, цитаты, блоки кода и таблицы:
| Заголовок 1 | Заголовок 2 || ----------- | ----------- || Ячейка 1 | Ячейка 2 |У таблицы обязательна строка-шапка и разделитель --- под ней; перенос строки
внутри ячейки — <br>. Сервер конвертирует Markdown в HTML при публикации.
Подробнее — Markdown в блоках.
Блоки с живыми данными
Заголовок раздела «Блоки с живыми данными»Если binding_slots непустой, слоту назначается источник. PageCraft подставляет
в предпросмотр заглушки (stub), поэтому «есть ли данные» проверять негде —
важно правильно описать, что должно прилететь на витрине. Значения слотов
по умолчанию — разумная заглушка, но не замена осмысленному выбору params.
{ "block_id": 12, "placeholder_values": { "headline": "Свяжитесь с нами" }, "bindings": { "products": { "source": "catalog.products", "params": { "limit": 8, "sort": "popular", "filter": { "rubric": "women-shoes" } } } }}Для каждого слота проходятся три запроса — это не опция.
1. Определение источника (для каждого значения из allowed_sources):
curl -s -H "Authorization: Bearer $KEY" "$API/data-sources/catalog.products" | jq '.'В ответе: id, title, description — назначение источника; result_type_id —
идентификатор формы данных; params_schema — JSON Schema объекта params.
Ключи properties — ровно то, что кладётся в params. Не угадывайте.
Все источники: GET /data-sources?limit=50.
2. Форма результата — что источник передаст в шаблон:
curl -s -H "Authorization: Bearer $KEY" "$API/data-result-types/offer_list" | jq '{schema_json, stub}'schema_json — JSON Schema результата, stub — готовый пример. В предпросмотре
подставляется именно stub, на витрине — реальные данные той же формы.
Список всех типов: GET /data-result-types?limit=50.
3. Вместимость блока против params.limit:
curl -s -H "Authorization: Bearer $KEY" "$API/blocks/12" | jq -r '.current_revision.template_content'В шаблоне ищется конструкция вывода ({% for item in products limit: 8 %}) или
фиксированное число слотов в разметке. params.limit должен совпадать с ним:
limit: 15 при limit: 8 в шаблоне — семь товаров потеряны; limit: 3
при восьми слотах — полблока пустое.
Три оси подбора params:
- Что брать —
filter,slug,category_id. Конкретный ресурс под смысл секции, а не «всё подряд». - Сколько брать —
limit, синхронизированный с вместимостью блока. - В каком порядке —
sortпо смыслу секции: «хиты» →popular, «новинки» →newest, «акции» →discount.
Разбор слотов и источников целиком — Биндинги и слоты.
Шаг 5. Создать страницу
Заголовок раздела «Шаг 5. Создать страницу»cat > /tmp/page-payload.json <<EOF{ "name": "Страница нового продукта", "site_id": $SITE, "slug": "new-product-2026", "meta_title": "Новый продукт — лучшее решение", "meta_description": "Краткое описание для поисковиков.", "blocks": [ { "block_id": 3, "placeholder_values": { } }, { "block_id": 7, "placeholder_values": { } }, { "block_id": 12, "placeholder_values": { }, "bindings": { } } ]}EOF
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ --data-binary @/tmp/page-payload.json "$API/pages" | jq '.'Порядок элементов blocks[] задаёт порядок блоков на странице. Успех — 201
с созданной страницей; её id пригодится дальше как $PID. Ответ разворачивает
blocks[] с id созданных вхождений — они нужны, если дальше вы кладёте блоки
в слоты (parent_page_block_id), и отдельный запрос за ними не требуется.
Шаг 6. Разобрать ошибку валидации
Заголовок раздела «Шаг 6. Разобрать ошибку валидации»Не создалась — в ответе errors[]:
{ "error": "Ошибка валидации блоков", "errors": [ { "path": "blocks[0].placeholder_values.title", "message": "поле обязательно" }, { "path": "blocks[2].bindings.products.source", "message": "источник не разрешён для слота" }, { "path": "site_id", "message": "This value should be of type int." } ]}Как чинить:
- Найдите место в payload по
path— dot-путьblocks[0].placeholder_values.titleдля блочной валидации, имя корневого поля (site_id,name,slug) для ошибок верхнего уровня. - Исправьте по
message. Типичное: «поле обязательно» — добавить поле; «источник не разрешён» — свериться сbinding_slots[*].allowed_sources;should be of type int/string— привести тип; «should not be blank» — заполнить. - Повторите запрос.
Отдельный случай — 409: slug уже занят другой страницей этого сайта.
В errors[] придёт запись с path: "slug"; поменяйте slug и повторите.
Шаг 7. Разобрать 404
Заголовок раздела «Шаг 7. Разобрать 404»404 с текстом вида blocks[i]: блок N не найден означает, что блок удалён или
недоступен вашему ключу. Подберите замену через GET /blocks?search=…
и повторите шаги 3–5.
Шаг 8. Проверить результат
Заголовок раздела «Шаг 8. Проверить результат»curl -s -H "Authorization: Bearer $KEY" "$API/pages/$PID?with_blocks=1" \ | jq '{id, name, blocks: [.blocks[] | {block_id: .block.id, name: .block.name, order_index, placeholder_values}]}'Без with_blocks=1 поле blocks равно null — это не пустая страница,
а «блоки не запрашивались». Правило про null касается только GET: ответы
на создание и на изменяющие операции разворачивают блоки всегда.
Шаг 9. Правки и уборка
Заголовок раздела «Шаг 9. Правки и уборка»| Действие | Эндпоинт |
|---|---|
| Заменить состав блоков целиком | PUT /pages/{id} с blocks |
| Изменить только метаданные | PUT /pages/{id} без blocks |
Дописать блоки (опционально с position) |
POST /pages/{id}/blocks |
| Поправить значения одного блока | PUT /pages/{pageId}/blocks/{pbId} |
| Переставить блок | PUT /pages/{pageId}/blocks/{pbId}/move |
| Заменить блок другим | PUT /pages/{pageId}/blocks/{pbId}/replace |
| Снять блок со страницы | DELETE /pages/{pageId}/blocks/{pbId} |
| Подтянуть minor-версии всех блоков | POST /pages/{pageId}/blocks/upgrade-all |
| Удалить страницу | DELETE /pages/{id} → 204 |
Обновление до major-версии блока не выполняется автоматически: upgrade вернёт
422 с error: "major_upgrade_blocked", списком удалённых полей и текущей
привязкой версии. Это не сбой, а требование перенести значения вручную —
см. Устаревание и апгрейды.
Шаг 10. Опубликовать на витрину
Заголовок раздела «Шаг 10. Опубликовать на витрину»Страница в PageCraft и статья на витрине — разные сущности. Пока страница не опубликована, её никто, кроме вас, не видит.
10a. Узнать статус. Запрос локальный, во внешний API не ходит:
curl -s -H "Authorization: Bearer $KEY" "$API/pages/$PID/publication" | jq '.'# → { "is_published": false, "external_id": null, "external_slug": null,# "published_at": null, "updated_at": "..." }10b. is_published: false — первая публикация:
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{ "rubric_ids": [42], "meta_title": "Новый продукт | Мой сайт", "meta_description": "Краткое описание для поисковиков.", "slug": "new-product", "preview_text": "Анонс для списка статей." }' "$API/pages/$PID/publish" | jq '.'Обязательное поле одно — rubric_ids (минимум один идентификатор рубрики 4CMS).
Остальные опциональны; name по умолчанию берётся из name страницы.
У документа с kind ≠ article (шаблон, макет) тело публикации пустое — {}:
rubric_ids, slug и meta_* шаблону не передаются, их ведёт 4CMS. Подробности —
Публикация и обновление.
Список рубрик это API не возвращает — rubric_ids берутся напрямую из API
4partners.io.
10c. is_published: true — обновление опубликованной статьи:
# перегенерировать HTML/CSS/JS блоков, метаданные на витрине оставить как естьcurl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{}' "$API/pages/$PID/publish-update" | jq '.'
# или поменять часть метаданныхcurl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"meta_title":"Новый заголовок","rubric_ids":[42,51]}' \ "$API/pages/$PID/publish-update" | jq '.'Оба эндпоинта отвечают тем же объектом статуса, что и GET /pages/{id}/publication.
Ошибки публикации:
| Код | Причина |
|---|---|
400 |
вызван publish на уже опубликованной странице — нужен publish-update |
400 |
вызван publish-update на неопубликованной — нужен publish |
400 |
у страницы нет сайта — сначала PUT /pages/{id} с site_id |
404 |
страница не найдена или принадлежит другому пользователю |
502 |
ошибка провайдера (4CMS): неверный токен, несуществующая рубрика и подобное |
Чеклист
Заголовок раздела «Чеклист»- сайт выбран,
site_idизвестен - блоки отобраны
- для каждого вызван
GET /blocks/{id}/placeholder-schema -
placeholder_valuesзаполнены поschema -
bindingsзаданы только для блоков с непустымbinding_slots - по каждому слоту пройдены
data-sourcesиdata-result-types -
params.limitсверен с вместимостью шаблона -
POST /pagesотправлен сsite_id -
errors[]разобраны, запрос повторён -
GET /pages/{id}?with_blocks=1подтверждает состав - перед публикацией вызван
GET /pages/{id}/publication - при первой публикации передан непустой
rubric_ids[]
Источники
Заголовок раздела «Источники»openapi/v1.json— пути, коды ответов, схемыCreatePagePublicRequest,PageBlockInput,BlockPlaceholderSchemaResponse,PublishPagePublicRequest,PagePublicationStatusResponse,MajorUpgradeBlockedErrorbackend/src/Controller/V1/Page/**,backend/src/Controller/V1/PageBlock/**backend/src/Exception/BlockPayloadValidationException.php,backend/src/Exception/SlugConflictException.php- предшественник страницы:
backend/public/docs/agents/playbooks/page-factory.md