skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
page-craft.4partners.io110 installs

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-pages
view source ↗

Is 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

Жёсткие правила

  1. Перед формированием blocks[] в POST /api/v1/pages ВСЕГДА вызывай node scripts/blocks-schema.mjs <id> для каждого выбранного блока. Угадывать структуру placeholder_values нельзя — schema_content блока меняется от ревизии к ревизии.

  2. Перед первым POST /api/v1/pages обязательно прочитай examples/payloads/about-page-full.json целиком. Не угадывай форму корневого payload — поля называются именно так как там, в name/meta_*, а не title/meta:{...}.

  3. Если у блока непустой 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 с примером).

  4. Не выдумывай форматы, имена полей и допустимые значения. Всё, что тебе нужно для корректного 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.

  5. Блок с непустым 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 при нарушении:

ПопыткаОтвет
лэйаут на страницу с другим kind400, 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). Просят «покажи макеты» — второе; «сделай макет» — сначала нужен лэйаут, потом документ.

  1. Если в брифе фигурируют локальные пути к ассетам (картинки, видео, PDF) — перед формированием placeholder_values запусти node scripts/s3-upload.mjs <site_id> <file> из соседнего скилла pagecraft-s3 для каждого файла. Возвращённый url подставь в поля типа image/url в placeholder_values. Не хардкодь локальные пути — на проде их не будет. Перед первой заливкой обязательно вызови s3-status.mjs <site_id> — если S3 не подключён, сообщи пользователю и не пытайся продолжать.
  2. Перед publish/publish-update ВСЕГДА вызывай node scripts/pages-publication-status.mjs <page_id>. В ответе is_published точно скажет какой эндпоинт использовать. Иначе при попытке опубликовать уже опубликованную страницу (или наоборот) API вернёт 400. Этот пинг бесплатный — он не ходит в 4CMS, просто читает локальное состояние.
  3. rubric_ids для публикации НЕ ПРОКСИРУЮТСЯ этим API. Получай их напрямую из API 4partners.io (4CMS). Если пользователь не дал rubric_ids для первой публикации — спроси, не выдумывай.
  4. Перед первым POST /api/v1/pages/{id}/publish обязательно прочитай examples/payloads/publish-page.json. Не угадывай форму payload — поля называются именно так как там.
  5. Перед pages-upgrade-all запусти pages-get --with-blocks и прочитай update_status всех блоков. Это снимает сюрпризы — увидишь, что будет затронуто и сколько major_blocked ждёт ручной миграции.
  6. На 422 major_upgrade_blocked НЕ ретраить upgrade. Это не временная ошибка. Только manual migration по Workflow 3.
  7. page-blocks-delete и page-blocks-replace без --yes возвращают current state в stderr и просят подтверждения. Не подавляй это — сначала покажи пользователю, что будет удалено/затёрто. У контейнера в отказе есть nested_blocks: удаление уносит поддерево целиком, и человек должен увидеть его состав. replace контейнера с непустыми слотами вернёт 422 — сначала разберись с вложенными блоками.
  8. После replace старый pbId инвалидирован. Не используй прежний id для последующих вызовов — возьми новый id из response.page.blocks по order_index.
  9. Все mutate-эндпоинты pageBlock возвращают { page: PagePublic }. Не делай лишний follow-up pages-get — данные уже у тебя в ответе. Ответ pages-create тоже содержит blocks[] с id созданных записей.

Минимальный валидный payload для POST /api/v1/pages

Это абсолютный минимум — добавляй блоки и метаданные сверху, но никогда не переименовывай эти поля.

{
  "site_id": 1,
  "name": "Заголовок страницы",
  "slug": "page-slug",
  "blocks": []
}

Корневые поля payload — точные имена и типы:

ПолеТипОбязательноЗаметка
namestring✅Не title — именно name.
site_idinteger✅Целое число, не строка.
kindstringопц.Вид документа, по умолчанию article. См. раздел «Вид документа» ниже.
slugstringопц.Уникальный в рамках сайта. Только для kind=article.
folder_idintegerопц.ID папки внутри сайта.
meta_titlestringопц.Плоское поле верхнего уровня. Не вкладывать в meta: {...}.
meta_descriptionstringопц.Аналогично — плоское.
meta_keywordsstringопц.Аналогично — плоское.
meta_robotsstringопц.Аналогично — плоское.
blocksarrayопц.Массив 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

