Данные
Откуда вставка op/repeat берёт список: системные источники, которые считает платформа, собственные наборы сайта с вариантами по языкам или инлайновый массив. Один резолвер, один порядок и ручки, которые его читают, пишут и проверяют.
Повторителю во фрейме нужен список. Вставка op/repeat называет его по имени, а печать разрешает это имя при выпечке фрейма:
<!-- wp:op/repeat {"source":"faq"} -->
<div class="faq__item">
<h3 data-op-bind='{"text":"item.q"}'></h3>
<p data-op-bind='{"text":"item.a"}'></p>
</div>
<!-- /wp:op/repeat -->
Три источника и один резолвер
Список может прийти из трёх мест, и один резолвер обходит их в неизменном порядке: system, затем site, затем inline. Поле origin в каждом ответе говорит, откуда взят результат.
Системный источник — состояние, которое платформа считает сама: меню, языки, настройки сайта, главная страница, дерево страниц, дети текущей страницы, её соседи, хлебные крошки. Эти имена зарезервированы.
Набор сайта — свободное имя (faq, tarify, team, reviews), залитое через /data. Обычный JSON без навязанной схемы: какие поля положили в элементы, такие и доступны внутри повторителя как item.<поле>.
Инлайновый список — массив прямо во вставке, {"items":[…]}, для списка, нужного ровно в одном месте.
Печать и API расходятся только в одном: имя, которое ни во что не разрешилось. На странице повторитель молча переходит к своему инлайновому items (обычно пустому) и ничего не печатает; API отвечает 404 source_not_found. Поэтому имена проверяйте через API, а не глядя на страницу.
Чтение — источник, изменение — своя ручка
Это правило и определяет, какая ручка нужна. Языки читаются как site.languages, а меняются языковыми ручками; содержимое меню читается как menu.header, а правится ручками меню; дерево документации читается как pages.tree, а перестраивается ручками сайдбара. /data пишет только то, чем владеет пользователь, — его собственные наборы. Системные источники — поверхность чтения над сайтом, а не второй способ его настроить.
Зарезервированы пространства menu.*, site.*, pages.* и имя breadcrumbs, а также два слова, занятых самими ручками, — sources и resolve. Набор с зарезервированным именем отклоняется раньше любых проверок, в том числе при dry_run:
curl -s -X PUT -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/data/menu.header" -d '{"blog_id": 235, "dry_run": true, "items": []}'
{
"success": false,
"error": "reserved_dataset_name",
"message": "the name \"menu.header\" belongs to the platform: the menu.*, site.*, pages.* namespaces and breadcrumbs are reserved for system sources and cannot be overwritten by a dataset",
"reserved": true
}
Наборы и язык
У набора бывают варианты по языкам. PUT /data/faq с "lang": "ru" кладёт русский faq рядом с общим, а не вместо него. PATCH и DELETE принимают lang так же (в теле или в строке запроса) и адресуют ровно один вариант; без lang любой вызов относится к общему набору.
| Что | Общий | Вариант для ru |
|---|---|---|
ключ в реестре onpress_datasets | faq | faq@ru |
| опция со списком | onpress_dataset_faq | onpress_dataset_faq__ru |
Какой вариант читает повторитель. Язык берётся по первому совпадению: из атрибута lang у op/repeat, из языка печатаемой страницы, из языка прогона печати (язык запроса на живой странице, язык выпечки для испечённой копии). Если для этого языка есть вариант, берётся он, иначе общий набор. GET /data/{name} и POST /data/resolve идут тем же порядком: параметр lang, затем язык страницы из page.
Откат идёт только на общий набор, а не на другой язык. Чтение с языком, для которого варианта нет, возвращает общий список и сообщает об этом полем lang_fallback. Если нет и общего — source_not_found. Сайт 235 устроен именно так: faq, footer и footer_legal существуют только как варианты en и ru, поэтому GET /data/faq без lang и без page отвечает 404, а каждая страница при этом получает свой язык. Нужен список для языков, которые ещё не переведены, — держите общий набор.
Запись в любой вариант перепекает все фреймы, которые читают набор по базовому имени.
Где лежат наборы
В опциях того самого сайта сети, как фреймы и произвольный CSS. onpress_datasets — реестр (у каждой записи items, bytes, sha256, fields, даты и автор), onpress_dataset_<имя> или onpress_dataset_<имя>__<язык> — сам список в JSON. Набор до 32 КБ пишется с autoload = yes, больше — без автозагрузки. Сетевой опции нет сознательно: стенд делит базу с продом, и сетевая опция ушла бы разом на все сайты.
Реестр — кэш, а не истина. Если опцию набора поменяли в обход API, чтение вернёт настоящие элементы и объект registry_stale со сравнением записи реестра и того, что лежит на самом деле; запись набора через PUT или PATCH приводит их в согласие.
Контекстные источники и печать на запросе
Каждый источник объявляет, зависит ли он от печатаемой страницы. menu.*, site.*, pages.all, pages.tree, pages.top и все наборы сайта не зависят: печать считает их один раз на фрейм. pages.children, pages.siblings, pages.adjacent и breadcrumbs зависят, поэтому их результат кэшируется на пару «фрейм плюс страница».
Из окружения ничего не угадывается: выпечка идёт вне запроса страницы, и текущей страницы в этот момент нет. Контекстный источник получает страницу явно (page в API, печатаемая страница в печати), а без неё возвращает пустой список с needs_page: true.
Фрейм, чей повторитель зависит от страницы, испечённой копии не имеет вовсе: он помечается renders_on_request и печатается на каждом запросе для той страницы, которую открыли. Подробнее — в разделе Как это устроено.
Фильтр match
match у op/repeat оставляет только элементы, поля которых равны заданным значениям. Один набор на весь сайт, отфильтрованный под страницу:
<!-- wp:op/repeat {"source":"faq","match":{"page":"page.slug"},"empty":"frame"} -->
<details class="faq__item">
<summary data-op-bind='{"text":"item.q"}'></summary>
<p data-op-bind='{"text":"item.a"}'></p>
</details>
<!-- /wp:op/repeat -->
Правила общие для печати и API:
- Ключи — поля элемента; точка адресует вложенное поле,
meta.lang. - Значения — токены биндов из того же словаря, что у
data-op-bind(page.slug,page.id,page.parent_id,site.lang,option.<имя>и остальные, см. Бинды), либоraw:<значение>для константы. - Сравнение — строгое равенство строк:
3равно"3",true— это"1",false— пустая строка; массивы, объекты,nullи отсутствующие поля не совпадают никогда. - Совпасть должны все пары. Если у токена нет значения (например,
page.parent_idу страницы верхнего уровня), список пуст, а API называет такой токен вmatch_unresolved. matchработает с любым источником: системным, набором сайта или инлайновым.
Токен page.* делает фрейм страницезависимым: в реестре фреймов он получает renders_on_request: true, и у каждой страницы свой список. Без страницы повторитель ничего не печатает и сообщает needs_page.
"empty": "frame" на той же вставке заставляет весь фрейм, в котором стоит повторитель, печататься пустой строкой, если после фильтра список пуст; фрейм, вложенный через op/frame, оставляет пустое место. Так на страницах без вопросов не появляется блок FAQ с одиноким заголовком. Без атрибута на месте пустого списка остаётся HTML-комментарий, а остальной фрейм печатается. Связывание фреймов описано в разделе Фреймы, рецепт целиком — FAQ из данных.
Системные источники
| Источник | Контекстный | Что отдаёт |
|---|---|---|
menu.header | нет | пункты меню, стоящего в области header темы; с lang берётся карта Polylang |
menu.footer_1, menu.footer_2, menu.footer_3 | нет | то же для трёх областей подвала |
site.settings | нет | имя, описание, адреса, часовой пояс, форматы дат, локаль, что стоит на главной, активная тема |
site.languages | нет | языки проекта: слаг, имя, локаль, флаг, rtl, какой по умолчанию, адрес локали |
site.front_page | нет | что на самом деле открывается на корне: id страницы, заголовок, путь |
pages.all | нет | все опубликованные страницы плоским списком, с адресами |
pages.tree | нет | те же страницы деревом по родителю |
pages.top | нет | страницы без родителя |
pages.children | да | дети текущей страницы |
pages.siblings | да | страницы того же уровня, включая текущую |
pages.adjacent | да | один элемент с prev и next |
breadcrumbs | да | цепочка от корня сайта до текущей страницы, каждый элемент помечен current |
Источник меню отдаёт ещё meta: какой терм меню взят и сколько пунктов отброшено. Пункт, ведущий на удалённую или неопубликованную страницу, невалиден, посетитель его не видит — и источник печатает ровно то же. Пустой список с "invalid_items": 4 означает «меню сломано», а не «источник не работает».
GET /data
Список собственных наборов сайта, по строке на вариант. Системные источники сюда не подмешиваются.
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/data?blog_id=235"
{
"success": true,
"blog_id": 235,
"home_url": "https://example.onpress.pro",
"registry_option": "onpress_datasets",
"option_prefix": "onpress_dataset_",
"resolution_order": ["system", "site", "inline"],
"system_sources": "/data/sources",
"total": 6,
"datasets": [
{
"name": "faq",
"lang": "en",
"origin": "site",
"contextual": false,
"items": 42,
"bytes": 9692,
"sha256": "31b4808cde7f9285afb8e7f53029f0ed6cec4965c5af47c719a1f5ae48a9ec61",
"fields": [
{ "field": "page", "types": ["string"], "in_items": 42 },
{ "field": "n", "types": ["number"], "in_items": 42 },
{ "field": "q", "types": ["string"], "in_items": 42 },
{ "field": "a", "types": ["string"], "in_items": 42 }
],
"created_at_gmt": "2026-09-21 19:35:22",
"updated_at_gmt": "2026-09-21 19:35:22",
"updated_by": 1,
"option": "onpress_dataset_faq__en"
},
{
"name": "faq",
"lang": "ru",
"items": 34,
"option": "onpress_dataset_faq__ru",
"…": "…"
},
{ "name": "footer", "lang": "en", "items": 1, "option": "onpress_dataset_footer__en", "…": "…" }
]
}
У общего набора lang равен null. fields — то, что реально есть в элементах, посчитанное по первым двумстам: когда пишете разметку под набор, это полезнее объявленной схемы.
GET /data/sources
Системные источники, только чтение, у каждого сказано, контекстный он или нет.
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/data/sources?blog_id=235"
{
"success": true,
"blog_id": 235,
"home_url": "https://example.onpress.pro",
"readonly": true,
"note": "system sources are read-only: the state of the site is edited through its own endpoints (languages, menus, settings) and arrives here already computed",
"contextual_note": "contextual=true — the source depends on which page is being baked; such a result is cached per frame-and-page pair, the rest are cached once per frame",
"total": 14,
"sources": [
{
"name": "menu.header",
"origin": "system",
"contextual": false,
"description": "Items of the menu assigned to the \"header\" location of the active theme; when a language is given, the Polylang map is used"
},
{
"name": "breadcrumbs",
"origin": "system",
"contextual": true,
"description": "Breadcrumbs of the current page: from the site root down to the page itself"
}
]
}
GET /data/{name}
Чтение одного источника. Системный источник и набор сайта читаются одинаково и отвечают в одной форме.
| Параметр | Что делает |
|---|---|
page | текущая страница: id, слаг или путь; нужна контекстным источникам и токенам page.* в match, остальными игнорируется |
lang | слаг языка; выбирает меню и вариант набора |
statuses | статусы страниц для источников pages.* через запятую; по умолчанию publish |
match | тот же фильтр, что у op/repeat, JSON в URL-кодировке |
theme | тема, чью карту областей читает источник menu.*; по умолчанию опция сайта stylesheet |
Меню на заданном языке:
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/data/menu.footer_1?blog_id=235&lang=ru"
{
"success": true,
"blog_id": 235,
"name": "menu.footer_1",
"origin": "system",
"contextual": false,
"page_id": null,
"needs_page": false,
"description": "Items of the menu assigned to the \"footer_1\" location of the active theme; when a language is given, the Polylang map is used",
"total": 4,
"meta": { "menu_id": 167, "invalid_items": 0 },
"items": [
{ "id": 6438, "title": "Возможности", "url": "/features/", "type": "post_type", "object": "page", "object_id": 6417, "parent": 0, "order": 1, "children": [], "…": "…" },
{ "id": 6441, "title": "ИИ-инструменты для маркетинга", "url": "https://aitoolsformarketing.pro", "type": "custom", "object": "custom", "object_id": 6441, "parent": 0, "order": 4, "children": [], "…": "…" }
]
}
У варианта набора в ответе есть option и lang, а fields и updated_at_gmt берутся из реестра:
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/data/faq?blog_id=235&lang=ru"
{
"success": true,
"blog_id": 235,
"name": "faq",
"origin": "site",
"contextual": false,
"option": "onpress_dataset_faq__ru",
"lang": "ru",
"fields": [
{ "field": "page", "types": ["string"], "in_items": 34 },
{ "field": "n", "types": ["number"], "in_items": 34 },
{ "field": "q", "types": ["string"], "in_items": 34 },
{ "field": "a", "types": ["string"], "in_items": 34 }
],
"updated_at_gmt": "2026-09-21 19:35:24",
"total": 34,
"items": [
{
"page": "pomelli-agent",
"n": 1,
"q": "Что такое Pomelli AI?",
"a": "Это экспериментальный ИИ-инструмент для маркетинга от Google Labs и Google DeepMind, который анализирует ваш сайт и создаёт кампании в фирменном стиле бренда."
},
{ "…": "…" }
]
}
Если в lang язык, для которого варианта нет, вернётся общий набор с "lang": null и фразой в lang_fallback.
PUT /data/{name}
Кладёт набор целиком.
| Поле | Тип | Что делает |
|---|---|---|
items | массив | список, обязателен; одиночный объект заворачивается в список из одного элемента с предупреждением |
lang | строка | записать вариант для этого языка вместо общего набора |
dry_run | bool | проверить и доложить, ничего не записывая |
blog_id / domain | int / строка | сайт сети |
Имя приводится к нижнему регистру и должно попадать в [a-z0-9][a-z0-9_-]{0,63}; иначе 400 bad_dataset_name с подсказкой годного имени в сообщении. Предел размера — 512 КБ (DATASET_MAX_BYTES), больше — 413 dataset_too_large с bytes и max_bytes: на таком объёме это уже контент, а не список для повторителя. Элементы, которые не объекты, принимаются с предупреждением: полей внутри повторителя у них нет.
curl -s -X PUT -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/data/team" -d '{
"blog_id": 235,
"lang": "ru",
"dry_run": true,
"items": [
{ "id": 1, "name": "Анна", "role": "редактор" },
{ "id": 2, "name": "Олег", "role": "разработчик" }
]
}'
{
"success": true,
"dry_run": true,
"applied": false,
"blog_id": 235,
"dataset": {
"name": "team",
"lang": "ru",
"items": 2,
"bytes": 113,
"sha256": "95966cda6797fb639da02739ce4b22349a88126d2550678f649d29bac795b72d",
"fields": [
{ "field": "id", "types": ["number"], "in_items": 2 },
{ "field": "name", "types": ["string"], "in_items": 2 },
{ "field": "role", "types": ["string"], "in_items": 2 }
],
"updated_at_gmt": "2026-09-21 21:11:01",
"updated_by": 1,
"origin": "site",
"option": "onpress_dataset_team__ru"
},
"warnings": [],
"items": [
{ "id": 1, "name": "Анна", "role": "редактор" },
{ "id": 2, "name": "Олег", "role": "разработчик" }
]
}
Настоящая запись отвечает "applied": true, полем updated (был ли вариант раньше), тем же dataset с created_at_gmt, warnings и rebaked. У пробного прогона rebaked нет: печь было нечего.
PATCH /data/{name}
Правит набор на месте, не пересылая его целиком. Тело — {"ops": [ … ]} или одна операция прямо в теле; lang выбирает вариант.
op | Адресация | Данные | Что делает |
|---|---|---|---|
set | index или match | item | заменяет элемент |
merge | index или match | fields | вливает поля в элемент |
append / prepend | — | item или items | добавляет в конец или в начало |
remove | index или match | — | удаляет элемент |
move | index или match | to | переставляет элемент на позицию |
match здесь — простой объект, который сравнивается с полями элемента как строки, побеждает первое совпадение: {"match": {"id": 2}}. Это не токенный фильтр op/repeat. Операции применяются по порядку в одной транзакции: упавшая операция оставляет набор нетронутым.
curl -s -X PATCH -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/data/faq" -d '{
"blog_id": 233,
"ops": [
{ "op": "merge", "match": { "id": 2 }, "fields": { "a": "Нет, всё делается агентом" } },
{ "op": "append", "item": { "id": 3, "q": "Есть ли API?", "a": "Да, v2" } }
]
}'
{
"success": true,
"blog_id": 233,
"dataset": { "name": "faq", "items": 3, "bytes": 233, "option": "onpress_dataset_faq", "…": "…" },
"applied": [
{ "op": "merge", "index": 1, "fields": ["a"] },
{ "op": "append", "added": 1, "total": 3 }
],
"total": 3
}
applied докладывает о каждой операции: index у set, merge и remove, from и to у move. В ответе есть и rebaked. Несуществующий вариант — 404 dataset_not_found: сначала заведите его через PUT.
DELETE /data/{name}
Удаляет набор и его запись в реестре. С lang (в теле или в строке запроса) уходит только этот вариант, общий набор и остальные варианты остаются. Ответ — deleted с name, lang, items и option, плюс rebaked. Системные источники не удаляются.
curl -s -X DELETE -H "Authorization: Bearer $OP_TOKEN" \
"$BASE/data/team?blog_id=235&lang=ru"
POST /data/resolve
Разрешает источник ровно так, как это сделает печать, и потому служит проверкой перед заливкой фрейма: видно, что получит op/repeat, откуда это пришло и кэшируется ли результат по странице.
| Поле | Тип | Что делает |
|---|---|---|
source | строка | имя для разрешения; без него проверяется инлайновый список |
items | массив | инлайновый список — используется, когда имени нет или оно ни во что не разрешилось |
page | int / строка | текущая страница: id, слаг или путь; даёт значения page.* и, без lang, язык набора |
lang | строка | слаг языка; принудительно выбирает вариант набора |
match | объект / строка | тот же фильтр, что во вставке, объектом или JSON-строкой |
statuses | строка | статусы страниц для источников pages.* |
Что страница получит из отфильтрованного FAQ:
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/data/resolve" -d '{"blog_id": 235, "source": "faq", "page": 6412, "match": {"page": "page.slug"}}'
{
"success": true,
"blog_id": 235,
"name": "faq",
"origin": "site",
"contextual": true,
"option": "onpress_dataset_faq__en",
"lang": "en",
"fields": [
{ "field": "page", "types": ["string"], "in_items": 42 },
{ "field": "n", "types": ["number"], "in_items": 42 },
{ "field": "q", "types": ["string"], "in_items": 42 },
{ "field": "a", "types": ["string"], "in_items": 42 }
],
"updated_at_gmt": "2026-09-21 19:35:22",
"total": 6,
"items": [
{
"page": "pomelli-workflow",
"n": 1,
"q": "Is Pomelli still free?",
"a": "Yes. Pomelli is a free beta from Google Labs. Available in the US, Canada, Australia, and New Zealand. Use a VPN if you are outside these countries."
},
{ "…": "…" }
],
"page_id": 6412,
"match": { "page": "pomelli-workflow" },
"match_total": 42
}
Язык взят со страницы (английский), match показывает разрешённое значение каждого токена, match_total — число элементов до фильтра, total — после. contextual равен true, потому что в фильтре есть page.slug, хотя сам набор не контекстный. Без page тот же вызов вернёт пустой список с needs_page: true и пояснением в match_note.
Контекстный системный источник, страница названа путём:
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/data/resolve" -d '{"blog_id": 235, "source": "breadcrumbs", "page": "/pomelli/"}'
{
"success": true,
"blog_id": 235,
"name": "breadcrumbs",
"origin": "system",
"contextual": true,
"page_id": 6419,
"needs_page": false,
"description": "Breadcrumbs of the current page: from the site root down to the page itself",
"total": 2,
"items": [
{ "id": 0, "title": "Pomelli", "slug": "", "url": "/", "current": false },
{ "id": 6419, "title": "Pomelli — бесплатный AI-инструмент от Google для маркетинга малого бизнеса", "slug": "pomelli", "url": "/pomelli/", "current": true }
]
}
Какие токены считает сервис
Сервис считает page.id, page.slug, page.title (заголовок записи как есть), page.status, page.type, page.parent_id, site.lang (используемый язык), site.blog_id и raw:<значение>. Любой другой токен — 400 match_token_unsupported со списками unsupported и supported. Печать знает весь словарь биндов, поэтому на странице такой фильтр всё равно сработает; проверяйте его выпечкой фрейма для этой страницы.
{
"success": false,
"error": "match_token_unsupported",
"message": "the service cannot compute \"option.blogname\" for match; supported here: page.id, page.slug, page.title, page.status, page.type, page.parent_id, site.lang, site.blog_id and raw:<value>. The baker knows the full bind dictionary, so such a filter still works on the page — check it by baking the frame for the page",
"unsupported": ["option.blogname"],
"supported": ["page.id", "page.slug", "page.title", "page.status", "page.type", "page.parent_id", "site.lang", "site.blog_id"]
}
Изменение набора перепекает фреймы
Повторитель разворачивается при выпечке фрейма, а не на каждом запросе, поэтому изменённый набор сам по себе на страницу не попадёт. Сервис берёт это на себя: PUT, PATCH и DELETE заново пекут все фреймы, чьи повторители читают набор (реестр фреймов хранит эти имена в sources), и докладывают об этом полем rebaked: status — ok, partial, failed или nothing, плюс список фреймов с причиной, по которой каждый выбран. Фреймы с языкозависимой вставкой пекутся по разу на каждый язык сайта. Фреймы, печатаемые на запросе, копии не имеют и видят изменение сразу.
Набор, записанный в обход API (прямо в опцию, миграцией, плагином), сервис не заметит. Перепеките читателей руками: POST /frames/rebake с {"datasets": ["faq"]}, см. Фреймы.
Коды ошибок
| Код | HTTP | Когда |
|---|---|---|
dataset_name_required | 400 | имя не передано |
bad_dataset_name | 400 | имя не попадает в [a-z0-9][a-z0-9_-]{0,63}; подсказка в сообщении |
bad_dataset_items | 400 | items нет, либо это не массив и не объект |
bad_patch_op | 400 | неизвестная операция или у операции нет данных |
item_ref_required | 400 | элемент не адресован ни index, ни match |
nothing_to_update | 400 | PATCH без операций |
match_token_unsupported | 400 | в match токен, который сервис не умеет считать |
dataset_not_found | 404 | набора (или варианта для этого lang) с таким именем нет |
item_not_found | 404 | нет элемента с таким индексом или совпадающего с объектом |
source_not_found | 404 | имя не системное и не набор, а инлайнового items нет |
page_not_found | 404 | в page страница, которой не существует |
reserved_dataset_name | 409 | имя принадлежит платформе |
dataset_too_large | 413 | больше 512 КБ, до или после PATCH |
Общие коды (токен, сайт, лимит запросов) — на странице Ошибки.