Сборка страницы из блоков
Страница в PageCraft — это упорядоченный список блоков плюс метаданные (название, адрес, SEO-поля). Собственного HTML у страницы нет: весь код приходит из блоков.
Список может быть не плоским: блок, объявивший в схеме слоты композиции, становится контейнером, и в его области вкладываются другие блоки — см. ниже.
Вид документа: страница, шаблон, макет
Заголовок раздела «Вид документа: страница, шаблон, макет»Документ создаётся с видом — полем kind. Вид решает, чем документ станет
на витрине и какие поля у него есть:
kind |
Что это | На витрине | Свой адрес и SEO |
|---|---|---|---|
article (по умолчанию) |
обычная страница со своим адресом | статья | да |
id типа страницы (rubric, product, …) |
шаблон страницы каталога, карточки товара и пр. | шаблон документа | нет, их ведёт 4CMS |
layout |
макет | шаблон документа | нет |
Доступные типы страниц приходят из справочника витрины — их список отдаёт
GET /api/v1/page-types:
[{ "id": "rubric", "title": "Страница каталога", "contexts": ["rubric", "brand", "search"] }]Правила простые:
kindзадаётся только при создании и потом не меняется:PUTс этим полем вернёт 400. Ошиблись видом — создайте документ заново.- значение вне списка (
article,layoutплюс id из справочника) — 400 «Недопустимый вид документа»; - у документа с
kind≠articleполяslugиmeta_*отвергаются с 400, а не игнорируются молча. Не отправляйте их.
В интерфейсе виды разложены по разделам сайдбара: Страницы, Шаблоны, Макеты.
Страница всегда принадлежит сайту
Заголовок раздела «Страница всегда принадлежит сайту»При создании страницы сайт указывается обязательно (site_id). Без сайта
страница не публикуется: провайдер публикации, его настройки и переменные,
доступные шаблонам, живут на сайте, а не на странице.
POST /api/v1/pagesContent-Type: application/json
{ "name": "О компании", "site_id": 3, "kind": "article", "slug": "about", "folder_id": 12}Папка (folder_id) необязательна — см. Папки.
Поля «оформление» у страницы нет: обёртку вокруг страницы строит витрина —
см. Что такое макет.
Блок на странице — это копия, а не ссылка
Заголовок раздела «Блок на странице — это копия, а не ссылка»Когда блок из библиотеки добавляется на страницу, создаётся отдельная запись
(в API она называется page_block и имеет собственный id). В ней хранятся:
| Что | Зачем |
|---|---|
| ссылка на блок библиотеки | какой блок используется |
| закреплённая ревизия блока | версия шаблона и схемы, по которой блок отрисуется |
placeholder_values (в интерфейсе — значения полей) |
что подставляется в шаблон |
bindings |
привязки полей к источникам данных |
| снимок схемы | схема полей на момент добавления, для блоков типов schema и markdown |
order_index |
позиция на странице, начиная с 0 |
Важное следствие: правка блока в библиотеке не меняет уже собранные страницы. Страница остаётся на закреплённой ревизии, пока её не обновят явно — см. «Версии блоков на странице» ниже.
Добавление, порядок, удаление
Заголовок раздела «Добавление, порядок, удаление»Блоки вставляются пачкой, с указанием позиции или в конец:
POST /api/v1/pages/42/blocks
{ "blocks": [ { "block_id": 3, "placeholder_values": { "title": "Привет" } }, { "block_id": 7 } ], "position": 2}position— 0-based; существующие блоки начиная с этой позиции сдвигаются вправо.- Без
positionблоки добавляются в конец. - Порядок элементов массива сохраняется.
- Значения полей и биндинги проверяются по схеме блока. При ошибке — 400 и массив
errors[]с записями{ path, message }, гдеpathуказывает на конкретное поле:blocks[0].placeholder_values.title.
Остальные операции:
| Действие | Запрос |
|---|---|
| изменить значения полей одного блока | PUT /api/v1/pages/{pageId}/blocks/{pbId} |
| переместить блок | PUT /api/v1/pages/{pageId}/blocks/{pbId}/move |
| удалить блок | DELETE /api/v1/pages/{pageId}/blocks/{pbId} |
| заменить весь список блоков | PUT /api/v1/pages/{id} с полем blocks |
Правка значений — полная замена объекта placeholder_values: то, чего нет
в запросе, будет стёрто. После удаления блока позиции пересчитываются в плотную
последовательность 0..N-1, поэтому все перечисленные ручки возвращают страницу
целиком — дополнительный GET не нужен.
Блоки в слотах контейнера
Заголовок раздела «Блоки в слотах контейнера»У экземпляра блока есть поля parent_page_block_id и slot_name: у блока верхнего
уровня они null, у вложенного указывают на контейнер и его слот. Чтобы положить блок
в слот, передайте их в элементе blocks[]:
POST /api/v1/pages/42/blocks
{ "blocks": [ { "block_id": 7, "parent_page_block_id": 85, "slot_name": "left" } ]}Что важно знать:
positionиorder_indexсчитаются внутри слота, а не по странице: у блоков верхнего уровня нумерация своя, внутри левой колонки — своя. Порядок внутри слота задаётPUT /api/v1/pages/{id}/blocks/reorder, и список в нём — блоки одного слота; блок из другого слота отклоняется с 422.- Переложить блок в слот, в другой слот или обратно на верхний уровень —
PUT /api/v1/pages/{pageId}/blocks/{pbId}/moveсpositionи, если нужно,parent_page_block_id+slot_name. Без этих полей блок оказывается на верхнем уровне страницы. - В
POST /api/v1/pagesиPUT /api/v1/pages/{id}эти поля не принимаются (400): контейнера в момент запроса ещё не существует. Сначала создайте страницу с контейнером, затем добавьте вложенные блоки отдельным вызовом. - Нарушения правил слота — 422 с причиной: неизвестный слот, переполненный
max, вложенность глубже одного уровня, перенос блока в собственное поддерево. - Удаление контейнера удаляет вложенные блоки. В ответе приходит
removed_count— сколько экземпляров исчезло; спрашивать подтверждение до вызова должен клиент.replaceконтейнера с непустыми слотами отклоняется с 422.
Версии блоков на странице
Заголовок раздела «Версии блоков на странице»У каждого блока на странице закреплены мажорная и минорная версии. Когда в библиотеке выходит новая ревизия, страница показывает блок устаревшим, а обновление зависит от того, что именно изменилось:
- Минорное обновление (мажор совпадает) применяется одним запросом:
POST /api/v1/pages/{pageId}/blocks/{pbId}/upgrade. Закреплённая ревизия и снимок схемы заменяются на актуальные, значения полей сохраняются. - Мажорное обновление автоматически не применяется: в ответ приходит 422
major_upgrade_blockedсо спискомremoved_fields— полей, которых в новой схеме больше нет. Перенос значений — на стороне вызывающего: либоPUT …/blocks/{pbId}/replace, либо вставка нового блока и удаление старого. Автомаппинга полей сервер не делает.
Обновить всю страницу разом: POST /api/v1/pages/{pageId}/blocks/upgrade-all —
применяет все минорные обновления одной транзакцией, а блоки с мажорной разницей
возвращает в skipped_major вместе с removed_fields. Найти страницы, которым
это нужно, помогает фильтр GET /api/v1/pages?has_outdated=1.
Во что страница превращается при публикации
Заголовок раздела «Во что страница превращается при публикации»Сборка идёт в таком порядке:
- HTML блоков. Шаблон каждого блока рендерится Liquid и склеивается в один
HTML в порядке
order_index, начиная с блоков верхнего уровня. Вложенные блоки собираются первыми, и их HTML приходит контейнеру в{{ slots.<имя> }}— иначе контейнеру нечего было бы подставить. Блок, отрисовавшийся в пустую строку, пропускается. - CSS и JS блоков собираются отдельно — конкатенацией CSS (и JS) закреплённых ревизий всех блоков страницы, через пустую строку. Одинаковый CSS двух экземпляров одного блока в выдачу попадёт дважды: дедупликации нет.
- Переменные сайта вида
@varnameподставляются после рендеринга.
HTML, CSS и JS уезжают провайдеру тремя отдельными полями — в какой макет положить страницу, решает витрина.
Блоки со слотами данных (bindingSlots в схеме) оборачиваются в
<div data-block-wrapper-id="…" style="display: contents">, а их шаблон уезжает
на витрину внутри <template data-block-id="…"> — отрисовывает его рантайм витрины.
Значения для такой отрисовки едут отдельным полем context. SSR-блок, лежащий в слоте
контейнера, уезжает <template>-ом внутри <template> родителя, а в context попадает
наравне с остальными: структура context плоская, вложенность выражена только вёрсткой.
Ошибки шаблонов ведут себя по-разному
Заголовок раздела «Ошибки шаблонов ведут себя по-разному»- Ошибка в шаблоне блока не роняет страницу: на месте блока окажется HTML-комментарий с текстом ошибки, остальные блоки опубликуются. Проверяйте результат — сломанный блок молча уезжает на витрину.
- Документ-макет без точки вставки ассетов не публикуется вовсе: отсутствие
{{ cms_assets_head }}в шаблоне лэйаута даёт422, и в 4CMS не уходит ничего.
Liquid работает в мягком режиме: неизвестная переменная или фильтр дают пустую строку, а не ошибку. Опечатка в имени переменной проявится как пропавший текст.
Что дальше
Заголовок раздела «Что дальше»- SEO-поля — адрес страницы и мета-теги.
- Публикация и обновление — как страница попадает на витрину.
- Анатомия блока — из чего состоит блок и что объявляется в схеме его полей.
Источники
Заголовок раздела «Источники»backend/src/Entity/Page.php,PageBlock.php,PageType.phpbackend/src/Service/Page/PageKindValidator.php,CreatePageService.php,UpdatePageService.phpbackend/src/Service/Page/AddPageBlockService.php,PageBlockService.php,UpgradePageBlockService.phpbackend/src/Publishing/BlockAssembler.php,LayoutRenderer.phpbackend/src/Controller/V1/Page/InsertBlocksPublicAction.php,UpdatePagePublicAction.phpbackend/src/Controller/V1/PageBlock/*