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

Схема полей: полный справочник

Схема полей (schema_content) — JSON, который описывает форму настроек блока. Из неё редактор собирает форму, а имена полей становятся переменными шаблона. Это самая проверяемая часть блока: она валидируется при каждом сохранении.

Мета-схема, по которой идёт проверка, опубликована по адресу /block-schema.json с разрешающим CORS — её можно скачать и валидировать свои схемы локально, до отправки на сервер.

{
"type": "object",
"label": "Карточка товара",
"fields": {}
}
Свойство Обязательно Значение
type да всегда строка "object"
fields да карта «ключ поля → определение поля»; может быть пустой {}
label нет заголовок формы в редакторе
examples нет наборы примерных данных, см. Пресеты
bindingSlots нет слоты живых данных; наличие делает блок SSR-блоком
slots нет слоты композиции: области под вложенные блоки, см. Блоки-контейнеры
client нет клиентское поведение SSR-блока (refreshOn)

Других свойств в корне быть не может — любое лишнее отклоняется.

Ключ поля становится переменной Liquid: title{{ props.title }}. Правила:

  • ключ произвольный, но осмысленный — им пользуется автор шаблона;
  • имена size, first, last запрещены на любом уровне вложенности. Это зарезервированные аксессоры Liquid: {{ props.size }} вернёт количество элементов, а не ваше значение, и фильтр | default не спасёт. Используйте box_size, first_item, last_item.

Эти свойства принимает большинство типов:

Свойство Тип Значение
type строка тип поля; единственное обязательное свойство
label строка подпись поля в форме
description строка пояснение под контролом; добавляйте, только если label недостаточно
required boolean поле обязательно к заполнению
default по типу поля значение, подставляемое в пустую форму
tab строка имя вкладки формы, под которой показать поле

required и default не принимают контейнерные типы object и array — у контейнера нет собственного значения. tab принимают все типы.

Как только tab появился хотя бы у одного поля, форма переключается в режим вкладок. Поля без tab попадают на вкладку «Общие», она идёт первой и открыта по умолчанию. Остальные вкладки идут в порядке первого появления полей в объекте fields — отдельной сортировки нет, порядком управляет порядок ключей в JSON. Если вкладка «Общие» не нужна, проставьте tab всем полям: пустая вкладка не показывается.

Всего двенадцать типов.

Различаются только контролом и подсказкой ввода; хранят строку.

Параметр Значение
default строка
{
"email": { "type": "email", "label": "Почта", "default": "hi@example.com" },
"about": { "type": "textarea", "label": "Описание", "tab": "Контент" }
}

В шаблоне: {{ props.about }}.

Кнопка загрузки картинки. Хранит публичный URL, а не файл. Требует настроенного в аккаунте S3-хранилища; можно вписать и внешний URL с CDN.

Параметр Значение
default строка — URL картинки
<img src="{{ props.cover }}" alt="" />

Поиск иконки через Iconify. Хранит готовую SVG-разметку, а не идентификатор иконки — поэтому в шаблоне поле выводится как есть и рисует инлайновый SVG.

Параметр Значение
default id иконки Iconify, например "mdi:home"; при инициализации формы превращается в SVG
<span class="icon">{{ props.icon }}</span>

Выбор одного значения из списка. Отличаются только видом контрола: выпадающий список против группы переключателей.

Параметр Обязательно Значение
options да массив { "label": "…", "value": "…" }, минимум один элемент
default нет одно из options[].value
{
"theme": {
"type": "select",
"label": "Тема",
"options": [
{ "label": "Светлая", "value": "light" },
{ "label": "Тёмная", "value": "dark" }
],
"default": "light"
}
}

Значение, которого нет в options, отклоняется при сохранении данных.

Переключатель. Хранит boolean.

Параметр Значение
default true или false (именно boolean, не строка "true")
{% if props.is_wide %}<div class="wide">{% endif %}

Палитра. Хранит строку с hex-цветом вида #ff0000.

Параметр Значение
default строка, например "#ffffff"

Выбор даты и времени. Хранят строку фиксированного формата:

Тип Формат Пример
date YYYY-MM-DD 2026-04-06
time HH:MM 14:30
datetime YYYY-MM-DDTHH:MM 2026-04-06T14:30

default задаётся в том же формате.

Группирует поля в один объект.

Параметр Обязательно Значение
fields да карта вложенных полей, тот же формат, что и в корне

Вложенность не ограничена: внутри object может лежать другой object или array. Запрет на имена size/first/last действует и здесь.

{
"button": {
"type": "object",
"label": "Кнопка",
"fields": {
"text": { "type": "text", "label": "Надпись", "default": "Купить" },
"href": { "type": "url", "label": "Ссылка" }
}
}
}

В шаблоне: {{ props.button.text }}.

Список однотипных элементов: слайды, пункты списка, карточки.

