Справочник: страницы
Все ручки здесь — ручки сайта: в примерах $API — https://onpress.pro/api/v2, $SITE — siteId сайта, $OP_TOKEN — ваш токен (op_XXXX), {ref} — id, слаг или путь с тильдами вместо слешей (ru~o-nas). Права — администратор сайта или суперадмин. Как устроены страницы в целом — страницы, поля frontmatter — frontmatter.
Публикация из markdown
| Пункт | Ручка | Тело |
|---|---|---|
| Опубликовать или обновить | POST /pages/markdown | {"markdown": "---\nname: …\n---\n\nТекст"} |
| Серия страниц по расписанию | POST /pages/series | {"publish_at": "…", "interval": "1h", "pages": [{"markdown": "…"}]} |
Опубликовать или обновить
Создаёт страницу из markdown или обновляет существующую (upsert).
Тело
| Поле | Тип | Обязательно | Смысл |
|---|---|---|---|
markdown | string | да | весь документ с frontmatter |
frame | string | нет | фрейм страницы (слаг или id); перекрывает frontmatter |
blocks | array | нет | дополнительные блоки после тела: core/*, блоки плагина onpress/*, {"type":"raw","content":"…"} |
create_missing_parents, link_soft, slug_lookup | bool | нет | флаги публикации; frontmatter главнее |
faq_autodetect | bool | нет | собрать раздел FAQ в блок плагина wp:onpress/faq-items |
faq_markers | string[] | нет | заголовки раздела FAQ для faq_autodetect |
template | string | нет | игнорируется, предупреждение template_ignored |
Сайт выбирается адресом запроса; domain во frontmatter только сверяется.
Превью страницы задаётся ключом image во frontmatter: номер вложения этого сайта или адрес картинки. Адрес своей картинки узнаётся без скачивания, чужой заливается в медиатеку (повтор того же адреса берёт то же вложение). image: "" снимает превью, нет ключа — превью не трогается. Превью — это og:image страницы, обложка во вставке onpress/featured-image и картинка в карточках дочерних страниц. Тот же ключ принимает публикация первой версии POST https://example.com/wp-json/onpress/v1/markdown. Отказы: 400 image_invalid (не номер и не адрес, или список), 404 image_not_found (вложения с таким номером нет), 422 image_not_image (вложение не картинка), 502 image_upload_failed (картинка по адресу не скачалась или не залилась в медиатеку). В ответе на эти отказы есть поле field — image.
Пошаговая разметка — ключ howto во frontmatter: auto, on или off. В режиме auto разметка HowTo появляется сама, если в тексте есть инструкция: три и больше подзаголовков «Шаг 1…», «Step 2…» подряд или нумерованный список от трёх пунктов под H2/H3, в названии которого есть слово инструкции на языке страницы («как», «шаг», «инструкция», «how to», «step», «guide»). on — разметка строится всегда, от двух шагов и без слов в подзаголовке; off — никогда. Шаги, размеченные фреймом steps, важнее поиска по тексту: они дают узел и в режиме auto, а off гасит и их. На странице не больше одного узла HowTo. Синонимы: true, yes, 1 — это on; false, no, 0, none — off. Нет ключа — режим не меняется, auto возвращает страницу к режиму по умолчанию. Выгрузка показывает howto, только когда режим не auto. Тот же ключ принимает публикация первой версии. Другое значение — 400 howto_invalid со списком allowed.
Редиректов у страниц нет. Старый адрес, переезд раздела или ссылка на внешний сайт заводятся правилом сайта — Редиректы: правило ловит любой путь, и страница-заглушка для него не нужна. Ключ redirect во frontmatter не записывается, ответ публикации несёт предупреждение. Сменили у опубликованной страницы slug или родителя — правило 301 со старого адреса на новый заводится само, для неё и для каждого опубликованного потомка (автоправила); ответ несёт их полями redirects_created и auto_redirects.
---
name: "Проба превью"
url: "/proba-prevyu/"
image: 5829
---
curl -s -X POST "$API/site/$SITE/pages/markdown" \
-H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
-d "$(jq -n --rawfile md page.md '{markdown: $md}')"
Ответ
{
"success": true,
"updated": false,
"id": 6446,
"status": "publish",
"lang": "ru",
"parent": 0,
"url": "/start/",
"link": "https://test.onpress.pro/ru/start/",
"visible": true,
"translations": { "ru": 6446 },
"frame": { "id": "a2dc0bc0d2da6961", "slug": "start", "meta_key": "_onpress_frame" },
"printed": { "how": "on_request", "url": "/start/", "link": "https://test.onpress.pro/ru/start/", "frame": { "id": "a2dc0bc0d2da6961", "slug": "start", "renders_on_request": true } },
"lang_changed": { "changed": true, "from": null, "to": "ru" },
"warnings": [],
"edit_url": "https://test.onpress.pro/wp-admin/post.php?post=6446&action=edit",
"view_url": "https://test.onpress.pro/ru/start/",
"meta": { "name": "Первая страница", "slug": "start", "…": "…" },
"blocks": [ { "blockName": "core/paragraph", "attrs": {}, "innerHTML": "…" } ],
"preview_url": null
}
| Поле | Когда есть | Смысл |
|---|---|---|
updated | всегда | true — обновлена существующая |
id, status, lang, parent | всегда | записанная страница |
url, link | всегда | путь в дереве; полный адрес (null у неопубликованной) |
visible | всегда | видит ли посетитель (опубликованы она и все предки) |
translations | всегда | группа переводов {язык: id} |
translation_group | с translation_of | {origin, term_id, created, changed, unlinked} |
translation_link_error | link_soft пропустил неудачный translation_of | {error, message} |
frame | если во входе был frame | {id, slug, meta_key} или {cleared: true} |
printed | всегда | {how, url, link, frame, published_by}; how — static, on_request, not_published, theme |
slug_changed | слаг занят | {requested, applied, reason} |
lang_changed | язык страницы сменился | {changed, from, to} |
created_stubs | create_missing_parents создал родителей | [{id, slug}] |
warnings | всегда | предупреждения |
edit_url, view_url, preview_url | всегда | адрес в админке, адрес страницы, адрес предпросмотра (null у опубликованной) |
date, date_gmt | всегда | дата публикации: местное время сайта и тот же момент в UTC (ISO 8601) |
redirects_created, auto_redirects | у опубликованной страницы сменился адрес | заведённые автоправила [{id, from, to}] и сводка {moves, created, updated, enabled} |
scheduled | всегда | null, если страница не запланирована; у future — {at, date, event}: момент выхода в UTC, он же по времени сайта и event — подтвердил ли сайт событие публикации (false приходит с предупреждением: сохраните ещё раз) |
meta | всегда | разобранный frontmatter |
blocks | всегда | блоки, в которые превратилось тело |
Отказы
| Код | HTTP | Когда |
|---|---|---|
markdown_required | 400 | нет markdown или не строка |
blocks_invalid | 400 | blocks не массив |
dry_run_not_supported | 400 | dry_run в теле — пробного прогона нет |
invalid_status | 400 | статус вне publish, draft, pending, private, future |
invalid_date | 400 | date — не дата; field, value |
frame_single | 400 | во frame список |
image_invalid | 400 | image не номер и не адрес картинки; field |
howto_invalid | 400 | howto не auto, on, off; allowed |
block_unsupported | 400 | тип блока в blocks вне разрешённых |
frame_not_found | 404 | фрейма нет на сайте |
ambiguous_page | 409 | слаг у нескольких страниц; matches |
lang_mismatch | 409 | id — страница другого языка |
domain_mismatch | 409 | domain во frontmatter — другой сайт |
translation_group_conflict, front_page_group_conflict | 409 | найденная по слагу страница состоит в другой группе |
translation_link_failed | 422 или 409 | translation_of не сработал; reason |
parent_not_found | 422 | сегмент url — не страница; segment |
absolute_url_in_path | 422 | схема или домен в url |
unknown_language | 422 | lang нет на сайте; languages |
page_vanished | 500 | страница исчезла во время записи — повторить |
post_type_not_supported | 501 | type не page |
acf_block_unsupported | 501 | ACF-блок в blocks |
Отложенная публикация
Страница выходит в назначенное время сама: дата впереди и статус future (или publish — он с датой впереди тоже становится future). Сайт ставит событие публикации на эту дату, и в пределах минуты после неё страница публикуется так же, как при обычном сохранении: появляется по адресу, в лентах и списках дочерних страниц, в карте сайта, в markdown-копии и llms.txt, кэш сбрасывается. Перенос даты переносит выход, сохранение со status: draft снимает планирование, удалённая страница не выйдет.
Запланировать можно одну страницу — date во frontmatter POST /pages/markdown, серию — ручкой ниже, перевод — полем publish_at в теле POST /pages/import, руками — в редакторе страницы панели («Запланировать», «Отменить планирование»). Запланированная страница находится по слагу как любая другая (повторная публикация обновит её, а не создаст дубль) и видна в списке страниц с status=future. Что именно назначено — поле scheduled в ответе.
---
name: "Анонс релиза"
url: "/news/release/"
status: "future"
date: "2026-10-05 09:00"
---
Серия страниц по расписанию
Несколько страниц одним вызовом с выходом по одной: первая — в publish_at, каждая следующая — через interval. Каждая страница пишется так же, как POST /pages/markdown, с тем же frontmatter; серия только назначает даты.
Тело
| Поле | Тип | Обязательно | Смысл |
|---|---|---|---|
pages | array | да | от 1 до 50 страниц: {"markdown": "…", "frame"?: "…", "blocks"?: [...]} или сама строка markdown |
publish_at | string, number | нет | дата первой страницы: время сайта "2026-10-05 09:00", ISO 8601 с Z или смещением, unix-секунды |
interval | string, number | нет | шаг между страницами: секунды (3600) или "90s", "15m", "1h", "1d", "1h30m"; от 60 секунд до 30 дней. Без publish_at первая страница выходит сразу |
dry_run | bool | нет | проверить и показать даты, ничего не записывая |
Страница номер N (с нуля) получает publish_at + N × interval и статус future; уже прошедшая дата публикует страницу сразу. При расписании status и date из frontmatter заменяются — об этом говорит warnings. Без publish_at и interval страницы пишутся как сказано в их frontmatter — обычная пачка.
Сначала проверяется всё: каждая страница разбирается, даты считаются, две страницы с одним слагом — отказ. Ошибка хоть в одной — 422 series_invalid, не записано ничего. Потом страницы пишутся по порядку; если запись всё же упала, серия останавливается: success: false, error: "series_partial", failed, записанные до неё остаются, остальные — not_written.
curl -s -X POST "$API/site/$SITE/pages/series" \
-H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
-d '{"publish_at": "2026-10-02T19:07:00Z", "interval": "2m", "pages": [
{"markdown": "---\nname: Серия 1\nslug: series-1\nurl: /docs/series-1/\nframe: docs\n---\nТекст"},
{"markdown": "---\nname: Серия 2\nslug: series-2\nurl: /docs/series-2/\nframe: docs\n---\nТекст"}]}'
Ответ
{
"success": true,
"applied": true,
"site_id": "SITE_ID",
"now": "2026-10-02T19:04:55Z",
"schedule": { "publish_at": "2026-10-02T19:07:00Z", "interval_seconds": 120, "last_at": "2026-10-02T19:09:00Z", "timezone": "UTC-5" },
"items": [
{
"index": 0, "result": "written", "id": 7064, "updated": false, "title": "Серия 1",
"status": "future", "date": "2026-10-02 14:07:00", "date_gmt": "2026-10-02T19:07:00Z",
"scheduled": { "at": "2026-10-02T19:07:00Z", "date": "2026-10-02 14:07:00", "event": true },
"url": "/docs/series-1/", "link": null, "preview_url": "https://example.com/?page_id=7064",
"warnings": ["page is scheduled for 2026-10-02 14:07:00 (site time): until then the address does not serve it"]
},
{ "index": 1, "result": "written", "id": 7065, "status": "future", "date_gmt": "2026-10-02T19:09:00Z", "…": "…" }
],
"warnings": []
}
| Поле | Смысл |
|---|---|
schedule | {publish_at, interval_seconds, last_at, timezone} — первая и последняя даты в UTC, часовой пояс сайта; null без расписания |
items | по странице на запись, по порядку: result (written, failed, not_written), id, updated, status, date, date_gmt, scheduled, url, link, preview_url, warnings. В пробном прогоне — {index, title, slug, status, date, date_gmt} |
now | время сервера на момент вызова, UTC |
Отказы
| Код | HTTP | Когда |
|---|---|---|
pages_required | 400 | нет pages или это не непустой список |
invalid_date | 400 | publish_at не дата; field: "publish_at" |
invalid_interval | 400 или 422 | interval не шаг (400) или вне 60 с … 30 дней (422; seconds, min, max) |
series_too_large | 413 | больше 50 страниц |
series_invalid | 422 | ошибки в странице: errors: [{index, code, message}] (markdown_required, blocks_invalid, invalid_status, invalid_date, duplicate_slug и отказы разбора); items — как было бы. Ничего не записано |
Разделы сайта
Раздел — корневая страница со слагом news, changelog или docs; записи раздела — её дочерние страницы. В дашборде это виды «Новости», «Чейнджлог» и «Документация» в «Страницах»: пока корня нет, там кнопка «Создать раздел».
| Пункт | Ручка | Тело |
|---|---|---|
| Создать корень раздела | POST /sections/{section} | {"lang": "ru"} или {} |
Создать корень раздела
Заводит страницу /news/, /changelog/ или /docs/ так же, как она устроена на шаблоне base-v2. Недостающие фреймы ставятся на сайт из библиотеки системных вместе с теми, что они вставляют; пустые наборы ленты заводятся, если их нет.
{section} | Фрейм страницы | Тело страницы | Ещё ставится |
|---|---|---|---|
news | landing--news | шапка news__masthead с заголовком и подзаголовком, лента news__feed | фреймы news__post, news__category; наборы news_posts, news_cats, news_more, news_more_cat, pager |
changelog | landing--changelog | шапка changelog__hero, лента changelog__feed из набора changelog | фрейм changelog__post; набор changelog на языке страницы и pager |
docs | docs | пустое | — |
Тело запроса необязательно. lang — язык страницы, по умолчанию основной язык сайта. Заголовок страницы — «Новости», «Чейнджлог», «Документация» для ru, иначе News, Changelog, Documentation. Страница публикуется сразу; текст и шапку дальше правят через POST /pages/markdown. Записи раздела — дочерние страницы: новости пишет автоматизация (/news/auto), чейнджлог — импорт GitHub, документацию — POST /pages/markdown с url: "/docs/<slug>/" и frame: docs.
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H "Content-Type: application/json" \
-d '{}' "$API/site/$SITE/sections/news"
{
"success": true,
"section": "news",
"lang": "en",
"page": { "id": PAGE_ID, "title": "News", "slug": "news", "url": "/news/", "link": "https://example.com/news/", "frame": "landing--news" },
"frames_installed": [
{ "blog_id": BLOG_ID, "slug": "landing--news", "status": "installed", "bake": "ok" },
{ "blog_id": BLOG_ID, "slug": "news__masthead", "status": "installed", "bake": "ok" }
],
"datasets_created": ["news_posts", "news_cats", "news_more", "news_more_cat", "pager"]
}
frames_installed — какие фреймы встали на сайт (уже стоявшие не перечисляются), datasets_created — какие наборы заведены, для набора на языке — с суффиксом (changelog__en).
| Код | HTTP | Когда |
|---|---|---|
invalid_section | 400 | {section} не news, changelog или docs; allowed |
invalid_lang | 400 | у сайта нет такого языка; languages |
section_exists | 409 | корень раздела на этом языке уже есть; page: {id, url, title} |
system_frame_not_found | 404 | нужного фрейма нет в библиотеке системных |
system_frame_missing | 409 | фрейм есть в библиотеке, но разметки его версии нет |
wrong_realm | 409 | сайт другого контура; ничего не записано |
Чтение
| Пункт | Ручка | Тело |
|---|---|---|
| Список страниц | GET /pages | ?lang=ru&parent=0 |
| Одна страница | GET /pages/{ref} | ?lang=ru&markdown=false |
| Тело в markdown | GET /pages/{ref}/markdown | ?as=translation_source |
| JSON для перевода | GET /pages/{ref}/exportGET https://example.com/wp-json/onpress/v1/pages/PAGE_ID/export | ?lang=ru |
Список страниц
| Параметр | По умолчанию | Смысл |
|---|---|---|
lang | все языки | слаг языка или all; ответ всегда называет язык в lang |
status | publish,draft,pending,private,future | CSV статусов или any (с корзиной); синоним statuses |
parent | — | id, слаг или путь родителя; 0 — верхний уровень; any или пусто — без фильтра |
frame | — | слаг или id фрейма; none — страницы без фрейма |
slug | — | точный слаг |
search | — | подстрока заголовка или слага |
page | 1 | страница выдачи |
per_page | 50 | до 200 |
orderby | menu_order | menu_order, title, date, modified, id, slug |
order | asc | asc, desc |
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages?lang=en&parent=0&per_page=2"
{
"success": true,
"site_id": "x9HTLHECq",
"lang": "en",
"lang_filtered": true,
"languages": ["en", "ru"],
"default_lang": "en",
"statuses": ["publish", "draft", "pending", "private", "future"],
"filters": { "parent": 0 },
"total": 34,
"page": 1,
"per_page": 2,
"total_pages": 17,
"orderby": "menu_order",
"order": "asc",
"items": [
{
"id": 6183,
"title": "Documentation",
"slug": "docs",
"status": "publish",
"lang": "en",
"url": "/docs/",
"link": "https://test.onpress.pro/docs/",
"parent": 0,
"menu_order": 0,
"children_count": 17,
"is_front": false,
"date": "2026-09-12 12:48:11",
"modified": "2026-09-21 16:20:24",
"frame": { "id": "af33214f168f8efd", "slug": "obolochka-dokumentaciya", "meta_key": "_onpress_frame_page" },
"topic": [],
"seo": { "title": "OnPress v2 documentation — a site platform driven by its API", "description": "…", "keyword": "" },
"target": "",
"anchor": "",
"static": { "published": false, "url": null, "app_key": "page-6183" }
}
]
}
Счётчик страниц выдачи — total_pages (не pages). url — с префиксом языка. Даты — местное время сайта в виде ГГГГ-ММ-ДД ЧЧ:ММ:СС. frame.meta_key показывает, через какую мету привязан фрейм (_onpress_frame — нынешняя, _onpress_frame_page — старая). static — есть ли статическая копия.
Отказы: 422 unknown_language, 422 unknown_status, 422 unknown_orderby, 404 frame_not_found, 404 page_not_found и 409 ambiguous_page (для parent), 409 polylang_inactive.
Одна страница
Паспорт, frontmatter, фрейм, группа переводов, мета и тело.
| Параметр | По умолчанию | Смысл |
|---|---|---|
lang | — | в каком языке искать и с чем сверить |
markdown | true | false — не выгружать тело (быстрее) |
as | — | translation_source — frontmatter выгрузки с translation_of вместо id, без status |
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages/proba-api?lang=ru"
Ответ: lang, languages, default_lang, объект page и рядом markdown.
Поле page | Смысл |
|---|---|
| поля элемента списка | id, title, slug, status, lang, url, link, parent, menu_order, children_count, is_front, date, modified, frame, topic, seo, target, anchor, static |
excerpt | выдержка |
thumbnail | превью: {id, url, width, height, alt}; null — превью нет |
howto | пошаговая разметка: auto, on или off |
path | путь без префикса языка |
parents | цепочка родителей от корня: [{id, title, slug, status}] |
translations | группа переводов |
frontmatter | frontmatter в том виде, в каком его пишет публикация, плюс frame; есть превью — image с адресом файла |
meta | вся значимая мета страницы (без служебного шума) |
edit_url, view_url, preview_url | адреса |
Если выгрузка тела не удалась, страница всё равно возвращается: markdown: null и markdown_error: {code, message}.
Отказы: 404 page_not_found, 404 not_a_page, 409 ambiguous_page (candidates), 409 page_language_mismatch (actual_lang), 422 unknown_language, 409 polylang_inactive.
Тело в markdown
Только тело — ровно тот формат, который принимает публикация.
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages/ru~proba-api/markdown"
{
"success": true,
"site_id": "x9HTLHECq",
"lang": "ru",
"id": 6431,
"frame": { "id": "e627bf4dbefcc374", "slug": "docs-proba", "meta_key": "_onpress_frame" },
"markdown": "---\nid: 6431\nname: \"Проба API по-русски\"\nslug: \"proba-api\"\ndate: \"2026-09-25 15:20:31\"\nstatus: \"publish\"\nurl: \"/proba-api/\"\nlang: \"ru\"\nframe: \"docs-proba\"\n---\n\nРусский текст.",
"meta": { "id": 6431, "name": "Проба API по-русски", "slug": "proba-api", "date": "2026-09-25 15:20:31", "status": "publish", "url": "/proba-api/", "lang": "ru", "frame": "docs-proba" }
}
Параметры lang и as — как у GET /pages/{ref}. Отказы — те же плюс 501 exporter_missing (на сайте нет выгрузчика) и export_failed.
JSON для перевода
Всё, что нужно перевести страницу, одним ответом: паспорт, тело в markdown, переводы, наборы данных, которые читает фрейм страницы и вложенные в него фреймы, и отдельно — общее для сайта: шапка, подвал, меню. JSON можно целиком отдать нейросети с заданием «переведи на язык X» и залить результат обратно — как это сделать, написано в поле howto самого ответа. Права и выбор страницы — как у GET /pages/{ref}; lang — в каком языке искать страницу.
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages/PAGE_ID/export"
{
"success": true,
"kind": "onpress-page-export",
"version": 1,
"site_id": "SITE_ID",
"howto": "Translate this page from \"en\" into the target language and return a result in the same shape.\n…",
"page": {
"id": 7036, "slug": "home", "lang": "en", "title": "Home", "status": "publish",
"url": "/home/", "link": "https://example.com/home/", "parent": null, "parents": [],
"frame": { "id": "4de31ed24a7771c3", "slug": "home", "meta_key": "_onpress_frame" },
"seo": { "title": "", "description": "", "keyword": "" },
"excerpt": "",
"frontmatter": { "id": 7036, "name": "Home", "slug": "home", "…": "…" }
},
"translations": { "en": 7036 },
"languages": ["en", "ru"],
"markdown": "---\ntranslation_of: 7036\nname: \"Home\"\nslug: \"home\"\n…\n---\n\n…",
"frames": [
{ "slug": "home", "id": "4de31ed24a7771c3", "shared": false },
{ "slug": "site__header", "id": "…", "shared": true },
{ "slug": "site__footer", "id": "…", "shared": true }
],
"datasets": {
"home_stats": {
"origin": "site", "lang": "en",
"lang_fallback": "there is no dataset \"home_stats\" for language \"en\", the shared one was returned",
"variants": [null], "total": 3,
"items": [ { "value": "1.2M+", "label": "drafts written last year", "icon": "<svg …>" } ]
}
},
"shared": {
"note": "Common to the whole site (header, footer, menus), not to this page: translate once per language.",
"sources": {
"header": { "origin": "site", "lang": "en", "variants": [null], "total": 1, "items": [ "…" ] },
"menu.header": { "origin": "system", "lang": "en", "variants": [], "total": 3, "items": [ { "id": 6394, "title": "Docs", "url": "/docs/", "…": "…" } ] }
}
},
"computed_sources": []
}
| Поле | Смысл |
|---|---|
kind, version | onpress-page-export и 1 — по ним видно, что это выгрузка страницы |
site_id | siteId сайта — тот, что подставляется в путь ручек записи |
howto | задание для нейросети на английском: что переводить (заголовок, SEO-поля, выдержку, тело, человеческий текст в items), что не трогать (id, slug, url, ключи, имена полей, ссылки, код, разметку) и какими ручками залить результат |
page | паспорт: id, slug, lang, title, status, url, link, parent, parents, frame, seo (title, description, keyword), excerpt, frontmatter целиком |
translations, languages | группа переводов {язык: id} и языки сайта |
markdown | тело в виде ?as=translation_source: во frontmatter translation_of: <id> вместо id и нет status — файл готов стать переводом после замены lang и текста |
frames | фреймы, из которых собирается страница: сначала её собственный, потом вложенные; shared: true — фрейм шапки или подвала |
datasets | наборы данных страницы на её языке: origin, lang, variants (языки, на которых набор хранится; null — общий список), total, items; lang_fallback — на языке страницы своего варианта нет, отдан общий |
shared | {note, sources} — то же по общему для сайта: источники menu.* и site.*, наборы header и footer и всё, что читают только шапка и подвал. Переводится один раз на язык, а не с каждой страницей |
computed_sources | pages.* и breadcrumbs — собираются из заголовков страниц, отдельно не переводятся |
unresolved_sources | только если есть: источники, которые фрейм называет, а на сайте их нет ({name, error}) |
missing_frames | только если есть: вложенные фреймы, которых нет на сайте |
Как залить перевод. Страница — POST /pages/markdown с переведённым markdown и целевым lang во frontmatter: translation_of в нём уже стоит, так что страница сразу встаёт в группу переводов. Каждый набор из datasets — PUT /data/{name} с {"lang": "<язык>", "items": [...]}: рядом с общим списком появляется вариант для языка (справочник данных). Наборы из shared — теми же ручками, один раз на язык; меню — PUT /data/menu.{область}.
Ограничение. В выгрузку попадает только текст страницы и наборов. Текст, вписанный прямо в разметку фрейма, а не лежащий в наборе, не попадает — его переводят правкой фрейма.
Первая версия. GET https://example.com/wp-json/onpress/v1/pages/PAGE_ID/export отдаёт тот же JSON. Это ручка для кнопки «Copy JSON» в меню правки на самом сайте: она пускает по куке входа WordPress с nonce и праву правки страницы, а также по токену op_. Без входа — 401 rest_forbidden. lang принимается так же.
Отказы: 404 page_not_found, 409 ambiguous_page, 409 page_language_mismatch, 422 unknown_language — как у GET /pages/{ref}. У первой версии: 404 (нет страницы или нет права её править), 502 (сервис API не ответил или выгрузка не удалась).
Ссылки страниц
Какие ссылки стоят в тексте страниц, куда ведут и с каким анкором — для проверки перелинковки. Ссылки берутся только из тела страницы: текст, markdown внутри вставок фреймов (FAQ и подобные слоты) и данные вставок. Шапка, подвал, меню, хлебные крошки и оглавление сюда не попадают. В дашборде это вид «Ссылки» в разделе «Страницы» с кнопкой «Собрать ссылки».
Ссылки собираются по запросу и хранятся: чтение отдаёт последнюю сборку и ничего не пересчитывает. Страница, записанная или удалённая через API (POST /pages/markdown, PATCH /pages/{ref} с данными или превью, DELETE /pages/{ref}), пересобирается сама через пару секунд, вместе с целями ссылок на неё. Правка в редакторе WordPress, новые редиректы, правка фреймов или наборов карточек видны в ответе как stale: true — до следующей сборки.
| Пункт | Ручка | Тело |
|---|---|---|
| Собрать ссылки | POST /links/build | — |
| Ссылки всех страниц | GET /links | ?lang=ru&kind=internal&status=broken |
| Ссылки одной страницы | GET /pages/{ref}/links | ?q=chatgpt |
Собрать ссылки
Разбирает все страницы сайта, кроме корзины, и сохраняет результат. Сайт в пару сотен страниц и пару тысяч ссылок собирается примерно за секунду.
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/links/build"
{
"success": true,
"blog_id": 233,
"building": false,
"joined": false,
"built": true,
"built_at": "2026-10-05T13:29:53.199Z",
"duration_ms": 210,
"pages": 102,
"links": 460,
"probed": 0
}
У сайта идёт одна сборка за раз. Повторный запрос, пока первая не кончилась, ждёт её и отвечает её итогом с joined: true. Если сборку уже ведёт другой экземпляр сервиса, ответ сразу — 202 с building: true и building_since; дождаться можно, опрашивая GET /links до building: false. Новая сборка подменяет прежнюю целиком в конце: пока она идёт, чтение отдаёт прежнюю.
links — сколько ссылок записано, вместе с карточками страниц. probed — сколько внутренних адресов пришлось проверить запросом к сайту: так проверяются только адреса, которых нет среди страниц, правил редиректов и файлов медиатеки (страница вне сайта, картинка нестандартного размера, старый адрес).
Отказы: 413 too_many_pages — больше 5000 опубликованных страниц; 500 links_build_failed — сборка упала, прежняя сборка остаётся.
Ссылки всех страниц
Страницы сайта (все статусы, кроме корзины) из последней сборки пачками, у каждой — её ссылки по порядку появления в тексте.
| Параметр | По умолчанию | Смысл |
|---|---|---|
lang | все языки | слаг языка или all |
offset | 0 | сдвиг по списку страниц |
limit | 100 | до 500 страниц в ответе |
kind | — | internal или external |
status | — | CSV из ok, broken, redirect, unknown |
nofollow | — | 1 — только ссылки с rel="nofollow" |
q | — | подстрока анкора, адреса или конечного адреса редиректа |
cards | 1 | карточки страниц, которые выводит вставка фрейма под текстом: дочерние (link-cards--children), кластер (link-cards--cluster) и любые другие карточки со ссылками, учитываются в ссылках и входящих; 0 — без них. Прежнее имя children тоже понимается |
С любым из фильтров kind, status, nofollow, q в ответе остаются только страницы, где нашлась подходящая ссылка, и только эти ссылки; счётчики counts при этом считаются по всем ссылкам страницы. Параметр fresh прежних версий принимается и ничего не делает: собрать заново — POST /links/build.
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/links?lang=en&kind=internal&limit=1"
{
"success": true,
"blog_id": 233,
"built": true,
"built_at": "2026-10-05T13:29:53.199Z",
"stale": false,
"changes": { "pages": 0, "rules": false, "cards": 0, "site": false },
"building": false,
"building_since": null,
"lang": "en",
"scope": "body",
"external_check": null,
"summary": { "pages": 62, "links": 231, "internal": 231, "external": 0, "cards": 0, "broken": 0, "redirect": 0, "nofollow": 0 },
"total": 28,
"offset": 0,
"limit": 1,
"pages": [
{
"id": 6183,
"title": "Documentation",
"url": "/docs/",
"lang": "en",
"parent": 0,
"status": "publish",
"front": false,
"counts": { "internal": 27, "external": 0, "cards": 0, "broken": 0, "redirect": 0, "nofollow": 0, "inbound": 0, "inbound_pages": 0 },
"links": [
{
"n": 1,
"anchor": "Quickstart",
"before": "$OP_TOKEN — set once in the ",
"after": ". Start here Introduction — what v2",
"href": "/docs/bystryy-start/",
"url": "https://example.com/docs/bystryy-start/",
"kind": "internal",
"domain": null,
"page": { "id": 6188, "title": "Quickstart", "url": "/docs/bystryy-start/" },
"status": "ok",
"final": null,
"nofollow": false,
"rel": [],
"new_tab": false,
"image": false,
"source": "body",
"frame": null
}
]
}
]
}
| Поле ссылки | Смысл |
|---|---|
n | номер по порядку в тексте страницы |
anchor | текст ссылки; у ссылки-картинки — её alt, image: true |
before, after | шесть слов до и после анкора, без разметки; знак вплотную к анкору (., «) словом не считается, пробел между контекстом и анкором сохранён — склеивать before + anchor + after как есть |
href, url | адрес как в тексте и он же полным адресом |
kind, domain | internal — свой сайт; external — чужой, domain без www. |
page | страница сайта, на которую ведёт внутренняя ссылка: {id, title, url}; null — страницы с таким адресом нет |
status | ok; broken — адрес отвечает 404 или 410 (код в code), внешняя — по последней проверке ссылок сайта; redirect — адрес уводит на другой, конечный в final: {url, page}; unknown — проверить не удалось; у внешних без отметки проверки — null |
nofollow, rel, new_tab | атрибуты ссылки |
source, frame | body — текст; frame — данные вставки фрейма (имя в frame, поле адреса в field); cards — карточка страницы из вставки фрейма (имя вставки в frame) |
| Поле | Смысл |
|---|---|
counts | по странице: internal, external, cards, broken, redirect, nofollow; inbound — сколько внутренних ссылок с других опубликованных страниц ведёт сюда (с учётом редиректа), inbound_pages — со скольких страниц |
summary | те же числа по всем страницам языка до фильтров |
scope | откуда взяты ссылки; всегда body — тело страницы, без шапки, подвала, меню и крошек. Это поле ответа, не параметр: ?scope=… ничего не меняет |
built, built_at | сборка была и когда; без сборки — built: false, built_at: null, пустой pages |
stale, changes | stale: true — после сборки что-то поменялось; changes.pages — сколько страниц правили, добавили или удалили мимо API, changes.rules — менялись редиректы, changes.cards — сколько страниц с карточками затронула правка фреймов или наборов, changes.site — сменились адрес сайта или главная |
building, building_since | прямо сейчас идёт сборка и с какого момента |
external_check | когда прошла проверка ссылок сайта (POST /site/seo/links/check), по которой отмечены битые внешние; null — ещё не проверялись |
total | сколько страниц попало в выдачу после фильтров; следующая пачка — offset + limit |
Внутренняя ссылка сопоставляется со страницей по полному пути с префиксом языка. Адрес, которого нет среди страниц, проходит правила редиректов сайта и файлы медиатеки, а если не нашёлся и там — при сборке проверяется запросом к сайту: так распознаются страницы вне сайта, старые адреса и 404. Внешние адреса не запрашиваются: битыми они отмечаются по последней проверке ссылок, и её итог попадает в сохранённую сборку сразу, как проверка закончилась. Якоря на своей странице (#…), mailto: и tel: ссылками не считаются.
Отказы: 400 invalid_kind, 400 invalid_status, 422 unknown_language, 409 polylang_inactive.
Ссылки одной страницы
Исходящие ссылки страницы и входящие на неё из последней сборки — с тем же контекстом. Параметры lang (сверка языка страницы), kind, status, nofollow, q, cards — как у списка; к входящим применяются все фильтры, кроме kind.
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages/6188/links"
Ответ: page (id, title, url, lang, parent, status, front), counts, links — исходящие в том же виде, что в списке, и inbound — входящие: каждая ссылка с полем from: {id, title, url, lang} — страница, где она стоит. Входящие считаются только с опубликованных страниц, ссылки страницы на саму себя не учитываются. Состояние сборки — те же built, built_at, stale, changes, building, что у списка. Страница появилась после сборки и ещё не собрана — page_built: false, у page нет адреса, списки пустые.
Отказы: 404 page_not_found, 409 ambiguous_page, 409 page_language_mismatch, 422 unknown_language, 400 invalid_kind, 400 invalid_status.
Трафик страниц
Сколько поиска и посетителей приносит каждая страница за последние 12 месяцев: показы и клики в Google из Search Console и посетители из Google Analytics 4 — все и из поиска, по странице, с которой начался визит. В дашборде это вид «Трафик» в разделе «Страницы»: колонки «GSC показы», «GSC клики», «GA визиты», «GA органика» в дереве страниц, сортировка по колонке внутри каждого уровня дерева. «GA визиты», «GA органика», «Время на странице» и «Отказы, %» считаются по визитам, начавшимся на странице. Стрелка у числа — тренд 7 дней к прошлым 7, при изменении от 5%. Что сделать со страницей — в соседнем виде «Рекомендации». Наведение на число показывает столбики страницы по всем метрикам сразу: 7 дней, недели или месяцы, с отметками дат, когда страницу правили.
Цифры обновляются раз в сутки ночью и перезаписываются целиком: хранится только итог за скользящий год, без истории по дням. Источник берётся, если он подключён у сайта в разделе «Аналитика»: Search Console — для показов и кликов, Google Analytics — для визитов. Трафик старых адресов страницы, с которых стоит редирект сайта, засчитывается ей: после смены адреса цифры не обнуляются. Адреса сводятся к пути — без протокола, www, параметров и якоря, со слэшем на конце или без.
| Пункт | Ручка | Тело |
|---|---|---|
| Трафик всех страниц | GET /traffic | ?sort=gsc_clicks&no_impressions=1 |
| История страницы | GET /pages/{ref}/traffic | ?granularity=week |
| Рекомендации | GET /traffic | ?advice=any&sort=advice |
| Отметить выполненной | PUT /pages/{ref}/advice/{kind}DELETE /pages/{ref}/advice/{kind} | — |
Трафик всех страниц
Опубликованные страницы сайта с трафиком за год, пачками.
| Параметр | По умолчанию | Смысл |
|---|---|---|
sort | gsc_impressions | gsc_impressions, gsc_clicks, ga_visits, ga_organic, ga_avg_time, ga_bounce_rate, advice (по важности рекомендаций), url или title |
advice | — | any — страницы с любой рекомендацией; или через запятую stale, no_impressions, not_crawled, low_ctr, engagement |
order | desc у чисел, asc у url и title | asc или desc |
no_impressions | — | 1 — только страницы без единого показа в Google за год |
lang | все языки | слаг языка или all |
status | publish | all — все страницы, кроме корзины |
q | — | подстрока заголовка или адреса |
offset | 0 | сдвиг по списку |
limit | 100 | до 500 страниц в ответе |
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/traffic?limit=1"
{
"success": true,
"site_id": "SITE_ID",
"period": { "days": 365, "from": "2025-10-06", "to": "2026-10-05" },
"updated_at": "2026-10-05T03:31:12.000Z",
"gsc": { "connected": true, "status": "ok", "updated_at": "2026-10-05T03:31:12.000Z", "error": null },
"ga": { "connected": true, "status": "ok", "updated_at": "2026-10-05T03:31:12.000Z", "error": null },
"unmatched": { "gsc_clicks": 0, "gsc_impressions": 4, "ga_visits": 86, "ga_organic": 6 },
"redirect_paths": 215,
"lang": "all",
"sort": "gsc_impressions",
"order": "desc",
"summary": { "pages": 207, "gsc_clicks": 4911, "gsc_impressions": 115871, "ga_visits": 36277, "ga_organic": 6469, "no_impressions": 50 },
"total": 206,
"offset": 0,
"limit": 1,
"pages": [
{
"id": 7901,
"title": "Z.ai (нейросеть GLM): что это и как пользоваться",
"url": "/z-ai/",
"lang": "ru",
"parent": 0,
"status": "publish",
"front": false,
"gsc_clicks": 119,
"gsc_impressions": 17979,
"ga_visits": 232,
"ga_organic": 135,
"ga_organic_7d": 4,
"ga_sessions": 262,
"ga_engaged_sessions": 142,
"ga_engagement_seconds": 13270,
"ga_avg_time": 50.6,
"ga_bounce_rate": 0.458,
"trend": { "gsc_impressions": -0.19, "gsc_clicks": -0.059, "ga_visits": -0.222, "ga_organic": -0.179, "ga_avg_time": null, "ga_bounce_rate": null }
}
]
}
| Поле | Смысл |
|---|---|
gsc_impressions, gsc_clicks | показы и клики страницы в Google за год; 0 — Search Console страницу не знает; null — Search Console не подключена, к ней нет доступа или данных ещё нет |
ga_visits | уникальные пользователи Google Analytics за год, начавшие визит на этой странице; null — Google Analytics не подключён, к нему нет доступа или данных ещё нет |
ga_organic | то же только для визитов из поиска (канал Organic Search: Google, Яндекс и другие поисковики) |
ga_organic_7d | пользователи из поиска за последние 7 полных дней (окно trend_windows.ga.cur), по той же странице входа; null — Google Analytics не подключён |
ga_sessions, ga_engaged_sessions, ga_engagement_seconds | суммы за год по визитам, начавшимся на странице: сеансы, вовлечённые сеансы и секунды вовлечения |
ga_avg_time | среднее время вовлечения на сеанс в секундах: ga_engagement_seconds / ga_sessions, как колонка «Среднее время вовлечения на сеанс» в отчёте GA4 «Страницы входа»; null — сеансов нет |
ga_bounce_rate | доля отказов по определению GA4: 1 − ga_engaged_sessions / ga_sessions, от 0 до 1; null — сеансов нет. В summary оба значения посчитаны по суммам всех страниц |
trend | изменение последних 7 полных дней к 7 перед ними, долей: 0.12 — рост на 12%. Показы, клики и визиты — по суммам окон, время и отказы — из сумм сеансов и вовлечения каждого окна. null у метрики — мало данных (оба окна вместе меньше 5, у времени и отказов — меньше 5 сеансов в окне) или прошлое окно пустое; null целиком — у страницы нет трафика в обоих окнах. Считается каждую ночь из истории по дням |
trend_windows | окна тренда по источникам: gsc и ga, у каждого cur и prev с from и to. У Search Console окна сдвинуты на 3 дня — последние дни у неё ещё не окончательные |
period | за какие дни цифры: последние 365 дней на момент сбора |
updated_at | когда цифры обновлялись в последний раз; null — ещё ни разу |
gsc, ga | состояние источника: connected — подключён ли у сайта; status: ok — собрано, pending — подключён, ждёт ночного сбора, error — последний сбор не удался, в ответе прежние цифры (причина в error), denied — у аккаунта платформы нет доступа к ресурсу, цифр нет, off — не подключён; updated_at — когда собран |
unmatched | трафик адресов, которые не нашлись среди страниц и редиректов сайта: файлы, служебные адреса, удалённые страницы без редиректа |
redirect_paths | сколько старых адресов из редиректов сайта учтено при последнем сборе |
summary | итог по всем страницам выдачи до no_impressions и q: сумма цифр и сколько страниц без показов |
total | сколько страниц попало в выдачу после фильтров; следующая пачка — offset + limit |
Цифры Search Console сходятся с её отчётом по страницам за тот же период, если сложить строки адреса с якорями (/page/#section): Search Console показывает их отдельно, здесь они засчитаны странице. Визиты — как отчёт Google Analytics «Страницы входа» с метрикой «Всего пользователей»; если у страницы есть старые адреса с редиректом, пользователи разных адресов складываются.
Отказы: 400 invalid_sort, 400 invalid_order, 400 invalid_status, 400 invalid_advice, 422 unknown_language, 409 polylang_inactive.
Рекомендации
У каждой страницы в ответе GET /traffic есть поле advice — что с ней сделать по уже собранным данным: итогам за год, истории по дням и неделям и визитам поисковых ботов. Новых запросов к Google ручка не делает. В дашборде это вид «Рекомендации» в разделе «Страницы».
| Рекомендация | kind | Когда срабатывает | Важность |
|---|---|---|---|
| Переработайте страницу или удалите с 301 редиректом: за 90 дней ни одного показа | stale | страница старше 90 дней, за последние 90 дней ни одного показа в Google; заменяет no_impressions | critical |
| Доработайте контент: подберите правильный ключ, раскройте тему полнее, добавьте перелинковку, проверьте индексацию | no_impressions | ни одного показа в Google за год, страница старше 14 дней | high |
| Отправьте на индексацию: страницу ещё не видел Googlebot, отправьте её и проверьте в поиске через site: | not_crawled | Googlebot не заходил 30 дней или ни разу с начала сбора визитов | high |
| Улучшите сниппет: перепишите title и description, впишите ключ так, чтобы хотелось кликнуть, добавьте выгоду или цифру | low_ctr | показов от 100, CTR ниже 1% | medium |
| Дайте больше ценности и закройте интент: ответьте на запрос в первом абзаце, сократите вступление, добавьте конкретику и пользу для обычного человека | engagement | отказы выше 25% или время на странице меньше 30 с, при сеансах от 10 | medium |
"advice": {
"items": [
{ "kind": "stale", "severity": "critical", "impressions_90d": 0, "days": 90, "age_days": 214, "path": "/google/nano-banana-2/", "done": null, "returned": false },
{ "kind": "engagement", "severity": "medium", "bounce_rate": 0.4444, "avg_time": 42.6, "sessions": 18, "reasons": ["bounce"], "done": null, "returned": false }
],
"skipped": [],
"score": 6
}
В каждой рекомендации — цифры, на которых она сработала: у low_ctr ещё title и description страницы, у stale — path для редиректа. score — сумма важности (critical 4, high 3, medium 2) для сортировки. skipped — правила, которые не проверялись, и почему: gsc_unavailable, ga_unavailable — источник не подключён или к нему нет доступа; too_new — страница моложе 14 дней; history_short — истории меньше 90 дней; crawl_unavailable, crawl_window_short — визиты ботов не собирались или собираются меньше 14 дней; few_sessions — меньше 10 сеансов; service_page — служебная страница (noindex, юридические, 404), рекомендаций у неё нет. У черновиков advice — null. В summary.advice — сколько страниц с рекомендациями и сколько рекомендаций каждого вида; пороги — в advice_thresholds.
Выполнено. PUT /pages/{ref}/advice/{kind} отмечает рекомендацию страницы выполненной, DELETE снимает отметку. Отметки общие для всех пользователей сайта. В ответе списка у рекомендации done: {at, user_id}. Если отметке больше 30 дней, а рекомендация по свежим данным всё ещё срабатывает, отметка перестаёт действовать: done: null, returned: true, прежняя отметка — в done_before.
curl -s -X PUT -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages/PAGE_ID/advice/low_ctr"
{ "success": true, "blog_id": 233, "page_id": 5776, "kind": "low_ctr", "done": { "at": "2026-10-06T09:02:49.000Z", "user_id": 1 } }
Отказы: 400 invalid_advice — неизвестный kind, 404 page_not_found, 409 ambiguous_page.
История страницы
Трафик одной страницы по периодам — для графиков. Последние 30 дней хранятся по дням, всё старше — по ISO-неделям без срока. Каждую ночь последние дни перезаписываются: Search Console уточняет цифры задним числом за 4 дня, Google Analytics — за 3. Неделя, все дни которой старше 30 дней, сворачивается: показы и клики складываются из дней, а пользователи Google Analytics берутся за неделю целиком — уникальные по дням не складываются.
| Параметр | По умолчанию | Смысл |
|---|---|---|
granularity | day | day — каждый из последних 30 дней по вчерашний; week — каждая неделя от первой с данными по сайту до текущей; month — месяцы из тех же недель |
metrics | все | через запятую: gsc_impressions, gsc_clicks, ga_users, ga_organic_users, ga_sessions, ga_engaged_sessions, ga_engagement_seconds, ga_avg_time, ga_bounce_rate |
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages/z-ai/traffic?granularity=day"
{
"success": true,
"site_id": "SITE_ID",
"page": { "id": 7901, "title": "Z.ai (нейросеть GLM): что это и как пользоваться", "status": "publish" },
"granularity": "day",
"from": "2026-09-06",
"to": "2026-10-05",
"points": [
{"day": "2026-10-04", "gsc_impressions": 189, "gsc_clicks": 1, "ga_users": 1, "ga_organic_users": 0, "ga_sessions": 1, "ga_engaged_sessions": 1, "ga_engagement_seconds": 55, "ga_avg_time": 55, "ga_bounce_rate": 0},
{"day": "2026-10-05", "gsc_impressions": 126, "gsc_clicks": 1, "ga_users": 1, "ga_organic_users": 0, "ga_sessions": 1, "ga_engaged_sessions": 0, "ga_engagement_seconds": 11, "ga_avg_time": 11, "ga_bounce_rate": 1}
],
"changes": [
{ "date": "2026-10-05", "kind": "edit", "kinds": ["edit"], "note": null }
]
}
Месяц с выбором метрик:
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages/z-ai/traffic?granularity=month&metrics=gsc_impressions,ga_users,ga_avg_time,ga_bounce_rate"
{ "month": "2026-10", "gsc_impressions": 1891, "ga_users": 33, "ga_avg_time": 54.8, "ga_bounce_rate": 0.5, "approx": ["ga_users"] }
У недельного ряда точка — week, понедельник недели, у месячного — month (YYYY-MM). Неделя относится к месяцу своего четверга, как у ISO-недель, поэтому месяц — это 4 или 5 целых недель. Показы, клики, сеансы и секунды вовлечения при этом складываются, а время на странице и отказы считаются из сумм за период. Пользователи — уникальные и не складываются: у месяцев и у текущих недель, ещё собранных из дней, они — сумма более коротких периодов и помечены в approx. В ряду нет пропусков: период без трафика приходит нулями, время и отказы без сеансов — null. ga_users и ga_organic_users — уникальные пользователи Google Analytics, начавшие визит на странице за период, все и из поиска. Трафик старых адресов страницы с редиректом засчитан ей, как в итоге за год. Свежие дни Search Console ещё не окончательные: цифры последних 2–3 дней дорастают в следующие ночи.
changes — даты правок страницы за период ряда, по одной записи на день: published — публикация, replaced — публикация страницы, у адреса которой трафик был и раньше (новая главная или страница на месте переехавшего адреса), address — смена адреса (редирект со старого адреса, в note — откуда и куда), edit — сохранение (ревизии без автосохранений и последнее изменение страницы). kind — главная правка дня, kinds — все. Отдельного журнала сохранений нет: на сайте без ревизий видно только последнее изменение.
Отказы: 400 invalid_granularity, 400 invalid_metrics, 404 page_not_found, 409 ambiguous_page.
Сохранённые виды
Свой набор колонок и фильтров раздела «Страницы» можно сохранить под именем — в дашборде это кнопка «Сохранить вид» рядом с выбором колонок, а сохранённый вид появляется в меню под видами раздела. Виды личные: у каждого пользователя на каждом сайте свои, другие пользователи сайта их не видят. Вид — базовый вид раздела (traffic, seo, default и другие) и параметры адреса, которые его настраивают: columns и фильтры (lang, status, visits, time и так далее). В дашборде вид открывается адресом ?view=<id>.
| Пункт | Ручка | Тело |
|---|---|---|
| Список | GET /views | — |
| Сохранить | POST /views | {"name": "Мой вид", "view": "traffic", "params": {"columns": "page,gscImpressions,gaOrganic", "organic7": "1-"}} |
| Переименовать или поменять параметры | PATCH /views/{id} | {"name": "Органика"} |
| Удалить | DELETE /views/{id} | — |
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Мой вид","view":"traffic","params":{"columns":"page,gscImpressions,gaOrganic","organic7":"1-"}}' \
"$API/site/$SITE/views"
{ "view": { "id": "v-3f9a1c2b", "name": "Мой вид", "view": "traffic", "params": { "columns": "page,gscImpressions,gaOrganic", "organic7": "1-" }, "created_at": "2026-10-09T08:12:40.000Z" } }
GET /views отдаёт {"views": [...]} в порядке сохранения. Имя — до 60 символов, параметров — до 30, ключ параметра — латиница в нижнем регистре, цифры и _, значение — строка до 2000 символов; пустые значения отбрасываются. Видов на сайт — до 50.
Отказы: 400 invalid_view_name, 400 invalid_view, 400 invalid_view_params, 400 nothing_to_update, 404 view_not_found, 409 views_limit.
Правка и удаление
| Пункт | Ручка | Тело |
|---|---|---|
| Выдержка, фрейм, превью, HowTo, свои поля | PATCH /pages/{ref} | {"excerpt": "…", "frame": "docs", "thumbnail": 5829, "howto": "on", "data": {"rating": "4.5"}} |
| Данные страницы | GET /pages/{ref}/data-fields | ?lang=ru |
| Удаление | DELETE /pages/{ref} | ?with_children=1&dry_run=1 |
| Язык страниц | POST /pages/language | {"missing": true, "dry_run": true} |
Выдержка, фрейм, превью, HowTo, свои поля
Поменять выдержку, фрейм, превью или пошаговую разметку, не трогая тело.
| Поле | Тип | Смысл |
|---|---|---|
excerpt | string | новая выдержка; "" или null очищают |
frame | string или null | слаг или id фрейма; null, false, "", "none" снимают |
thumbnail | number, string или null | превью: номер вложения-картинки или адрес картинки (чужой заливается в медиатеку); null, 0, "" снимают; {"id": …} и {"url": …} тоже принимаются. Список или другой объект — 400 image_invalid, превью остаётся прежним. То же, что image во frontmatter |
howto | string | пошаговая разметка HowTo: auto, on, off (синонимы — как у howto во frontmatter, null — auto) |
data | object | свои поля страницы, которые фрейм читает токеном page.data.<ключ>: {"ключ": "значение"}; "" или null снимают поле. Ключ ложится постметой и встаёт в frontmatter страницы как data.<ключ> |
lang | string | язык для сверки (также в строке запроса) |
republish | bool | false — не трогать статическую копию; по умолчанию true |
curl -s -X PATCH "$API/site/$SITE/pages/ru~proba-api" \
-H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
-d '{"excerpt": "Выдержка ru", "frame": null}'
{
"success": true,
"site_id": "x9HTLHECq",
"lang": "ru",
"languages": ["en", "ru"],
"default_lang": "en",
"page": { "id": 6431, "title": "Проба API по-русски", "slug": "proba-api", "status": "publish", "lang": "ru", "url": "/ru/proba-api/", "link": "https://test.onpress.pro/ru/proba-api/", "excerpt": "Выдержка ru", "frame": null },
"changed": ["excerpt", "frame"],
"excerpt": { "from": "", "to": "Выдержка ru" },
"frame": { "from": { "id": "e627bf4dbefcc374", "slug": "docs-proba", "meta_key": "_onpress_frame" }, "to": null },
"static": { "published": false, "url": "", "path": null, "bytes": 0, "reason": "no_frame", "message": "", "was_published": false, "was_url": "", "removed": false, "changed": false },
"republish": { "status": "ok", "static": { "…": "…" }, "frame": null, "flushed": { "object": true, "supercache": { "status": "flushed" } } }
}
changed — что реально изменилось; ничего — перепубликации нет (republish.status: "skipped", reason: "nothing_changed"). static.reason: renders_on_request, no_frame, not_public, bad_path, пусто — копия на месте. republish.status: ok, skipped, unavailable, failed — неудача не отменяет правку.
Превью. В ответе page.thumbnail — превью после правки ({id, url, width, height, alt} или null) и рядом thumbnail: {from, to} — номера вложений до и после. Ручка отвечает, когда og:image на странице уже новый. Если в запросе было только превью, статическая копия не пересобирается отдельно: republish.status: "skipped", reason: "content_saved".
curl -s -X PATCH "$API/site/$SITE/pages/proba-prevyu" \
-H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
-d '{"thumbnail": "https://example.com/wp-content/uploads/cover.jpg"}'
{
"success": true,
"page": { "id": 7025, "slug": "proba-prevyu", "…": "…", "thumbnail": { "id": 5828, "url": "https://example.com/wp-content/uploads/cover.jpg", "width": 822, "height": 616, "alt": "" } },
"changed": ["thumbnail"],
"thumbnail": { "from": 5829, "to": 5828 },
"static": { "unknown": true, "why": "content_saved" },
"republish": { "status": "skipped", "reason": "content_saved" }
}
Пошаговая разметка. {"howto": "on"} включает узел HowTo, "off" выключает, "auto" возвращает к поиску инструкции в тексте. Страница пересобирается и кэш сайта сбрасывается в том же запросе, так что разметка на странице меняется сразу. В ответе page.howto и howto: {from, to}; тот же режим — повтор без изменений (changed: []).
curl -s -X PATCH "$API/site/$SITE/pages/PAGE_ID" \
-H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
-d '{"howto": "on"}'
{
"success": true,
"page": { "id": 7025, "…": "…", "howto": "on" },
"changed": ["howto"],
"howto": { "from": "auto", "to": "on" },
"republish": { "status": "ok", "…": "…", "flushed": { "object": true, "supercache": { "status": "flushed" } } }
}
Свои поля страницы. {"data": {"rating": "4.5"}} пишет постмету rating, и фрейм видит её токеном page.data.rating; {"data": {"rating": null}} снимает. Ключи с подчёркиванием и поля со своей обработкой (title, slug, frame, excerpt, image и прочие из frontmatter) сюда не принимаются. Значение — строка, число или булево: списки живут в наборах данных. Копия страницы пересобирается тем же событием, что у превью: republish.status: "skipped", reason: "content_saved". В ответе data: {ключ: {from, to}}. Какие ключи фрейм страницы читает — GET /pages/{ref}/data-fields.
curl -s -X PATCH "$API/site/$SITE/pages/maya-collins" \
-H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
-d '{"data": {"cta": "1"}}'
{
"success": true,
"changed": ["data"],
"data": { "cta": { "from": "0", "to": "1" } },
"static": { "unknown": true, "why": "content_saved" },
"republish": { "status": "skipped", "reason": "content_saved" }
}
Пробного прогона у правки полей нет: с dry_run (в теле или строке запроса) — 400 dry_run_not_supported, страница не меняется.
Отказы: 400 dry_run_not_supported, 400 nothing_to_update (нет ни excerpt, ни frame, ни thumbnail, ни howto, ни data; в ответе fields), 400 redirect_field_removed (прислано поле redirect — редиректы теперь правила сайта, Редиректы), 400 data_invalid (data не объект), 400 data_key_invalid, 400 data_key_reserved, 400 data_value_invalid (список или объект; у отказов data поля field: "data" и key), 400 excerpt_invalid, 400 howto_invalid (allowed), 400 frame_single, 400 image_invalid (не номер и не адрес, список или объект без id и url), 404 image_not_found, 422 image_not_image, 502 image_upload_failed — у отказов превью поле field: "thumbnail", 404 frame_not_found, 404 page_not_found, 409 ambiguous_page, 409 page_language_mismatch, 409 polylang_inactive, 422 unknown_language.
Голый слаг с lang ищется среди страниц этого языка — так же, как у GET: PATCH /pages/proba-api с {"lang": "ru"} правит русскую страницу. 409 page_language_mismatch приходит, только если id или путь с префиксом называют страницу другого языка.
Данные страницы
GET /pages/{ref}/data-fields — что фрейм страницы (вместе с вложенными фреймами) берёт из данных именно для этой страницы, какого типа каждое поле и куда записать правку. На этом построен блок «Данные страницы» во вкладке «Поля» панели страницы в дашборде.
Разбор делает сам сайт по дереву фреймов страницы: бинды data-op-bind, вставки onpress/value, атрибуты вставок и повторители onpress/repeat. Значения токенов страницы считаются так же, как при сборке по шаблону, включая токены темы (page.author_slug). Данные вызовов фреймов в теле страницы (onpress/frame {"data": …}) сюда не входят — они в markdown страницы.
| Раздел | Что это | Чем писать |
|---|---|---|
page_data | свои поля страницы: токены page.data.<ключ> и page.meta.<ключ> в биндах, атрибутах вставок и match (role: "selector") | PATCH /pages/{ref} {"data": {ключ: значение}} |
items | элемент набора сайта, который страница выбирает повторителем с match по полю-идентификатору (slug, id, key, page, url, path, name) и токеном page.*: автор по page.author_slug, фича по слагу страницы | PATCH /data/{набор} {"lang", "ops": [{"op": "merge", "match": ref, "fields": {…}}]} |
shared | наборы и системные источники, общие для сайта (шапка, подвал, меню) или списки страниц (contextual: true — список зависит от страницы, но это не её данные) | в разделе «Данные», PUT/PATCH /data/{имя} |
У каждого поля type — тип редактора: string, url, text, image, number, boolean, list, object. Схемы у набора нет, поэтому тип выводится из значения и из того, куда фрейм его ставит (src — картинка, href — адрес, html/md — длинный текст). used — выводит ли поле хоть один фрейм страницы, uses — где именно (frame, as — атрибут бинда). У элемента набора выводятся все его поля, а не только забинденные: тема читает элемент и мимо биндов (разметка Person у автора). Поле, по которому страница выбирает элемент, приходит с readonly: true. У списка — fields, подполя его элементов.
curl -s "$API/site/$SITE/pages/maya-collins/data-fields" \
-H "Authorization: Bearer $OP_TOKEN"
{
"success": true,
"page_id": 7151,
"lang": "en",
"frame": { "id": "3c91ed8471c4f391", "slug": "author__page" },
"frames": [{ "id": "3c91ed8471c4f391", "slug": "author__page" }, { "id": "1bc91d1c3b348641", "slug": "author__socials" }, { "…": "…" }],
"page_data": [
{ "key": "cta", "value": "0", "type": "string", "in_frontmatter": true, "written": "cta", "role": "selector",
"uses": [{ "frame": "site__cta", "as": "match" }],
"write": { "method": "PATCH", "path": "/pages/7151", "body": { "data": { "cta": "<value>" } } } }
],
"items": [
{
"dataset": "author", "lang": null, "option": "onpress_dataset_author", "index": 0, "total": 1,
"ref": { "slug": "default" },
"selector": { "slug": { "token": "page.author_slug", "value": "default" } },
"found": true,
"frames": ["author__head", "author__avatar", "author__page", "author__posts"],
"fields": [
{ "key": "slug", "value": "default", "type": "string", "used": false, "uses": [], "readonly": true, "readonly_because": "selector" },
{ "key": "role", "value": "Head of Product, Acme", "type": "string", "used": true, "uses": [{ "frame": "author__page", "as": "text" }] },
{ "key": "avatar_url", "value": "/wp-content/uploads/…/maya.png", "type": "image", "used": true, "uses": [{ "frame": "author__avatar", "as": "src" }] },
{ "key": "socials", "value": [{ "network": "linkedin", "label": "Maya Collins on LinkedIn", "link": "https://www.linkedin.com/" }], "type": "list", "used": true,
"uses": [{ "frame": "author__socials", "as": "repeat" }],
"fields": [
{ "key": "network", "type": "string", "used": true, "uses": [{ "frame": "author__socials", "as": "data-network" }] },
{ "key": "label", "type": "string", "used": true, "uses": [{ "frame": "author__socials", "as": "text" }] },
{ "key": "link", "type": "url", "used": true, "uses": [{ "frame": "author__socials", "as": "href" }] }
] }
],
"write": { "method": "PATCH", "path": "/data/author", "body": { "ops": [{ "op": "merge", "match": { "slug": "default" }, "fields": {} }] } }
}
],
"shared": [
{ "source": "header", "origin": "site", "contextual": false, "frames": ["site__header"] },
{ "source": "menu.header", "origin": "system", "contextual": false, "frames": ["site__header"] },
{ "source": "news_posts", "origin": "site", "contextual": true, "frames": ["author__posts"] }
]
}
Правка элемента набора видна на всех страницах, которые его выводят: автор default стоит и в подписи под новостями. Нет элемента для страницы (found: false, у страницы свой _onpress_author, а в наборе такого слага нет) — в ответе missing_note и write: null; элемент добавляется PATCH /data/{набор} с {"op": "append"}. У страницы без фрейма все разделы пустые и frame: null.
Отказы: 404 page_not_found, 409 ambiguous_page, 409 page_language_mismatch, 422 unknown_language, 502 internal_unavailable (сайт не ответил на разбор).
Удаление
| Поле (тело или строка запроса) | По умолчанию | Смысл |
|---|---|---|
force | false | удалить безвозвратно вместо корзины |
with_children | false | удалить всё поддерево |
lang | — | язык для сверки |
dry_run | false | показать план |
curl -s -X DELETE "$API/site/$SITE/pages/6428?with_children=1&dry_run=1" -H "Authorization: Bearer $OP_TOKEN"
{
"success": true,
"site_id": "x9HTLHECq",
"lang": "en",
"dry_run": true,
"mode": "trash",
"page": { "id": 6428, "title": "Проба API", "slug": "proba-api", "lang": "en" },
"targets": [
{ "id": 6429, "title": "Дочерняя проба", "slug": "dochernyaya-proba", "status": "draft", "lang": "en", "url": "/?page_id=6429", "frame": "", "translations": [], "app_key": "page-6429", "static": { "published": false, "url": "" } },
{ "id": 6428, "title": "Проба API", "slug": "proba-api", "status": "publish", "lang": "en", "url": "/proba-api/", "frame": "e627bf4dbefcc374", "translations": { "ru": 6431 }, "app_key": "page-6428", "static": { "published": false, "url": "" } }
],
"translations": { "en": 6428, "ru": 6431 }
}
Настоящий вызов отвечает mode (trash или delete), page (с status_before), removed — по записи на удалённую страницу (removed, static с was_published, was_url, published, removed, translations_unlinked, translations_kept), translations: {before, after}, flushed, home_url и restore — подсказку, как найти страницу в корзине (null после force). У опубликованной страницы ответ несёт ещё redirects_created и auto_redirects: адрес удалённой страницы и её опубликованных потомков уводится правилом 301 на ближайшего опубликованного родителя или на главную языка (автоправила). Поддерево удаляется снизу вверх. Отказ ядра по одной странице приходит в её записи (removed: false, error: "delete_failed"), а не ошибкой запроса.
Отказы: 409 page_has_children (children), 409 front_page_delete (главная, страница блога или главная языка), 409 page_has_no_language (с lang у страницы без языка), 409 page_language_mismatch, 404 page_not_found, 409 ambiguous_page, 422 unknown_language, 502 internal_unavailable.
Язык страниц
Проставить язык страницам.
| Поле | Смысл |
|---|---|
lang | язык; по умолчанию — основной |
missing | true — взять все страницы без языка |
pages | массив ссылок на страницы (id, слаг, путь); синоним одиночного page |
statuses | статусы для missing через запятую; по умолчанию publish |
dry_run | показать и не писать |
curl -s -X POST "$API/site/$SITE/pages/language" \
-H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
-d '{"missing": true, "dry_run": true}'
{ "success": true, "site_id": "x9HTLHECq", "lang": "en", "dry_run": true, "total": 0, "changed": 0, "pages": [] }
Каждая страница в pages: {id, title, url, lang: {from, to}, changed}. Всё в одной транзакции. Правило редиректа смена языка не заводит: если адрес страницы из-за неё сменился, заведите его вручную (Редиректы). Отказы: 400 nothing_to_update (ни pages, ни missing), 409 polylang_inactive, 422 unknown_language, 404 page_not_found.
Markdown-копии страниц
Каждая опубликованная страница открывается ещё и чистым текстом по своему адресу с .md вместо завершающего слеша. Копия нужна людям и нейросетям, которым нужен текст страницы без шапки, меню и разметки; на них же ссылается llms.txt.
| Пункт | Ручка | Тело |
|---|---|---|
| Копия страницы | GET https://example.com/<путь>.md | — |
| Состояние копий | GET /site/markdown | — |
| Пересобрать копии | POST /site/markdown/build | {"llms": true} |
Копия страницы
https://example.com/docs/start/ → https://example.com/docs/start.md. Главная — /index.md, главная языка — /ru/index.md. Токен не нужен: это публичный адрес сайта.
curl -s "https://example.com/docs/start.md"
# Первая страница
Выдержка страницы.
Текст страницы со [ссылкой](https://example.com/docs/).
Ответ — 200 text/plain; charset=utf-8 с Content-Disposition: inline: браузер показывает текст, а не скачивает файл. Внутри H1, выдержка (у страницы без выдержки — SEO-описание) и тело в markdown; ссылки и картинки — полными адресами. У страницы без тела — главная, раздел, страница, которую целиком собирает фрейм, — вместо тела список дочерних страниц со ссылками на их .md. Блоки без markdown-формы остаются HTML без классов. Страница, которую фрейм собирает целиком из данных, даёт короткую копию: заголовок, выдержку и список детей.
404 text/plain отвечают: черновик и любая неопубликованная страница, страница под паролем, закрытая от индексации (noindex), страница без фрейма, страница с неопубликованным родителем, служебная 404 и адрес, которого нет. На закрытом сайте 404 отвечают все копии. Флаг «не индексировать сайт» (blog_public: 0) копии не закрывает: это просьба к поисковикам, а не закрытие доступа.
Копия обновляется сама: при публикации и правке страницы (в том числе POST /pages/markdown и PATCH /pages/{ref}), смене статуса, переносе и удалении. Страница перестала быть публичной или сменила адрес — старая копия сразу отвечает 404. Отдаётся копия готовым файлом, без сборки страницы; браузер и прокси держат её до минуты (Cache-Control: public, max-age=60).
Состояние копий
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/markdown"
{
"success": true,
"site_id": "SITE_ID",
"host": "example.com",
"site_closed": false,
"pages_in_manifest": 60,
"files": 60,
"index_url": "https://example.com/index.md",
"llms_auto": false
}
files — сколько копий уже собрано, pages_in_manifest — для скольких страниц (у главной языка копий две: по адресу страницы и index.md). llms_auto — llms.txt собран автоматически и пересобирается сам (подробно — в llms.txt). Копии, которых ещё нет, собираются при первом запросе, так что files: 0 на новом сайте — нормальное состояние.
Пересобрать копии
Собирает копии всех публичных страниц сайта разом и, если не передано "llms": false, llms.txt.
| Поле | Тип | По умолчанию | Смысл |
|---|---|---|---|
llms | bool | true | собрать заодно llms.txt. Пустой llms.txt становится автоматическим; свой текст владельца не трогается |
curl -s -X POST "$API/site/$SITE/markdown/build" \
-H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' -d '{}'
{
"success": true,
"site_id": "SITE_ID",
"pages": 72,
"written": 60,
"skipped": 12,
"failed": 0,
"removed": 0,
"llms": { "rebuilt": true, "pages": 60, "bytes": 18140 },
"host": "example.com",
"site_closed": false,
"pages_in_manifest": 60,
"files": 60,
"index_url": "https://example.com/index.md",
"llms_auto": true
}
pages — сколько на сайте опубликованных страниц и записей, written — собрано копий, skipped — пропущены, потому что не публичны (noindex, без фрейма, под паролем, с неопубликованным родителем), failed — не записались, removed — снято копий страниц, которых больше нет. На закрытом сайте копии только снимаются: written: 0, files: 0. llms: {"rebuilt": true, "pages", "bytes"} — llms.txt собран и сохранён; {"rebuilt": false, "reason": "custom_text"} — у сайта свой текст llms.txt, он оставлен как есть; "reason": "nothing_to_list" — публичных страниц нет или текст не уложился в 64 КБ; {"rebuilt": false} без причины — передано "llms": false. Ответ приходит после сборки, на большом сайте — до двух минут.
Отказы: 500 markdown_missing — на сайте не включена сборка копий.