Правила выбора вида:

  1. Пользователь просит статью, лендинг, текстовую страницу — kind не передавай вовсе, по умолчанию будет article. Это подавляющее большинство задач.
  2. Пользователь просит оформить категорию каталога, карточку товара, поиск или бренд — это шаблон. Сначала вызови node scripts/page-types-list.mjs и возьми id подходящего типа. Одного типа хватает на все страницы своего вида: страницы каталога, брендов и поиска обслуживает тип с contexts, содержащим rubric/brand/search.
  3. Тип из головы не выдумывай: допустимы только id из справочника — иначе API вернёт 400 Недопустимый вид документа. Справочник может быть пуст; тогда доступны лишь article и layout, и это надо сказать пользователю, а не подставлять случайный тип.
  4. Вид потом не поменять: PUT /pages/{id} с полем kind вернёт 400. Ошибся видом — создавай документ заново.
  5. У шаблона нет своего адреса и 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.mjsGET /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.mjsGET /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.mjsGET /page-typesПеред созданием шаблона — допустимые значения kind и их окружения
node scripts/data-sources-list.mjsGET /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.mjsGET /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}/upgrademinor upgrade; 422 для major
node scripts/pages-upgrade-all.mjs <pageId>POST /pages/{pageId}/blocks/upgrade-allbulk 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. Выбрать коллекцию блоков

Блоки для сборки страницы выбираются через коллекции, не через прямой поиск по библиотеке.

Алгоритм:

  1. Получить глобальные коллекции:
    node scripts/collections-list.mjs
    # --scope global (по умолчанию) | private | all
    
  2. Если глобальная коллекция одна — берём её молча. Если несколько — спросить пользователя, из какой коллекции ставить блоки.
  3. Пользователь может явно указать свою личную коллекцию по id (например, коллекцию своих приватных блоков).
  4. Получить список блоков выбранной коллекции:
    node scripts/collection-show.mjs --id <id>
    
    Ответ содержит collection.blocks[] — это и есть набор блоков для работы.
  5. Для каждого блока из коллекции, который планируется использовать, ОБЯЗАТЕЛЬНО вызывай 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_values
  • example — минимальный валидный example
  • binding_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**, картинки ![alt](url)). Сервер рендерит её в HTML на этапе публикации.

block_type: "schema" — соответствие типов полей значениям

  • text, textarea, url, email, image, icon, color, date, time, datetime → строка
  • select, radio → строка из options[].value
  • switch → boolean
  • object → вложенный объект по fields
  • array → массив объектов по 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 бизнес-осмысленно по трём осям:

  1. Что брать — filter/slug/category_id/etc. Конкретный ресурс на целевом сайте. Например для секции «Хиты женской обуви» — filter: { rubric: 'women-shoes' }, а не дефолт.
  2. Сколько брать — limit. Синхронизируй с visual capacity блока (см. ниже).
  3. В каком порядке — 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ЭндпоинтЧто делает
falsePOST /pages/{id}/publishСоздаёт сущность в 4CMS, проставляет странице external_id. Для страницы требует rubric_ids[], для шаблона тело пустое.
truePOST /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 — референс корневого payload POST /pages. Прочитай в первую очередь — там точные имена обязательных полей (name, плоские meta_*).
  • examples/payloads/hero-block.json — пример заполненного hero-блока (только placeholder_values).
  • examples/payloads/features-block.json — то же для features-блока.
  • examples/payloads/publish-page.json — референс payload POST /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:

  1. Прочитай новую схему блока:

    node scripts/blocks-schema.mjs 7
    
  2. Собери новые placeholder_values по новой схеме, перенося значения из старых полей. removed_fields из ответа 422 / skipped_major подскажет, какие поля удалены.

  3. Вставь новый 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
    
  4. Удали старый 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

Дополнительные ресурсы

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>