Перейти к содержимому

Сборка страницы из блоков

Страница в 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 «Недопустимый вид документа»;
  • у документа с kindarticle поля slug и meta_* отвергаются с 400, а не игнорируются молча. Не отправляйте их.

В интерфейсе виды разложены по разделам сайдбара: Страницы, Шаблоны, Макеты.

При создании страницы сайт указывается обязательно (site_id). Без сайта страница не публикуется: провайдер публикации, его настройки и переменные, доступные шаблонам, живут на сайте, а не на странице.

POST /api/v1/pages
Content-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.

Во что страница превращается при публикации

Заголовок раздела «Во что страница превращается при публикации»

Сборка идёт в таком порядке:

  1. HTML блоков. Шаблон каждого блока рендерится Liquid и склеивается в один HTML в порядке order_index, начиная с блоков верхнего уровня. Вложенные блоки собираются первыми, и их HTML приходит контейнеру в {{ slots.<имя> }} — иначе контейнеру нечего было бы подставить. Блок, отрисовавшийся в пустую строку, пропускается.
  2. CSS и JS блоков собираются отдельно — конкатенацией CSS (и JS) закреплённых ревизий всех блоков страницы, через пустую строку. Одинаковый CSS двух экземпляров одного блока в выдачу попадёт дважды: дедупликации нет.
  3. Переменные сайта вида @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 работает в мягком режиме: неизвестная переменная или фильтр дают пустую строку, а не ошибку. Опечатка в имени переменной проявится как пропавший текст.

  • backend/src/Entity/Page.php, PageBlock.php, PageType.php
  • backend/src/Service/Page/PageKindValidator.php, CreatePageService.php, UpdatePageService.php
  • backend/src/Service/Page/AddPageBlockService.php, PageBlockService.php, UpgradePageBlockService.php
  • backend/src/Publishing/BlockAssembler.php, LayoutRenderer.php
  • backend/src/Controller/V1/Page/InsertBlocksPublicAction.php, UpdatePagePublicAction.php
  • backend/src/Controller/V1/PageBlock/*