pagecraft-pages
Use this skill when the user asks you to build, list or publish any document in PageCraft — a page, a 4CMS template (catalog rubric, product card) or a layout document that wraps pages — by selecting blocks from the library, filling their placeholders via the V1 REST API, and optionally publishing to 4CMS. Trigger phrases include "сделай страницу", "собери лендинг", "напиши статью на сайте", "наполни рубрику", "создай страницу в PageCraft", "сгенерируй landing", "сделай шаблон рубрики", "сделай шаблон карточки товара", "собери макет сайта", "сделай макет с шапкой и подвалом", "какие есть шаблоны", "какие есть шаблоны рубрик", "какие есть типы шаблонов", "какие есть макеты", "покажи мои страницы", "наполни блоки контентом", "опубликуй страницу", "обнови опубликованную страницу", "обнови устаревшие блоки", "приведи страницу к актуальным версиям", "найди где используется блок", "мигрируй мажорную версию блока", "сделай аудит блоков на странице".
How do I install this agent skill?
npx skills add https://page-craft.4partners.io --skill pagecraft-pagesIs this agent skill safe to install?
No partner audit is available yet. Read the source before installing.
What does this agent skill do?
PageCraft Pages
Этот skill учит агента создавать любые страницы в PageCraft из готовых блоков библиотеки через V1 REST API и публиковать их в 4CMS. Тип страницы определяется тем, какие блоки выбирает пользователь — это может быть лендинг, статья, корзина, рубрика каталога, FAQ или что угодно ещё. Скил с этим типом не завязан.
Когда применять
- Пользователь просит собрать или наполнить любую страницу: лендинг, статью, рубрику, корзину, FAQ-страницу
- Пользователь даёт бриф/описание и просит «собрать страницу из блоков»
- Пользователь хочет наполнить уже выбранные блоки контентом
- Пользователь просит опубликовать или переопубликовать страницу в 4CMS
Жёсткие правила
-
Перед формированием
blocks[]вPOST /api/v1/pagesВСЕГДА вызывайnode scripts/blocks-schema.mjs <id>для каждого выбранного блока. Угадывать структуруplaceholder_valuesнельзя —schema_contentблока меняется от ревизии к ревизии. -
Перед первым
POST /api/v1/pagesобязательно прочитайexamples/payloads/about-page-full.jsonцеликом. Не угадывай форму корневого payload — поля называются именно так как там, вname/meta_*, а неtitle/meta:{...}. -
Если у блока непустой
binding_slots— для КАЖДОГО слота ВСЕГДА вызывайnode scripts/data-sources-get.mjs <source_id>(для всехsourceизallowedSources, которые рассматриваешь). Структуруparamsбери изparams_schemaответа — это JSON Schema, описывающая обязательные поля и типы. Если хочешь увидеть форму данных, которые источник вернёт в шаблон —node scripts/data-result-types-get.mjs <result_type_id>(там же естьstubс примером). -
Не выдумывай форматы, имена полей и допустимые значения. Всё, что тебе нужно для корректного payload, выложено в API:
- placeholder_values поля →
blocks-schema.mjs - bindings params →
data-sources-get.mjs <source_id>→params_schema - форма данных источника →
data-result-types-get.mjs <result_type_id> - визуальная ёмкость блока (см. ниже про Visual capacity) →
blocks-get.mjs <id>→template_content
Любая попытка вывести значение «из общего смысла» — это баг. Если сомневаешься — открой соответствующий endpoint.
- placeholder_values поля →
-
Блок с непустым
slotsвschema_content— контейнер. Вложить в него блок можно только отдельным вызовомpages-insert-blocksсparent_page_block_idиslot_nameу элементаblocks[]: вPOST /pagesиPUT /pages/{id}эти поля отклоняются (400), потому что контейнера в момент запроса ещё не существует. Порядок (position,order_index) считается внутри слота, а не по странице. Ограничения: вложенность на один уровень (блок в слоте своих детей иметь не может),maxу слота, имя слота — только изschema_content.slotsконтейнера. Нарушение — 422 с причиной.
5а. Документ kind = layout собирается из лэйаута. На верхнем уровне такого документа
допустим ровно один блок и только с role = "layout" (лэйаут). Обычные блоки
кладутся в слоты лэйаута отдельным вызовом pages-insert-blocks с parent_page_block_id
и slot_name. Что вернёт API при нарушении:
| Попытка | Ответ |
|---|---|
лэйаут на страницу с другим kind | 400, errors[].message: «лэйаут кладётся только в документ вида layout» |
| обычный блок на верхний уровень макета | 400: «на верхнем уровне документа-макета стоит только лэйаут» |
| второй лэйаут в тот же документ | 400: «в документе-макете лэйаут только один» |
| лэйаут в слот другого блока | 400: «лэйаут в слот не кладётся» |
publish макета без лэйаута | 422: «В документе-макете нет лэйаута…». Сам документ при этом сохранён — добавь лэйаут и публикуй снова |
publish макета, чей лэйаут без {{ cms_assets_head }} | 422: «В лэйауте … нет точки вставки ассетов витрины». Поправь шаблон лэйаута — точка ставится внутрь <head> |
Порядок работы — два вызова: лэйаут уезжает вместе с документом, а блоки в его слоты
отдельно, потому что parent_page_block_id становится известен только после того,
как лэйаут встал на страницу.
node scripts/pages-create.mjs layout-document.json # kind: "layout" + лэйаут в blocks[]; id лэйаута — в ответе
node scripts/pages-insert-blocks.mjs <page_id> layout-slot-blocks.json # блоки в слоты
node scripts/pages-publish.mjs <page_id> …
Готовые payload'ы — examples/payloads/layout-document.json (шаг 1)
и examples/payloads/layout-slot-blocks.json (шаг 2; parent_page_block_id в нём
надо заменить на id лэйаута из ответа шага 1).
Id лэйаута берётся из ответа на создание:
POST /pagesвозвращаетblocks[]с созданными записями. ОтдельныйGETдля этого не нужен.
Лэйаут ищется фильтром: node scripts/blocks-list.mjs --role layout. Нет подходящего —
его делает скилл pagecraft-block, а не этот.
Две вещи с похожим названием, не путать. Лэйаут — это блок-обёртка (
blocks-list.mjs --role layout), строительный материал. Макет — документ, в который этот блок положили (pages-list.mjs --kind layout). Просят «покажи макеты» — второе; «сделай макет» — сначала нужен лэйаут, потом документ.
- Если в брифе фигурируют локальные пути к ассетам (картинки, видео, PDF) — перед формированием
placeholder_valuesзапустиnode scripts/s3-upload.mjs <site_id> <file>из соседнего скиллаpagecraft-s3для каждого файла. Возвращённыйurlподставь в поля типаimage/urlвplaceholder_values. Не хардкодь локальные пути — на проде их не будет. Перед первой заливкой обязательно вызовиs3-status.mjs <site_id>— если S3 не подключён, сообщи пользователю и не пытайся продолжать. - Перед
publish/publish-updateВСЕГДА вызывайnode scripts/pages-publication-status.mjs <page_id>. В ответеis_publishedточно скажет какой эндпоинт использовать. Иначе при попытке опубликовать уже опубликованную страницу (или наоборот) API вернёт 400. Этот пинг бесплатный — он не ходит в 4CMS, просто читает локальное состояние. rubric_idsдля публикации НЕ ПРОКСИРУЮТСЯ этим API. Получай их напрямую из API 4partners.io (4CMS). Если пользователь не далrubric_idsдля первой публикации — спроси, не выдумывай.- Перед первым
POST /api/v1/pages/{id}/publishобязательно прочитайexamples/payloads/publish-page.json. Не угадывай форму payload — поля называются именно так как там. - Перед
pages-upgrade-allзапустиpages-get --with-blocksи прочитайupdate_statusвсех блоков. Это снимает сюрпризы — увидишь, что будет затронуто и сколькоmajor_blockedждёт ручной миграции. - На 422
major_upgrade_blockedНЕ ретраить upgrade. Это не временная ошибка. Только manual migration по Workflow 3. page-blocks-deleteиpage-blocks-replaceбез--yesвозвращают current state в stderr и просят подтверждения. Не подавляй это — сначала покажи пользователю, что будет удалено/затёрто. У контейнера в отказе естьnested_blocks: удаление уносит поддерево целиком, и человек должен увидеть его состав.replaceконтейнера с непустыми слотами вернёт 422 — сначала разберись с вложенными блоками.- После
replaceстарыйpbIdинвалидирован. Не используй прежний id для последующих вызовов — возьми новый id изresponse.page.blocksпоorder_index. - Все mutate-эндпоинты pageBlock возвращают
{ page: PagePublic }. Не делай лишний follow-uppages-get— данные уже у тебя в ответе. Ответpages-createтоже содержитblocks[]с id созданных записей.
Минимальный валидный payload для POST /api/v1/pages
Это абсолютный минимум — добавляй блоки и метаданные сверху, но никогда не переименовывай эти поля.
{
"site_id": 1,
"name": "Заголовок страницы",
"slug": "page-slug",
"blocks": []
}
Корневые поля payload — точные имена и типы:
| Поле | Тип | Обязательно | Заметка |
|---|---|---|---|
name | string | ✅ | Не title — именно name. |
site_id | integer | ✅ | Целое число, не строка. |
kind | string | опц. | Вид документа, по умолчанию article. См. раздел «Вид документа» ниже. |
slug | string | опц. | Уникальный в рамках сайта. Только для kind=article. |
folder_id | integer | опц. | ID папки внутри сайта. |
meta_title | string | опц. | Плоское поле верхнего уровня. Не вкладывать в meta: {...}. |
meta_description | string | опц. | Аналогично — плоское. |
meta_keywords | string | опц. | Аналогично — плоское. |
meta_robots | string | опц. | Аналогично — плоское. |
blocks | array | опц. | Массив PageBlockInput — см. шаги 3-5 ниже. |
Вид документа: kind
У документа есть неизменяемый вид, который задаётся при создании полем kind
и определяет, чем документ станет в 4CMS:
kind | Что это | Куда публикуется | slug и meta_* |
|---|---|---|---|
article (по умолчанию) | Страница со своим адресом на витрине | статья 4CMS (article-html) | свои, ведёт PageCraft |
id типа из справочника (rubric, product, …) | Шаблон страницы каталога, карточки товара и т.п. | шаблон 4CMS (doc_template) | нет, ведёт 4CMS |
layout | Макет: лэйаут-обёртка вокруг страниц | шаблон 4CMS (doc_template) | нет, ведёт 4CMS |
Правила выбора вида:
- Пользователь просит статью, лендинг, текстовую страницу —
kindне передавай вовсе, по умолчанию будетarticle. Это подавляющее большинство задач. - Пользователь просит оформить категорию каталога, карточку товара, поиск или бренд —
это шаблон. Сначала вызови
node scripts/page-types-list.mjsи возьмиidподходящего типа. Одного типа хватает на все страницы своего вида: страницы каталога, брендов и поиска обслуживает тип сcontexts, содержащимrubric/brand/search. - Тип из головы не выдумывай: допустимы только
idиз справочника — иначе API вернёт400 Недопустимый вид документа. Справочник может быть пуст; тогда доступны лишьarticleиlayout, и это надо сказать пользователю, а не подставлять случайный тип. - Вид потом не поменять:
PUT /pages/{id}с полемkindвернёт 400. Ошибся видом — создавай документ заново. - У шаблона нет своего адреса и SEO:
slug,meta_title,meta_description,meta_keywords,meta_robots,markdownдля не-articleотвергаются с 400, а не игнорируются. Не отправляй их.
Публикация шаблона проще, чем страницы: rubric_ids не нужен, тело можно передать пустым —
привязку шаблона к рубрикам ведёт 4CMS (см. раздел про публикацию).
Референс payload шаблона — examples/payloads/rubric-template.json.
Текстовый вариант страницы: markdown
У страницы (kind=article) есть второе представление содержимого — тот же материал
текстом, в markdown. Витрина отдаёт его по адресу статьи с суффиксом .md, чтобы
языковые модели читали текст, а не разметку блоков.
Из блоков он не собирается: что запишешь, то и уедет. Собрал или перебрал блоки — текстовый вариант обнови сам, иначе на витрине разойдутся две версии одной страницы.
| Действие | Как |
|---|---|
| Записать или переписать | pages-update.mjs <id> <payload.json> с полем markdown |
| Стереть | то же с "markdown": "" — вместе с текстом с витрины уйдёт и ссылка |
| Прочитать | node scripts/pages-get.mjs <id> --with-markdown |
| Узнать, есть ли он вообще | поле has_markdown — оно приходит всегда, в том числе в списках |
Полный текст в списках страниц не отдаётся никогда: у сайта сотни страниц, и текст
каждой превратил бы выдачу в мегабайты. При создании страницы поле не передаётся —
пиши его следующим PUT.
Payload для правки только текстового варианта:
{ "markdown": "# О компании\n\nМы делаем витрины с 2019 года." }
Ответить на вопрос «что уже есть»
Пользователь часто спрашивает не «сделай», а «покажи». Не собирай ответ обходом всех страниц — есть прямые фильтры:
| Вопрос пользователя | Команда |
|---|---|
| какие есть типы шаблонов, что вообще можно создать | node scripts/page-types-list.mjs |
| какие есть шаблоны | node scripts/pages-list.mjs --kind <тип> для нужных типов из справочника |
| какие есть шаблоны рубрик | node scripts/pages-list.mjs --kind rubric |
| какие есть макеты | node scripts/pages-list.mjs --kind layout |
| какие есть лэйауты (блоки-обёртки) | node scripts/blocks-list.mjs --role layout |
| какие есть обычные страницы | node scripts/pages-list.mjs --kind article |
Тип шаблона и шаблон — разные вещи. Тип (rubric, product, …) приходит
из справочника 4CMS и отвечает «что бывает». Шаблон — уже созданный документ этого
типа, «что есть у пользователя». На «какие есть типы шаблонов» отвечает
page-types-list, на «какие есть шаблоны» — pages-list --kind.
--kind принимает article, layout и любой id из справочника. Неизвестное
значение вернёт пустой список, а не ошибку, — поэтому пустой ответ проверяй
по справочнику, прежде чем говорить «таких нет».
Окружение
URL продакшен-API уже захардкожен в скиллов как https://page-craft.4partners.io/api/v1 — отдельно его задавать не надо. Пользователю достаточно сообщить API-ключ.
PAGECRAFT_API_KEY— обязательная переменная: API-ключpb_xxxx...(создаётся в UI PageCraft → API keys).PAGECRAFT_API_URL— опциональная: переопределяет базовый host. Использовать только если нужно протестировать скилл на другом окружении (напримерhttp://localhost:6767; префикс/api/v1добавляется автоматически). В обычной работе пропускать — скилл сам ходит на прод. Legacy-имяPAGE_CRAFT_API(host с/api/v1) ещё поддерживается как fallback.
Скрипты-обёртки (scripts/)
В скилле лежат готовые CLI-обёртки над каждым endpoint API — по одному файлу на операцию. Они экономят контекст: агент не пишет fetch/curl/URLSearchParams руками. Каждый скрипт читает PAGECRAFT_API_KEY/PAGECRAFT_API_URL из env и печатает JSON-ответ в stdout. При ошибке — non-zero exit + структурированный JSON в stderr.
| Команда | Endpoint | Когда вызывать |
|---|---|---|
node scripts/sites-list.mjs | GET /sites | Шаг 1 — выбрать сайт |
node scripts/collections-list.mjs [--scope global|private|all] | GET /collections | Шаг 2 — получить коллекции блоков (по умолчанию global) |
node scripts/collection-show.mjs --id <id> | GET /collections/{id} | Шаг 2б — список блоков выбранной коллекции |
node scripts/blocks-list.mjs --search hero [--role content|layout] | GET /blocks | Поиск блоков по name; --role layout — только лэйауты документов-макетов (для сборки обычных страниц не используется, см. ниже) |
node scripts/blocks-search.mjs --q "hero c CTA" | GET /search/blocks | Полнотекстовый поиск по блокам (для сборки страниц — не используется, см. ниже) |
node scripts/blocks-favorites.mjs | GET /blocks/favorites | Избранные блоки (для сборки страниц — не используется, см. ниже) |
node scripts/blocks-get.mjs <id> | GET /blocks/{id} | Посмотреть детали блока |
node scripts/blocks-schema.mjs <id> | GET /blocks/{id}/placeholder-schema | Шаг 3 — обязательно перед формированием placeholder_values |
node scripts/page-types-list.mjs | GET /page-types | Перед созданием шаблона — допустимые значения kind и их окружения |
node scripts/data-sources-list.mjs | GET /data-sources | Опционально для bindings |
node scripts/data-sources-get.mjs <source_id> | GET /data-sources/{id} | Получить params_schema источника |
node scripts/data-result-types-get.mjs <type_id> | GET /data-result-types/{id} | Опционально — увидеть форму результата |
node scripts/tags-list.mjs | GET /tags | Список тегов для фильтрации блоков |
node scripts/pages-create.mjs <payload.json> | POST /pages | Шаг 4 — создать страницу из payload |
node scripts/pages-list.mjs [--kind k] [--external_id N] [--bindingSource id] [--bindingValue v] | GET /pages | Список страниц с фильтрами: --kind — вид документа (article, layout, тип шаблона), точный lookup по external_id, поиск по графу биндингов |
node scripts/pages-search.mjs --q "время работы" | GET /search/pages | Полнотекстовый поиск по содержимому страниц (FTS, snippet с <mark>, морфология) |
node scripts/pages-get.mjs <id> [--with-blocks] [--with-markdown] | GET /pages/{id} | Шаг 5 — проверить результат; --with-markdown добавляет текстовый вариант страницы |
node scripts/pages-update.mjs <id> <payload.json> | PUT /pages/{id} | Перегенерировать страницу |
node scripts/pages-insert-blocks.mjs <id> <payload.json> | POST /pages/{id}/blocks | Дописать блоки в существующую |
node scripts/pages-delete.mjs <id> | DELETE /pages/{id} | Убрать тестовую страницу |
node scripts/pages-publication-status.mjs <id> | GET /pages/{id}/publication | Перед publish/publish-update — узнать, опубликована ли страница |
node scripts/pages-publish.mjs <id> <payload.json> | POST /pages/{id}/publish | Первая публикация. Только для неопубликованных страниц. |
node scripts/pages-publish-update.mjs <id> [payload.json] | POST /pages/{id}/publish-update | Переопубликовать уже опубликованную страницу. Payload опционален. |
node scripts/page-blocks-update.mjs <pageId> <pbId> <payload.json> | PUT /pages/{pageId}/blocks/{pbId} | Точечная правка placeholder_values одного блока на странице |
node scripts/page-blocks-delete.mjs <pageId> <pbId> [--yes] | DELETE /pages/{pageId}/blocks/{pbId} | Удалить блок-инстанс вместе с вложенными в его слоты блоками. Без --yes — отказ + current state и состав поддерева в stderr. Сжимает order_index внутри слота. |
node scripts/page-blocks-move.mjs <pageId> <pbId> --position N [--parent <pbId> --slot <name>] | PUT /pages/{pageId}/blocks/{pbId}/move | Переместить блок: 0-based позиция внутри своего уровня, --parent/--slot — в слот контейнера. Без них блок уезжает на верхний уровень |
node scripts/page-blocks-replace.mjs <pageId> <pbId> <payload.json> [--yes] | PUT /pages/{pageId}/blocks/{pbId}/replace | Заменить block_id (DELETE+INSERT). Новый pageBlock получает новый id |
node scripts/page-blocks-upgrade.mjs <pageId> <pbId> | POST /pages/{pageId}/blocks/{pbId}/upgrade | minor upgrade; 422 для major |
node scripts/pages-upgrade-all.mjs <pageId> | POST /pages/{pageId}/blocks/upgrade-all | bulk minor upgrade; major блоки в skipped_major |
node scripts/blocks-usages.mjs <blockId> [--outdated] [--limit N] [--offset N] | GET /blocks/{id}/usages | Найти все pageBlock, использующие блок (с фильтром устаревших) |
Полный список: ls scripts/. Все скрипты следуют одному паттерну, общий код в scripts/_api.mjs.
Медиа-файлы. Для загрузки локальных ассетов (картинки, видео, PDF) в S3 сайта используется отдельный скилл pagecraft-s3 со скриптами s3-status.mjs и s3-upload.mjs. Открой его SKILL.md для деталей.
Workflow
Шаг 1. Выбрать сайт
node scripts/sites-list.mjs
Запомни id нужного сайта.
Шаг 2. Выбрать коллекцию блоков
Блоки для сборки страницы выбираются через коллекции, не через прямой поиск по библиотеке.
Алгоритм:
- Получить глобальные коллекции:
node scripts/collections-list.mjs # --scope global (по умолчанию) | private | all - Если глобальная коллекция одна — берём её молча. Если несколько — спросить пользователя, из какой коллекции ставить блоки.
- Пользователь может явно указать свою личную коллекцию по
id(например, коллекцию своих приватных блоков). - Получить список блоков выбранной коллекции:
Ответ содержитnode scripts/collection-show.mjs --id <id>collection.blocks[]— это и есть набор блоков для работы. - Для каждого блока из коллекции, который планируется использовать, ОБЯЗАТЕЛЬНО вызывай
blocks-schema.mjsперед заполнениемplaceholder_values(жёсткое правило #1 — без изменений).
Примечание: скрипты
blocks-list.mjs,blocks-favorites.mjs,blocks-search.mjsостаются рабочими и доступны вscripts/. Скилл сборки страниц их для выбора блоков не использует — точкой входа являются коллекции. Если нужен выбор по избранному или своим блокам вне коллекции — это отдельный пользовательский сценарий, который не входит в данный скилл.
Шаг 3. Для КАЖДОГО выбранного блока получить контракт
node scripts/blocks-schema.mjs 3
Ответ содержит:
schema— JSON Schema (Draft-07) дляplaceholder_valuesexample— минимальный валидный examplebinding_slots— список SSR-слотов (пусто для статических блоков)
Шаг 4. Сформировать placeholder_values по контракту и брифу
Сначала посмотри поле block_type в ответе blocks-schema.mjs:
block_type === "schema"— обычный блок, заполняйplaceholder_valuesстрого по полямschema(см. список типов ниже).block_type === "markdown"— особый блок (WYSIWYG в UI). Используй зарезервированный ключ_markdownсо строкой Markdown — это основной контент блока. Поля изschemaостаются опциональными.
Не пропускай markdown-блоки — они обычные части страницы (тексты, описания, FAQ-разделы) и должны попадать в композицию.
block_type: "markdown" — пример payload
{
"block_id": 7,
"placeholder_values": {
"_markdown": "## О компании\n\nМы делаем сайты с 2018 года.\n\n- Лендинги за 72 часа\n- 50+ запущенных проектов\n- Конверсия от 12%"
}
}
_markdown — это обычная Markdown-строка (заголовки #, списки -/1., ссылки [text](url), выделение **bold**, картинки ). Сервер рендерит её в HTML на этапе публикации.
block_type: "schema" — соответствие типов полей значениям
text,textarea,url,email,image,icon,color,date,time,datetime→ строкаselect,radio→ строка изoptions[].valueswitch→ booleanobject→ вложенный объект поfieldsarray→ массив объектов поitem.fields
Шаг 5. Для блоков с binding_slots сформировать bindings
Сначала пойми что такое bindings в PageCraft:
- PageCraft мокает данные. Когда ты собираешь страницу в PageCraft, реальных товаров/рубрик/каталогов здесь нет. SSR-источники возвращают
stubизdata-result-types. То есть валидация «есть ли источник» в PageCraft бессмысленна — он всегда «есть», но он отдаёт mock. bindings.params— это декларация о production. Когда страница будет опубликована (POST /pages/{id}/publish), именно тогда source реально вызовется и вернёт настоящие данные. Параметры, которые ты сейчас укажешь вparams, описывают что должно прилететь на проде.- Defaults — это разумная заглушка, но не подмена твоей работы. Если оставить
defaultбезparams— на проде блок получит дефолтное количество элементов и дефолтный порядок. Часто это не то, что нужно для конкретной секции страницы.
Подбирай params бизнес-осмысленно по трём осям:
- Что брать —
filter/slug/category_id/etc. Конкретный ресурс на целевом сайте. Например для секции «Хиты женской обуви» —filter: { rubric: 'women-shoes' }, а не дефолт. - Сколько брать —
limit. Синхронизируй с visual capacity блока (см. ниже). - В каком порядке —
sort. Должен соответствовать смыслу секции: «хиты» →popular, «новинки» →newest, «акции» →discount.
Visual capacity блока. Liquid-шаблон в template_content определяет, сколько элементов реально нарисуется:
node scripts/blocks-get.mjs 38
# смотри template_content — там Liquid вида:
# {% for product in products limit:8 %} ... {% endfor %}
# либо фиксированный HTML с N <div class="product-slot"> подряд.
Если в params.limit 15, а template limit:8 — лишние 7 теряются. Если 3, а template ждёт 8 — половина блока пустая. Эти числа должны совпадать.
Алгоритм формирования bindings
5a. Посмотри слот в ответе blocks-schema.mjs. Элемент binding_slots выглядит так:
{
"name": "products",
"allowedSources": ["catalog.products", "partner_offers"],
"default": { "source": "catalog.products", "params": { "limit": 8 } }
}
name — ключ в bindings. allowedSources — допустимые ID источников. default — что подставится без явного bindings.
5b. ОБЯЗАТЕЛЬНО получи определение источника (для каждого source, который рассматриваешь). Это правило #3 — без него ты не знаешь точные имена и типы params:
node scripts/data-sources-get.mjs catalog.products
В ответе:
id— ID источника.result_type_id— ID типа данных, который вернёт источник.title,description— назначение источника (помогает понять, подходит ли он для этой секции).params_schema— JSON Schema параметров запроса. Это единственный референс для params. Имена ключей вpropertiesсовпадают с теми, что ты должен положить вparams. Типы, форматы,enum,descriptionкаждого param — оттуда же.context— окружения, из которых источник берёт данные. Пусто — источник годится для любого документа. Непусто — см. ниже.
Список всех доступных источников: node scripts/data-sources-list.mjs.
Контекстные источники (context непустой). Такой источник берёт данные той сущности,
которую показывают: rubrics.current — текущую рубрику, catalog.current_products —
товары текущего листинга. Его можно привязать только на документе, чей вид даёт нужное
окружение: список окружений вида — node scripts/page-types-list.mjs (поле contexts),
вид документа — kind страницы. У документа kind = layout фильтра нет: куда назначат
макет, знает 4CMS, а не PageCraft.
Правила:
- в
allowedSourcesслота контекстных источников нет — там перечислены только те, что работают везде. Контекстный источник указывается вbindingsвместо значения изallowedSources, если тип результата слота совпадает сresult_type_idисточника; - привязка контекстного источника на неподходящем документе (например
catalog.current_productsна странице видаarticle) отклоняется с 400 и текстом причины вerrors[].message; - если окружение исчезло у вида документа позже, слот приезжает в шаблон как
null— страница не падает, но блок покажет пустое состояние.
5c. ОБЯЗАТЕЛЬНО получи форму результата. Это даёт понимание, какую структуру получит Liquid-шаблон блока:
node scripts/data-result-types-get.mjs offer_list
В ответе:
schema_json— JSON Schema формы результата (массив объектов, объект, и т.д.).stub— готовый пример того, что вернётся в PageCraft при превью (и что вернёт source в production, по той же форме).
Список всех типов результатов: node scripts/data-result-types-list.mjs.
5d. Сверь visual capacity блока с params.limit. Если в template_content {% for ... limit:N %} — твой params.limit должен совпадать с N.
5e. Собери bindings с реальными бизнес-параметрами:
{
"bindings": {
"products": {
"source": "catalog.products",
"params": {
"limit": 8,
"sort": "popular",
"filter": { "rubric": "women-shoes" }
}
}
}
}
Когда можно опустить bindings
Только если default слота семантически точно подходит для этой секции страницы и его params.limit совпадает с visual capacity блока. В сомнительных случаях лучше прописать явно.
Шаг 6. Создать страницу
Сохрани собранный payload в файл (например /tmp/page.json), потом:
node scripts/pages-create.mjs /tmp/page.json
Скрипт распечатает в stdout созданную страницу с id. При ошибке валидации он печатает { error, errors:[{path,message}] } в stderr и выходит с non-zero. Структуру payload см. в examples/payloads/about-page-full.json.
Шаг 7. Если non-zero exit и errors[] в stderr — исправить и ретраить
{
"error": "Ошибка валидации блоков",
"errors": [
{ "path": "blocks[0].placeholder_values.title", "message": "поле обязательно" },
{ "path": "site_id", "message": "This value should be of type int." }
]
}
path — dot-нотация (blocks[0].placeholder_values.title) для блочной валидации, либо имя корневого поля (site_id, name) для type-mismatch. Найди это место в payload, исправь по message и повтори POST.
Типичные ошибки на корневом payload:
site_id:should be of type int→ передал строку вместо числа.name:should not be blank→ передал пустую строку или забыл поле.- Поле не распознано (например
titleвместоname) — Symfony может вернуть общий 422; перечитай таблицу полей выше.
Шаг 8. Дополнительные операции
| Действие | Скрипт | Когда использовать |
|---|---|---|
| Удалить тестовую страницу | node scripts/pages-delete.mjs <id> | После проб/экспериментов — убрать за собой. |
| Полностью заменить блоки | node scripts/pages-update.mjs <id> <payload.json> | Перегенерировать страницу с новым набором блоков. |
| Добавить блоки | node scripts/pages-insert-blocks.mjs <id> <payload.json> | Дописать секции, не трогая остальное. Поддерживает position в payload. |
| Поменять только метаданные | pages-update.mjs с payload без blocks | Обновить только SEO/имя — массив блоков останется как был. |
| Поменять текстовый вариант | pages-update.mjs с payload из одного поля markdown | Обновить текст для языковых моделей; "" его стирает. |
Шаг 9. Финальная проверка
node scripts/pages-get.mjs <id> --with-blocks
Публикация страницы
Страница в PageCraft и опубликованная статья в 4CMS — разные сущности. После POST /pages страница существует только внутри PageCraft. Чтобы она появилась на конечном сайте, нужно её опубликовать.
Два сценария — выбираются по is_published
is_published | Эндпоинт | Что делает |
|---|---|---|
false | POST /pages/{id}/publish | Создаёт сущность в 4CMS, проставляет странице external_id. Для страницы требует rubric_ids[], для шаблона тело пустое. |
true | POST /pages/{id}/publish-update | Перегенерирует HTML/CSS/JS и обновляет статью по сохранённому external_id. Тело опционально. |
Перепутать нельзя — у каждого эндпоинта валидация на это состояние и он вернёт 400.
Workflow публикации
Шаг П1. Узнать статус публикации.
node scripts/pages-publication-status.mjs 123
# → { "is_published": false, "external_id": null, "external_slug": null, "published_at": null, "updated_at": "..." }
Шаг П2a. Если is_published === false — первая публикация.
Подготовь payload по examples/payloads/publish-page.json. Обязательное поле — rubric_ids (массив ID рубрик в 4CMS, минимум один). Опциональные: name, meta_title, meta_description, meta_keywords, meta_robots, slug, preview_text.
Если у страницы kind ≠ article (шаблон или макет) — публикация идёт в отдельную сущность 4CMS doc_template, и payload не нужен вовсе:
node scripts/pages-publish.mjs 123 examples/payloads/publish-template.json # тело {}
rubric_ids, slug и meta_* шаблону не передаются: адрес, SEO и привязку к рубрикам ведёт 4CMS. Обновление шаблона — тот же pages-publish-update.mjs без payload.
node scripts/pages-publish.mjs 123 /tmp/publish-payload.json
# → { "is_published": true, "external_id": 4567, "external_slug": "my-article", "published_at": "...", "updated_at": "..." }
rubric_ids берутся из API 4partners.io (4CMS) — этот скилл их не проксирует. Если пользователь не дал — спроси у него явно, не выдумывай.
Шаг П2b. Если is_published === true — обновление.
# обновить только контент блоков (без изменения метаданных в 4CMS)
node scripts/pages-publish-update.mjs 123
# обновить контент + что-то из метаданных
node scripts/pages-publish-update.mjs 123 /tmp/publish-payload.json
В payload опционально кладёшь только те поля, которые нужно поменять: rubric_ids, name, meta_*, slug, preview_text. Поля, которых нет в payload, в 4CMS не меняются.
Что может пойти не так
400 Page already published. Use publish-update instead.— забыл шаг П1 и вызвалpublishдля опубликованной страницы.400 Page not published yet. Use publish first.— наоборот,publish-updateна ещё не опубликованной.400 Page has no associated site— у страницы нетsite_id. Сначала привяжи сайт черезPUT /pages/{id}.404 Page not found— страница чужая или удалена.502 4CMS API error: ...— провайдер вернул ошибку. Тело содержит сообщение от 4CMS — покажи его пользователю как есть.
Поиск и обратные связи
PageCraft предоставляет два класса поисковых эндпоинтов — полнотекстовый и структурный.
Полнотекстовый поиск (FTS)
Использует PostgreSQL tsvector + pg_trgm (для опечаток). Поддерживает русскую морфологию. Возвращает score и HTML-snippet с <mark>…</mark>.
node scripts/pages-search.mjs --q "время работы" --limit 10
node scripts/blocks-search.mjs --q "карточка hero" --tags 1,5 --scope global
Лаг индексации страниц — до 5 секунд после правки (worker подхватывает изменения отложенно через search_dirty_at). Если только что обновил страницу — подожди.
Не ищет по динамическим данным с binding-источников (рубрики, товары) — этих данных у PageCraft нет, они резолвятся на стороне сайта-приёмника. Только метаданные страницы + статический контент блоков.
Структурный поиск по биндингам
Когда нужно найти страницы, ссылающиеся на конкретную сущность (рубрику, ID товара, slug) — используй фильтры pages-list.mjs:
# Все страницы, использующие источник "rubric_by_slug"
node scripts/pages-list.mjs --bindingSource rubric_by_slug
# Все страницы, в биндингах которых есть значение "computers"
node scripts/pages-list.mjs --bindingValue computers
# Точно: страницы с биндингом rubric_by_slug → slug=computers
node scripts/pages-list.mjs --bindingSource rubric_by_slug --bindingParamName slug --bindingValue computers
Полезно для:
- impact-анализа: «какие страницы сломаются, если убрать источник X»
- массового обновления: «рубрика переименована — найти и пересохранить все страницы, ссылающиеся на старый slug»
- inventory: «все страницы, использующие конкретный источник»
Lookup по external_id
Если у страницы есть внешний идентификатор (external_id — ID статьи в 4CMS), используй точечный поиск:
node scripts/pages-list.mjs --external_id 4567
O(1) через partial index. Возвращает максимум одну страницу для текущего пользователя.
Что НЕ делает этот skill
- Не отдаёт список рубрик 4CMS.
rubric_idsдляpublishпользователь приносит из API 4partners.io сам. - Не создаёт блоки/сайты/папки. Эти ресурсы готовятся пользователем заранее.
Примеры payload'ов
examples/payloads/about-page-full.json— референс корневого payloadPOST /pages. Прочитай в первую очередь — там точные имена обязательных полей (name, плоскиеmeta_*).examples/payloads/hero-block.json— пример заполненного hero-блока (толькоplaceholder_values).examples/payloads/features-block.json— то же для features-блока.examples/payloads/publish-page.json— референс payloadPOST /pages/{id}/publish. Точные имена полей для публикации (rubric_ids, плоскиеmeta_*,slug,preview_text).examples/payloads/rubric-template.json— референс payload шаблона (kindиз справочника, безslugиmeta_*).examples/payloads/layout-document.json— референс payload документа-макета (kind: "layout"+ лэйаут вblocks[], безslugиmeta_*).examples/payloads/layout-slot-blocks.json— вставка блоков в слоты лэйаута (parent_page_block_id+slot_name).examples/payloads/publish-template.json— пустое тело публикации шаблона.
Аудит и обновление версий блоков
PageCraft версионирует блоки типов schema и markdown. PageBlock пинится к ревизии, и при обновлении блока в библиотеке версия pageBlock остаётся прежней — пока его явно не «апгрейднешь».
В ответе pages-get --with-blocks каждый pageBlock содержит:
block_version_major/minor— pin (что закреплено на странице)block.version_major/minor— текущая ревизия библиотекиupdate_status—up_to_date|minor_available|major_blocked
Workflow 1. Аудит сайта + bulk minor upgrade
# 1. Найти страницы, требующие обновления
node scripts/pages-list.mjs --has-outdated --limit 50
# 2. Для каждой — bulk-apply minor
for id in 42 43 51 60; do
node scripts/pages-upgrade-all.mjs $id | jq '{page: '$id', upgraded: (.upgraded | length), skipped: (.skipped_major | length)}'
done
Собирай skipped_major для следующего workflow.
Workflow 2. Точечная правка контента одного блока
# 1. Найти pbId нужного блока
node scripts/pages-get.mjs 42 --with-blocks | jq '.blocks[] | {id, block_id, block_name: .block.name, placeholder_values}'
# 2. Применить правку
echo '{"placeholder_values":{"title":"Новый заголовок","subtitle":"..."}}' > /tmp/p.json
node scripts/page-blocks-update.mjs 42 12 /tmp/p.json
Workflow 3. Major-миграция (breaking change в блоке)
Если pageBlock попал в skipped_major или page-blocks-upgrade вернул 422 major_upgrade_blocked:
-
Прочитай новую схему блока:
node scripts/blocks-schema.mjs 7 -
Собери новые
placeholder_valuesпо новой схеме, перенося значения из старых полей.removed_fieldsиз ответа 422 /skipped_majorподскажет, какие поля удалены. -
Вставь новый pageBlock на ту же позицию:
OLD_ORDER=$(node scripts/pages-get.mjs 42 --with-blocks | jq '.blocks[] | select(.id == 13) | .order_index') cat > /tmp/insert.json <<EOF { "blocks": [ {"block_id": 7, "placeholder_values": { /* новые поля */ }} ], "position": $OLD_ORDER } EOF node scripts/pages-insert-blocks.mjs 42 /tmp/insert.json -
Удали старый pageBlock:
node scripts/page-blocks-delete.mjs 42 13 --yes
Альтернатива — page-blocks-replace одним шагом, если позиция и order устраивают.
Workflow 4. После редактирования блока в библиотеке — найти и апгрейднуть pageBlocks
# Найти все pageBlock, отстающие от текущей библиотеки
node scripts/blocks-usages.mjs 3 --outdated
# Обновить minor-готовые: проходим по уникальным page_id
PAGES=$(node scripts/blocks-usages.mjs 3 --outdated --limit 200 | jq -r '[.data[].page_id] | unique | .[]')
for id in $PAGES; do
node scripts/pages-upgrade-all.mjs $id
done
Дополнительные ресурсы
- Glossary — словарь домена
- Page factory playbook — расширенный workflow
- Swagger UI — полная справка API
- llms.txt — индекс для агентов
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/page-craft.4partners.io/pagecraft-pages">View pagecraft-pages on skillZs</a>