Страницы
Страница — единственное, что на сайте открывается по адресу. В v2 её пишут одним способом — markdown с frontmatter в POST /pages/markdown — и читают тремя: списком, целиком и выгрузкой обратно в markdown. Отдельные поля (выдержку и фрейм) меняет PATCH, убирает DELETE.
Всё ниже — ручки сайта, то есть пути вида $API/site/$SITE/pages…. Полные параметры и ответы — в справочнике страниц.
Страница = markdown + фреймы
В теле страницы в идеале только markdown и вызовы фреймов. Разметка живёт во фреймах, данные — в теле и во frontmatter. Тогда текст читается и правится как текст, а вид блока меняется в одном фрейме сразу на всех страницах.
Пример — демо-страница bota.chat/base/canva/. Чат ассистента там стоит во фрейме над телом, а тело первой строкой передаёт ему агента вставкой onpress/assistant:
---
name: "Canva"
frame: "article"
data.rating: 5
---
<!-- onpress/assistant {"id":"x8OA7Azs4hBy","agent":"canva","agent_view":"profile"} /-->
# Canva: …
<!-- onpress/frame {"name":"rating"} /-->
<!-- onpress/frame {"name":"outline"} /-->
Текст статьи обычным markdown.
## FAQ
<!-- onpress/frame {"name":"faq"} -->
### Вопрос?
Ответ.
<!-- /onpress/frame -->
Фрейм rating — пять звёзд и число из frontmatter в атрибуте. Условий во фрейме нет: закраску делает CSS по значению атрибута.
<div class="rating" data-op-bind='{"data-rating":"page.data.rating"}'><span class="rating__star">★</span><span class="rating__star">★</span><span class="rating__star">★</span><span class="rating__star">★</span><span class="rating__star">★</span></div>
.rating{display:flex;gap:2px;color:var(--line-color)}
.rating[data-rating="1"] .rating__star:nth-child(-n+1),
.rating[data-rating="2"] .rating__star:nth-child(-n+2),
.rating[data-rating="3"] .rating__star:nth-child(-n+3),
.rating[data-rating="4"] .rating__star:nth-child(-n+4),
.rating[data-rating="5"] .rating__star:nth-child(-n+5){color:#f5a524}
Это только визуальный пример: рейтинг, который страница поставила себе сама, без отзывов, размечать schema.org нельзя.
Фрейм outline — оглавление в теле:
<div class="not-prose"><!-- onpress/outline {"shell":"spoiler"} /--></div>
not-prose нужен, когда оглавление стоит в теле внутри колонки .prose: иначе на список ложатся стили текста. Оглавление в теле собирается после всего тела, заголовки и якоря в нём те же, что у оглавления во фрейме страницы.
Фрейм faq получает вопросы слотом и выводит аккордеон и FAQPage — см. рецепт FAQ. Значения frontmatter во фреймах — page.data.*, параметры и слот — вставка onpress/frame.
Публикация
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}')"
page.md:
---
name: "Тарифы"
slug: "tarify"
url: "/tarify/"
lang: "ru"
status: "publish"
frame: "article"
seo_title: "Тарифы OnPress"
seo_description: "Сколько стоит OnPress и что входит в каждый тариф."
excerpt: "Три тарифа и что в них входит."
---
Текст страницы в **markdown**.
Все поля frontmatter, их смысл и то, что с ними происходит, — на странице frontmatter. Что можно писать в теле — в синтаксисе markdown.
Ответ говорит всё, что нужно проверить после записи:
{
"success": true,
"updated": false,
"id": 6428,
"status": "publish",
"lang": "ru",
"parent": 0,
"url": "/tarify/",
"link": "https://example.onpress.pro/tarify/",
"visible": true,
"translations": { "ru": 6428 },
"frame": { "id": "c33c7d69d5a35456", "slug": "article", "meta_key": "_onpress_frame" },
"printed": { "how": "on_request", "url": "/tarify/", "link": "https://example.onpress.pro/tarify/", "frame": { "id": "c33c7d69d5a35456", "slug": "article", "renders_on_request": true } },
"warnings": [],
"edit_url": "https://example.onpress.pro/wp-admin/post.php?post=6428&action=edit",
"view_url": "https://example.onpress.pro/tarify/",
"preview_url": null,
"meta": { "name": "Тарифы", "slug": "tarify", "…": "…" },
"blocks": [ { "blockName": "core/paragraph", "…": "…" } ]
}
| Поле | Что проверять |
|---|---|
updated | false — создана новая страница, true — обновлена существующая |
url / link | путь страницы в дереве и адрес, по которому её отдаёт сайт (с префиксом языка, если язык не основной) |
visible | видит ли страницу посетитель: false у черновика и у страницы под неопубликованным родителем |
printed.how | как страница отдаётся: static, on_request, not_published или theme (фрейма нет — на сайте v2 это 404), подробно в сборке |
warnings | всё, что сделано не так, как просили: проигнорированный template, незнакомые ключи frontmatter, незаписанные служебные ключи с _, сменившийся слаг, неопубликованный родитель |
slug_changed | есть, только если записанный слаг отличается от запрошенного (занят в том же языке под тем же родителем) |
preview_url | у неопубликованной страницы — адрес вида /?page_id=…, у опубликованной null |
Пробного прогона у публикации нет. dry_run: true в теле даёт 400 dry_run_not_supported: публикация всегда пишет. Чтобы посмотреть результат до выхода в свет, публикуйте с status: draft — в ответе будут блоки, адрес и preview_url.
Какую страницу обновит публикация
Публикация — это upsert. Существующая страница ищется так:
- Есть
idво frontmatter и это страница — обновляется она. Если её язык не совпадает сlang—409 lang_mismatch: так защищён оригинал от перевода, в который по ошибке скопировалиid(для переводов естьtranslation_of). - Иначе ищется страница с тем же слагом под тем же родителем и в том же языке, среди статусов
publish,draft,pending,private. Нашлась одна — обновляется она. Нашлось несколько —409 ambiguous_pageсо спискомmatches, уточнитеidилиlang. Страница без языка (осталась с тех времён, когда язык не проставлялся) считается своей для любого языка. - Не нашлась — создаётся новая, в конец списка своих соседей.
Поиск по слагу отключается slug_lookup: false — тогда без id всегда создаётся новая страница.
Следствие, которое ломает переносы. Родитель — часть ключа поиска. Если опубликовать страницу /a/page/ с url: "/b/page/" без id, API не найдёт её под b и создаст вторую страницу, а старая останется на месте. Перенос страницы в другую ветку делается с id во frontmatter или ручками сайдбара — см. рецепт переноса.
Адрес и дерево
Адрес страницы — цепочка слагов её родителей и её собственного слага. Задаётся полем url: полный путь страницы, последний сегмент — сама страница. Все сегменты кроме последнего должны быть существующими страницами (в том же языке, если он есть) — иначе 422 parent_not_found с именем сегмента. Флаг create_missing_parents: true создаёт недостающих родителей пустыми черновиками — но черновик-родитель делает адрес потомка недоступным, и ответ об этом предупреждает.
Слаг берётся из поля slug, а не из url. Если slug не указан, он собирается из name транслитерацией — и последний сегмент url при этом не используется:
---
name: "Дочерняя страница"
url: "/razdel/drugoe-imya/"
---
даст адрес /razdel/dochernyaya-stranica/. Всегда пишите slug и url согласованно. Языковой префикс в url (/ru/razdel/…) распознаётся по реальному списку языков сайта и не считается уровнем дерева; язык страницы при этом задаёт только поле lang. Схема и домен в url — 422 absolute_url_in_path.
Порядок среди соседей (menu_order) при создании — последним; дальше он меняется ручками сайдбара (для страниц документации) и влияет на системные источники pages.* и на onpress/pager.
Слаг уникален среди соседей одного языка. Занятый слаг получает суффикс (tarify-2), и ответ сообщает об этом полем slug_changed и предупреждением — адрес уехал, это надо заметить.
Статусы
publish, draft, pending, private, future. По умолчанию publish. Неизвестный статус — 400 invalid_status со списком допустимых. Черновик и отложенная страница не отдаются по адресу, приватная видна только вошедшим администраторам. Страница видна посетителю, только если опубликованы она и все её предки.
Чтение
Список:
curl -s -H "Authorization: Bearer $OP_TOKEN" \
"$API/site/$SITE/pages?lang=ru&parent=0&per_page=50&orderby=menu_order" \
| jq '{lang, total, total_pages, items: [.items[] | {id, url, status, frame: .frame.slug}]}'
Фильтры: lang (или all), status (CSV или any — с корзиной), parent (id, слаг, путь или 0 для верхнего уровня), frame (слаг, id или none), slug, search (по заголовку и слагу, не по телу), page, per_page (до 200), orderby (menu_order, title, date, modified, id, slug), order. Без lang фильтра по языку нет, и ответ прямо говорит "lang": "all".
Одна страница целиком — паспорт, родители, группа переводов, frontmatter, вся значимая мета и тело в markdown:
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages/tarify" | jq '{page: .page | {id, url, frame, seo, frontmatter}, markdown}'
{ref} в пути — id, слаг или путь, где слеши заменены тильдой: /docs/api/ пишется docs~api, русская /ru/o-nas/ — ru~o-nas. Параметры: lang — в каком языке искать и с чем сверить; markdown=false — не выгружать тело (быстрее).
Только тело в markdown — ровно то, что принимает публикация:
curl -s -H "Authorization: Bearer $OP_TOKEN" "$API/site/$SITE/pages/tarify/markdown" | jq -r .markdown > tarify.md
Выгрузка отдаёт frontmatter с id, всеми документированными полями, excerpt, frame (всегда, даже пустым) и вашими собственными ключами в порядке последней публикации. Круг «выгрузил — поправил — залил» не меняет ни байта. С ?as=translation_source вместо id приходит translation_of, а status не приходит — заготовка под перевод.
Правка отдельных полей
PATCH /pages/{ref} меняет выдержку и фрейм, не трогая тело:
curl -s -X PATCH "$API/site/$SITE/pages/tarify" \
-H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
-d '{"excerpt": "Три тарифа и что в них входит.", "frame": "article"}' \
| jq '{changed, frame, static, republish: .republish.status}'
frame: null (или "", "none", false) снимает фрейм — на сайте v2 страница после этого отвечает 404. Смена фрейма сразу приводит в порядок статическую копию страницы, исход — в поле static. Остальные поля (заголовок, слаг, адрес, SEO, тело) меняются только публикацией markdown.
Удаление
# план: что будет удалено, ничего не трогая
curl -s -X DELETE "$API/site/$SITE/pages/tarify?dry_run=true" -H "Authorization: Bearer $OP_TOKEN" | jq '{mode, targets: [.targets[] | {id, url}]}'
# в корзину (по умолчанию)
curl -s -X DELETE "$API/site/$SITE/pages/tarify" -H "Authorization: Bearer $OP_TOKEN" | jq '{mode, restore}'
# безвозвратно
curl -s -X DELETE "$API/site/$SITE/pages/tarify?force=true" -H "Authorization: Bearer $OP_TOKEN" | jq '{mode}'
Страница с детьми — 409 page_has_children со списком; with_children=true удаляет поддерево снизу вверх. Главную страницу и главные страницы языков удалить нельзя (409 front_page_delete) — сначала переназначьте главную. Корзина сохраняет страницу в группе переводов, безвозвратное удаление выводит её оттуда. Статическая копия снимается в обоих случаях. Корзина видна в GET /pages?status=trash.
Язык страницы
На сайте с Polylang у страницы всегда есть язык: названный в lang или язык сайта по умолчанию (тогда ответ предупреждает). Страница без языка на многоязычном сайте не открывается вовсе — WordPress уводит её редиректом саму на себя. Если такие страницы остались, их чинит POST /pages/language:
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}' | jq '{lang, total, changed}'
Переводы, группы и hreflang — на странице языки и переводы страниц.
Частые ошибки
| Код | Причина | Что делать |
|---|---|---|
markdown_required (400) | нет поля markdown или это не строка | тело — {"markdown": "…"} |
parent_not_found (422) | сегмент url — не существующая страница этого языка | создать родителя или create_missing_parents: true |
absolute_url_in_path (422) | в url схема или домен | только путь |
ambiguous_page (409) | слаг есть у нескольких страниц | id во frontmatter или lang |
lang_mismatch (409) | id указывает на страницу другого языка | для перевода — translation_of вместо id |
unknown_language (422) | lang нет на сайте | список в languages |
invalid_status (400) | статус не из списка | publish, draft, pending, private, future |
frame_single (400) | во frame список | один слаг или id |
frame_not_found (404) | фрейма нет на этом сайте | GET /frames |
post_type_not_supported (501) | type не page | v2 пишет только страницы |
domain_mismatch (409) | domain во frontmatter — другой сайт | убрать domain, сайт выбирается адресом запроса |
page_language_mismatch (409) | id или путь с префиксом называют страницу другого языка, чем lang | адресовать страницу нужного языка или убрать lang |
Ссылка и язык. GET, PATCH и DELETE /pages/{ref} находят страницу одинаково. Голый слаг с lang ищется среди страниц этого языка: PATCH $API/site/$SITE/pages/tarify с {"lang": "ru"} правит русскую /ru/tarify/, даже если у английской тот же слаг; русской нет — 404 page_not_found с языком. Голый слаг без lang — страница основного языка. Id и путь с префиксом называют страницу сами, и lang с ними только сверяется.