Страницы и markdown
Один вызов превращает markdown-документ с frontmatter в страницу: создаёт или обновляет её, ставит в дерево, даёт язык, группу переводов и фрейм.
POST /pages/markdown — ручка, через которую на страницу попадает текст. На вход — целый markdown-документ с frontmatter; тело конвертируется в блоки Гутенберга, а страница, её мета, язык и группа переводов пишутся одной транзакцией: сохраняется либо всё, либо ничего. Что понимает конвертер и во что превращает каждую конструкцию — на странице Синтаксис markdown.
Запрос
curl -s -X POST "$BASE/pages/markdown" \
-H "Authorization: Bearer $OP_TOKEN" \
-H "X-OnPress-Blog-Id: 233" \
-H 'Content-Type: application/json' \
-d "$(jq -n --rawfile md page.md '{markdown: $md}')"
| Поле | Тип | Описание |
|---|---|---|
markdown | string | весь документ вместе с frontmatter; обязательно |
blocks | array | дополнительные блоки в API-формате, дописываются после всего, что получилось из markdown: {"type": "core/paragraph", "content": "…"}, onpress/* с attrs или {"type": "raw", "content": "…"}. Любой другой тип — 501 acf_block_unsupported |
frame | string | фрейм страницы, то же, что поле frontmatter; тело запроса главнее |
faq_autodetect | boolean | собрать секцию FAQ в блок onpress/faq-items; по умолчанию выключено, см. Синтаксис markdown |
faq_markers | array | свой список заголовков FAQ вместо встроенного |
blog_id, domain | int, string | сайт, как в любой ручке v2 |
Всё остальное пишется во frontmatter. create_missing_parents, link_soft и slug_lookup читаются только из frontmatter: те же ключи в JSON-теле v2 не замечает.
Сайт берётся из запроса — blog_id в теле или X-OnPress-Blog-Id. Ключ domain во frontmatter сайт не выбирает, а запрос без сайта пишет на главный сайт сети, blog_id 1.
Документ
---
name: "Медиа"
slug: "media"
url: "/docs-ru/spravochnik/media/"
lang: "ru"
status: "publish"
frame: "obolochka-dokumentaciya"
translation_of: 6370
excerpt: "Загрузка картинок в медиатеку."
seo_title: "Медиа — OnPress API v2"
seo_description: "POST /media, идемпотентная загрузка, поиск по источнику."
---
## Загрузка
Один абзац — одна строка.
Frontmatter
Frontmatter — плоские строки ключ: значение, разрезанные по первому двоеточию, так что двоеточия внутри значения безопасны. Одинарные и двойные кавычки вокруг значения снимаются, строки с # в начале пропускаются. Списков, вложенных ключей и многострочных значений нет: любое значение — строка, флаги принимают true, 1, yes или on.
| Поле | Что делает |
|---|---|
name | заголовок страницы (post_title), HTML-теги вырезаются; псевдоним title тоже принимается |
slug | слаг страницы; без него строится из name с транслитерацией кириллицы. Последний сегмент url слаг не задаёт |
status | publish (по умолчанию), draft, pending, private; published читается как publish |
url | место страницы в дереве, /docs-ru/spravochnik/media/: каждый сегмент, кроме последнего, — существующий родитель. Подробности ниже |
parent | id родителя; работает, только когда url нет |
create_missing_parents | true — недостающие сегменты url создаются черновиками-стабами вместо ошибки |
lang | язык страницы в Polylang; на многоязычном сайте передавайте всегда |
translation_of | оригинал, переводом которого является страница: id, слаг или путь. Страница входит в группу переводов оригинала в той же транзакции |
link_soft | true — страница публикуется, даже если translation_of не разрешился; сбой уходит в warnings |
frame | единственный фрейм, в котором печатается страница, — слаг или id; "" снимает его. См. Фреймы |
template | в v2 игнорируется, в ответ уходит предупреждение template_ignored: вид задаёт фрейм, и второго способа сказать то же самое быть не должно. Мету _wp_page_template v2 не пишет и не стирает, поэтому у страницы, где она есть, она остаётся — просто перестаёт что-либо решать. Пункт сайдбара документации по-прежнему определяется этой метой, так что новая страница документации попадает в сайдбар через POST /docs/sidebar/items/{ref}/show, а не через frontmatter |
excerpt | post_excerpt, печатается лидом под заголовком; "" очищает, отсутствующий ключ оставляет прежний |
seo_title, seo_description | заголовок и мета-описание Yoast |
keyword | фокусное слово Yoast (заодно пишется метой keyword — для совместимости с v1) |
id | обновить именно эту страницу; поиск по слагу пропускается |
slug_lookup | false выключает поиск по слагу: без id всегда создаётся новая страница |
type | только page; всё остальное — 501 post_type_not_supported |
date | post_date, например 2026-09-12T10:00:00; при создании — текущее время, при обновлении сохраняется |
topic | терм таксономии topic, отсутствующий создаётся |
target, anchor, max_publish_date | служебная мета для перелинковки и канала MAX; "" удаляет |
category | игнорируется с предупреждением: у страниц нет рубрик |
| любой другой ключ | пишется в мету под своим именем и называется в warnings — обычно это опечатка |
Какая страница будет записана
С id запись идёт в эту страницу. Без него страница ищется по слагу среди страниц в статусах publish, draft, pending и private; если задан url или parent — только под этим родителем. На многоязычном сайте совпадения сужаются до языка страницы. Страница совсем без языка считается совпадением: так страницы, написанные до появления языков, обновляются, а не дублируются.
Одно совпадение обновляется, в ответе updated: true. Совпадений нет — страница создаётся. Несколько — 409 ambiguous_page со списком кандидатов в matches, и ничего не пишется: передайте id, lang или slug_lookup: false.
Слаг уникален в пределах одного родителя и одного языка, поэтому английская и русская страницы обе называются stranicy: их адреса разводит языковой префикс. Если слаг уже занят другой страницей того же языка под тем же родителем, страница получает следующий свободный (stranicy-2), а в ответе появляются slug_changed с полями requested, applied, reason и предупреждение. Новый слаг у опубликованной страницы сохраняет старый в _wp_old_slug, и старый адрес отдаёт редирект. Новый родитель — нет: старый адрес становится 404.
id страницы другого языка, чем lang, — 409 lang_mismatch: такое сочетание всегда означает перевод, а перевод — это translation_of. Страница, найденная по слагу и уже состоящая в чужой группе переводов с другими участниками, за translation_of не переезжает: 409 translation_group_conflict, для главной языка — front_page_group_conflict. Перенос между группами — PUT /pages/{ref}/translations.
Путь и языковой префикс
url — иерархия страниц, а не адрес. Он разбирается по сегментам:
- Первый сегмент, совпадающий с одним из языков сайта, отбрасывается. Сверка идёт по настоящему списку языков, поэтому страница со слагом
itилиnoна сайте без таких языков остаётся родителем. - Последний сегмент — сама страница, он тоже отбрасывается.
- Каждый оставшийся сегмент должен найтись существующей страницей под предыдущим. Среди страниц с одинаковым слагом выигрывает страница того же языка.
Поэтому перевод описывает своё дерево, а язык задаёт в lang: url: "/about/" с lang: "ru" публикуется по адресу /ru/about/, и /ru/about/ в url ведёт себя так же. Адрес, который реально отдаёт сайт, — link в ответе, с префиксом, который добавляет Polylang (у языка по умолчанию его нет, если он скрыт).
Сегмент, который не нашёлся, — 422 parent_not_found с полем segment, и ничего не пишется. С create_missing_parents: true недостающие сегменты становятся стабами: черновиками с заголовком из слага, метой _onpress_stub_parent и языком страницы; они пишутся в той же транзакции и перечисляются в created_stubs. Стаб-черновик прячет всё, что под ним, — его сегмент пути отдаёт 404, — поэтому ответ говорит visible: false и называет родителя в warnings. Опубликуйте стаб, прежде чем полагаться на адрес.
Схема или домен в url — 422 absolute_url_in_path, голый example.onpress.pro/docs/x/ тоже. Автопочинки нет.
Ответ
Ответ — состояние страницы после записи, а не эхо запроса. Это настоящий ответ на публикацию этой самой страницы:
{
"success": true,
"updated": false,
"id": 6353,
"status": "publish",
"lang": "ru",
"parent": 6231,
"url": "/docs-ru/stranicy/",
"link": "https://example.onpress.pro/ru/docs-ru/stranicy/",
"visible": true,
"translations": { "ru": 6353, "en": 6187 },
"translation_group": {
"origin": { "id": 6187, "lang": "en" },
"term_id": 126,
"created": false,
"changed": true,
"unlinked": [6234]
},
"frame": { "id": "af33214f168f8efd", "slug": "obolochka-dokumentaciya", "meta_key": "_onpress_frame" },
"printed": {
"how": "on_request",
"url": "/docs-ru/stranicy/",
"link": "https://example.onpress.pro/ru/docs-ru/stranicy/",
"frame": { "id": "af33214f168f8efd", "slug": "obolochka-dokumentaciya", "renders_on_request": true }
},
"lang_changed": { "changed": true, "from": null, "to": "ru" },
"warnings": [
"page #6234 was the \"ru\" translation of #6187 and has left the group: a group holds one page per language",
"template_ignored: \"template\" is not applied in v2 — how a page looks is set by \"frame\". The theme template meta was not written; pass \"frame\": \"<slug>\" instead"
],
"edit_url": "https://example.onpress.pro/wp-admin/post.php?post=6353&action=edit",
"view_url": "https://example.onpress.pro/ru/docs-ru/stranicy/",
"meta": { "name": "Страницы и markdown", "…": "…" },
"blocks": [{ "index": 0, "type": "core/paragraph", "content": "<p>…</p>" }, "…"],
"preview_url": null
}
| Поле | Значение |
|---|---|
updated | true — обновлена существующая страница, false — создана новая |
id, status, parent, lang | страница в том виде, в каком записана |
url | путь в дереве, без языкового префикса |
link | публичный адрес с языковым префиксом; null, пока адреса нет (черновик, на модерации) |
preview_url | есть всегда: null у страницы с адресом, иначе адрес вида ?page_id=… |
visible | true, когда статус publish и ни один предок не снят с публикации; каждая причина false — в warnings |
warnings | предложения обо всём, что не так, но не сорвало запрос |
translations | группа переводов после записи, {lang: id}; {}, если группы нет |
translation_group | при translation_of: origin, term_id, created, changed, unlinked |
translation_link_error | при link_soft, если связать не удалось: error и message |
frame | если передан frame: {id, slug, meta_key}, для "" — {cleared: true} |
printed | как страница доходит до посетителя, поле есть всегда: how — static (фрейм испечён, страница опубликована в дерево аппов файлом, published_by: "php"), on_request (фрейм зависит от страницы, PHP печатает её на каждый запрос), theme (фрейма нет, печатает шаблон темы) или not_published (адреса пока нет — черновик или неопубликованный предок, url равен null); плюс url, frame: {id, slug, renders_on_request} и, когда у страницы есть адрес, link |
lang_changed | терм языка, который поставила эта запись: from и to; у новой страницы from: null |
slug_changed, created_stubs | только если слаг сдвинулся или были созданы стабы |
edit_url, view_url, meta, blocks | оставлены для старых клиентов: ссылка в админку, сырой пермалинк, разобранный frontmatter, итоговые блоки |
Предупреждение template_ignored значит ровно то, что сказано: поле не применили. Для страницы, которая уже стоит в сайдбаре документации, не меняется ничего — её _wp_page_template не трогают. Для новой меняется: template из frontmatter убирают, а в сайдбар страницу ставят ручками сайдбара.
Ошибки
Ошибка — {"success": false, "error": "<код>", "message": "…"} плюс поля конкретной ошибки; ветвиться нужно по error. Две проверки самого тела отвечают в старой форме — текст в error, без message: нет markdown и blocks не массив, обе 400. См. также Ошибки.
| HTTP | error | Когда |
|---|---|---|
| 400 | frame_single | frame — список в любой форме: a, b, [a, b], массив или объект |
| 404 | frame_not_found | такого фрейма на сайте нет |
| 409 | lang_mismatch | id указывает на страницу другого языка, чем lang; нужен translation_of |
| 409 | ambiguous_page | слагу соответствует несколько страниц; matches[] с id, status, url, link |
| 409 | translation_group_conflict | найденная по слагу страница состоит в другой группе; current_group, target_group |
| 409 | front_page_group_conflict | то же для главной языка; её переносит только PUT /site/front-page |
| 422 | translation_link_failed | translation_of не держится; reason — polylang_missing, lang_required, origin_not_found, origin_has_no_language или same_language. link_soft: true публикует всё равно |
| 422 | unknown_language | lang не язык сайта; список — в languages |
| 422 | parent_not_found | сегмент url не нашёлся; segment называет его |
| 422 | absolute_url_in_path | в url схема или домен |
| 500 | parent_create_failed | не удалось вставить стаб-родителя |
| 501 | post_type_not_supported | type не page |
| 501 | acf_block_unsupported | в blocks тип не из core/*, onpress/* или raw |
Починка языка: POST /pages/language
Любая страница, написанная v2, получает язык — названный или язык сайта по умолчанию, с предупреждением. У страниц, записанных раньше, языка нет, и на многоязычном сайте такая страница не открывается: redirect_canonical отправляет её саму на себя. Эта ручка ставит страницам терм языка.
| Поле | Тип | Описание |
|---|---|---|
lang | string | какой язык поставить; без него — язык сайта по умолчанию |
missing | boolean | взять все страницы без языка — режим починки |
pages | array | конкретные страницы: id, слаг или путь |
statuses | string | статусы через запятую, которые просматривает режим missing; по умолчанию publish |
dry_run | boolean | показать, что изменится, ничего не писать |
curl -s -X POST "$BASE/pages/language" \
-H "Authorization: Bearer $OP_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"blog_id": 233, "pages": ["/docs/vvedenie/"], "dry_run": true}'
{
"success": true,
"blog_id": 233,
"lang": "en",
"dry_run": true,
"total": 1,
"changed": 0,
"pages": [
{ "id": 6186, "title": "Introduction", "url": "/docs/vvedenie/", "lang": { "from": "en", "to": "en" }, "changed": false }
]
}
На здоровом сайте {"missing": true, "dry_run": true} ничего не находит, и «ничего» — это код ошибки, а не пустой список. Настоящий ответ сайта 233:
{
"success": false,
"error": "nothing_to_update",
"message": "nothing to change: pass \"pages\" with the links, or \"missing\": true to fix every page without a language"
}
Всё пишется одной транзакцией. Сайт без Polylang — 409 polylang_inactive, неизвестный язык — 422 unknown_language, ссылка на страницу, которая не разрешилась, — 404 с кодом резолвера. Ручка ставит только терм языка; группы — это Переводы.
После записи: content_saved
Когда транзакция закоммичена, сервис шлёт событие onpress/content_saved на собственный домен сайта — для записанной страницы, а если изменилась группа переводов, то и для каждой страницы, которая в ней есть или была. PHP сбрасывает кэши записи и Polylang у этих страниц и их записи в страничном кэше и пересобирает индексы Yoast, из которых печатаются карта сайта и hreflang. Событие асинхронное: ответ его не ждёт, сбой только пишется в лог.
Экспорта обратно в markdown в v2 нет. Прочитать страницу как markdown — это v1, GET /wp-json/onpress/v1/pages/{id}/markdown, который принимает тот же op_-токен.