Параметр Обязательно Значение
item да определение элемента; должно быть полем типа object
min нет минимум элементов; он же задаёт стартовое количество
max нет максимум элементов
itemLabel нет шаблон подписи элемента в редакторе, {{fieldKey}} подставляется
{
"slides": {
"type": "array",
"label": "Слайды",
"min": 1,
"max": 6,
"itemLabel": "{{caption}}",
"item": {
"type": "object",
"fields": {
"image": { "type": "image", "label": "Картинка" },
"caption": { "type": "text", "label": "Подпись" }
}
}
}
}

В шаблоне разворачивается циклом:

{% for slide in props.slides %}
<figure><img src="{{ slide.image }}"><figcaption>{{ slide.caption }}</figcaption></figure>
{% endfor %}

Пустая форма и блок без сохранённых значений заполняются по одним и тем же правилам — одинаково на сервере при публикации и в браузере при предпросмотре:

Тип Значение, если default не задан
текстовые, image, icon, color, date, time, datetime "" (пустая строка)
select, radio первое значение из options
switch false
object объект, собранный по тем же правилам из вложенных полей
array min элементов (по умолчанию один), каждый собран по правилам item.fields

Отсюда практическое следствие: массив без min в пустой форме приходит с одним пустым элементом, а не пустым списком.

{
"type": "object",
"label": "Карточка тарифа",
"fields": {
"title": { "type": "text", "label": "Название", "required": true, "default": "Базовый" },
"price": { "type": "text", "label": "Цена", "tab": "Контент" },
"accent": { "type": "color", "label": "Акцентный цвет", "default": "#2563eb", "tab": "Оформление" },
"is_featured": { "type": "switch", "label": "Выделить", "default": false, "tab": "Оформление" },
"size": { "type": "select", "label": "Размер",
"options": [
{ "label": "Обычный", "value": "md" },
{ "label": "Крупный", "value": "lg" }
],
"default": "md",
"tab": "Оформление"
},
"cta": {
"type": "object",
"label": "Кнопка",
"fields": {
"text": { "type": "text", "label": "Надпись", "default": "Выбрать" },
"href": { "type": "url", "label": "Ссылка" }
}
},
"features": {
"type": "array",
"label": "Что входит",
"min": 1,
"max": 10,
"itemLabel": "{{text}}",
"item": {
"type": "object",
"fields": {
"text": { "type": "text", "label": "Пункт" },
"icon": { "type": "icon", "label": "Иконка", "default": "mdi:check" }
}
}
}
},
"examples": [
{
"title": "Тариф Pro",
"data": { "title": "Pro", "price": "990 ₽" }
}
]
}

Непустой slots в корне делает блок контейнером: редактор сможет вкладывать в объявленные области другие блоки, а шаблон выводит собранный HTML детей.

{
"type": "object",
"fields": {},
"slots": {
"left": { "label": "Левая колонка" },
"right": { "label": "Правая колонка", "max": 1 }
}
}
<div class="cols">
<div class="cols__left">{{ slots.left }}</div>
<div class="cols__right">{{ slots.right }}</div>
</div>
Свойство Обязательно Значение
label нет название области в редакторе; без него показывается имя ключа
max нет максимум блоков в области; без него — без ограничения

Правила:

  • имя слота — латиница, цифры и подчёркивание, не с цифры;
  • имена slots и bindingSlots не должны пересекаться: в шаблоне это {{ slots.x }} и {{ data.x }}, одинаковое имя отклоняется валидатором;
  • вложенность — один уровень: блок, лежащий в слоте, своих детей иметь не может. Число лежит в мета-схеме под ключом x-maxSlotDepth;
  • пустая область даёт пустую строку, а не ошибку;
  • контейнер может быть и SSR-блоком: признаки независимы. Слоты в таком блоке PageCraft подставляет сам, остальной шаблон уезжает на витрину как есть;
  • вложенные блоки — полноценные экземпляры со своими значениями полей, привязками и закреплённой версией. Порядок считается внутри слота, а не по странице.

Как складывать блоки в слоты через API — Сборка страницы.

Схема проверяется в два слоя, и второй не запускается, если провалился первый — иначе поверх структурных ошибок пришёл бы шум производных претензий.

Слой 1 — структура. Схема проверяется мета-схемой /block-schema.json. Сюда попадают: неизвестный тип поля, лишние свойства, отсутствие options у select, неверный тип default, запрещённые имена полей.

Слой 2 — семантика. То, что JSON Schema выразить не может: ключи examples[].data должны существовать в fields, источники в bindingSlots — в реестре источников, а имена slots не должны совпадать с именами bindingSlots.

Ошибки возвращаются кодом 422 одной строкой:

{ "error": "Ошибка схемы: schema_content.fields: имя поля \"size\" недопустимо — зарезервированное имя Liquid (нельзя size/first/last)" }
Что не так Сообщение
Нет fields в корне schema_content: The required properties (["fields"]) are missing
Имя поля size / first / last schema_content.fields: имя поля "size" недопустимо — зарезервированное имя Liquid (нельзя size/first/last)
Неизвестный тип ("type": "string") schema_content.fields.title.type: The data should match one item from enum
Лишнее свойство поля (placeholder) schema_content.fields.title: Additional object properties are not allowed: ["placeholder"]
select без options schema_content.fields.mode: The required properties (["options"]) are missing
switch с "default": "true" schema_content.fields.flag.default: The data (string) must match the type: boolean
Ключ data примера не найден в fields schema_content.examples[0].data: поле "titel" не существует в схеме
Имя слота начинается с цифры schema_content.slots: имя слота "1bad" недопустимо — только латиница, цифры и подчёркивание, не с цифры
Имя слота занято bindingSlots slots.x: имя занято bindingSlots — слот композиции и SSR-слот не могут называться одинаково

Валидация значений при работе со страницей

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

Схема описывает форму, значения приходят отдельно — в placeholder_values экземпляра блока на странице. Они проверяются против схемы того блока, который ставится на страницу.

Ошибки возвращаются кодом 400 и, в отличие от ошибок схемы, разложены по путям — клиент может исправить их точечно:

{
"error": "Ошибка валидации блоков",
"errors": [
{ "path": "blocks[0].placeholder_values.title", "message": "ожидалась строка, получено int" },
{ "path": "blocks[0].placeholder_values.items[1].name", "message": "поле обязательно — передайте значение либо задайте default в схеме блока" }
]
}

Что именно проверяется:

Ситуация Сообщение
Обязательное поле не передано и default в схеме нет поле обязательно — передайте значение либо задайте default в схеме блока
Не тот тип у строкового поля ожидалась строка, получено int
Не тот тип у switch ожидалось boolean, получено string
Значение вне options значение "c" не входит в options[].value (a, b)
В object пришёл массив ожидался объект
В array пришёл объект ожидался массив
В _markdown пришла не строка ожидалась строка (Markdown-контент)

Проверка нестрогая к лишнему: ключи, которых нет в схеме, ошибкой не считаются — шаблон Liquid вправе использовать переменные, не объявленные в fields.

Схема:

{
"type": "object",
"fields": {
"title": { "type": "text", "required": true },
"is_wide": { "type": "switch" },
"mode": { "type": "select", "options": [
{ "label": "A", "value": "a" }, { "label": "B", "value": "b" }
]},
"items": { "type": "array", "item": { "type": "object",
"fields": { "name": { "type": "text", "required": true } } } }
}
}

Валидные значения:

{
"title": "Привет",
"is_wide": true,
"mode": "b",
"items": [{ "name": "Раз" }, { "name": "Два" }],
"utm": "лишний ключ — не ошибка"
}

Невалидные и почему:

{
"is_wide": "yes",
"mode": "c",
"items": { "name": "Раз" }
}
title: поле обязательно — передайте значение либо задайте default в схеме блока
is_wide: ожидалось boolean, получено string
mode: значение "c" не входит в options[].value (a, b)
items: ожидался массив

Ещё две тонкости про required, которые видно только на практике:

  • Пустая строка проходит. Сервер проверяет наличие ключа и тип значения, а не «непустоту»: {"title": ""} для обязательного поля будет принято. Форма редактора такое значение не пропустит, API — пропустит.
  • required у switch бессмыслен. Булево значение всегда определено; форма это правило игнорирует, и сервер тоже.

Рекомендация автору блока: если поле помечено required, задайте ему default. Тогда блок останется работоспособным независимо от того, каким путём в него пришли значения.

Как проверить схему, не отправляя её на сервер

Заголовок раздела «Как проверить схему, не отправляя её на сервер»
  1. Скачайте мета-схему: curl -O https://page-craft.4partners.io/block-schema.json.
  2. Прогоните свою схему любым валидатором JSON Schema Draft-07 (ajv, opis, python-jsonschema).
  3. Для конкретного блока дополнительно возьмите GET /api/v1/blocks/{id}/placeholder-schema — там готовый контракт для placeholder_values, минимальный валидный example и список SSR-слотов, так что структуру значений не придётся выводить из schema_content руками.

Источники: backend/public/block-schema.json, backend/src/Service/BlockSchemaValidator.php, backend/src/Service/Block/BlockPlaceholderValidator.php, backend/src/Service/Block/BlockPlaceholderSchemaBuilder.php, backend/src/Publishing/BlockRenderer.php (buildDefaultData), backend/src/Service/Page/PageBlockService.php, backend/src/EventListener/ApiExceptionListener.php, front/src/lib/schema.ts, front/src/lib/schemaValidation.ts.