Меню
Меню навигации читаются источниками данных menu.* и меняются ручками /menus. Как хранится меню, почему имя темы входит в ключ назначения и что такое невалидный пункт.
Меню в OnPress — обычное меню WordPress. Ручки v2 закрывают его целиком, но пункты меню читаются не здесь.
Читать — источником, писать — ручкой
Содержимое меню читается источниками данных menu.header, menu.footer_1, menu.footer_2 и menu.footer_3: читаешь состояние для разметки — это источник, меняешь состояние — это ручка (см. Данные). Фрейм печатает меню вставкой op/menu, которая читает то же самое (см. Вставки). Поэтому ручки /menus только пишут.
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/data/menu.header?blog_id=233&lang=ru"
Единственное чтение здесь — GET /menus: список меню и того, куда они назначены. Это состояние управления, а не содержимое, и без него меню нечем адресовать.
Как хранится меню
Меню — терм таксономии nav_menu. Пункт — запись типа nav_menu_item: позиция в menu_order, связь с термом меню через связи термов, тип, цель, адрес, классы и родительский пункт — в метаполях.
Назначение в область темы лежит в опции theme_mods_<тема> под ключом nav_menu_locations. На многоязычном сайте главная карта — polylang.nav_menus[<тема>][<область>][<язык>], потому что Polylang подменяет карту областей на лету под язык запроса. Тема регистрирует четыре области: header, footer_1, footer_2 и footer_3.
Имя темы входит в ключ
Обе карты ключуются именем темы, и это важнее, чем кажется. Запишете не в ту тему — ничего громко не сломается: меню назначено, а область на странице просто пуста. Тема берётся из опции сайта stylesheet, если не передать theme, и каждый ответ говорит, какая использовалась.
Ловушка стенда: onpress против onpress-v2. На стенде mu-плагин подменяет тему некоторых сайтов на onpress-v2 фильтром и ничего не пишет в базу. Тема, записанная в базе, и тема, которая рисует страницу, поэтому могут расходиться, а назначения копятся под обоими именами. На сайте 233 сейчас в базе onpress-v2, текущие подвалы назначены там, а старые подвалы по-прежнему назначены под onpress:
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/menus?blog_id=233"
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/menus?blog_id=233&theme=onpress"
{
"success": true,
"blog_id": 233,
"theme": "onpress-v2",
"total": 14,
"menus": [
{ "id": 84, "name": "footer left en", "slug": "footer-left-en", "items": 10, "assigned": [] },
{ "id": 83, "name": "header en", "slug": "header-en", "items": 3, "assigned": [{ "location": "header", "lang": "en" }] },
{ "id": 143, "name": "Документация", "slug": "footer-docs-en", "items": 5, "assigned": [{ "location": "footer_1", "lang": "en" }] },
{ "…": "…" }
],
"…": "…"
}
{
"success": true,
"blog_id": 233,
"theme": "onpress",
"total": 14,
"menus": [
{ "id": 84, "name": "footer left en", "slug": "footer-left-en", "items": 10, "assigned": [{ "location": "footer_1", "lang": "en" }] },
{ "id": 83, "name": "header en", "slug": "header-en", "items": 3, "assigned": [{ "location": "header", "lang": "en" }] },
{ "id": 143, "name": "Документация", "slug": "footer-docs-en", "items": 5, "assigned": [] },
{ "…": "…" }
],
"…": "…"
}
Отсюда практика: сравнивайте обе карты, прежде чем писать, и передавайте theme только тогда, когда сознательно пишете в карту другой темы. Пустой assigned у всех меню значит «вы смотрите не в ту тему», а не «ничего не назначено». Если область на отрисованной странице пуста, а GET /menus показывает её назначенной, первым делом проверяйте расхождение имён тем. Тот же параметр theme принимают источники menu.*, все записи ниже и адресация ~область.
Адресация меню
{ref} в пути — id терма меню, его слаг, имя или область с тильдой впереди: ~header означает «то меню, что сейчас стоит в шапке» и разрешается с учётом lang и theme, если они переданы (без lang — по языку сайта по умолчанию). Имя, которое носят несколько меню, — 409 ambiguous_menu с id в candidates. На сайте 233 два меню называются Документация (143 и 146), так что там работают только id, слаг или область. Область, в которой ничего не стоит, — 404 menu_not_assigned.
Невалидные пункты
Пункт, ведущий на удалённую, помещённую в корзину или неопубликованную страницу, для ядра WordPress невалиден: он не печатается, а меню целиком из таких пунктов выглядит пустым без единого объяснения. Источник печатает ровно то же и сообщает число таких пунктов в meta. У старого центрального подвала сайта 233 под темой onpress девять пунктов, пять из них невалидны:
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/data/menu.footer_2?blog_id=233&lang=en&theme=onpress"
{
"success": true,
"blog_id": 233,
"name": "menu.footer_2",
"origin": "system",
"contextual": false,
"page_id": null,
"needs_page": false,
"description": "Items of the menu assigned to the \"footer_2\" location of the active theme; when a language is given, the Polylang map is used",
"total": 4,
"meta": { "menu_id": 87, "invalid_items": 5 },
"items": [
{ "id": 5885, "title": "Widgets", "url": "/title/", "type": "post_type", "object_id": 5847, "parent": 0, "order": 1, "children": [], "…": "…" },
{ "…": "…" }
]
}
Запись ведёт себя так же, только раньше: каждая ссылка разрешается до того, как что-то записано. Пункт на несуществующую страницу — 409 menu_items_unresolved, не пишется ничего. Пункт на существующую, но неопубликованную страницу — 409 menu_items_invalid, каждый виновник перечислен с at, page, status, title и reason. "allow_invalid": true разрешает записать всё равно; тогда ответ помечает каждый пункт полями valid и reason, и сознательно ранняя ссылка видна, а не молчалива.
GET /menus
Какие меню есть и куда назначены.
| Параметр | Что делает |
|---|---|
theme | чьи карты областей читать; по умолчанию stylesheet сайта |
curl -s -H "Authorization: Bearer $OP_TOKEN" "$BASE/menus?blog_id=235"
{
"success": true,
"blog_id": 235,
"theme": "onpress-v2",
"locations": ["header", "footer_1", "footer_2", "footer_3"],
"default_lang": "en",
"read_items_at": "/data/menu.{location}",
"total": 6,
"menus": [
{ "id": 165, "name": "Articles", "slug": "pomelli-footer-2-en", "items": 5, "assigned": [{ "location": "footer_2", "lang": "en" }] },
{ "id": 164, "name": "Guide", "slug": "pomelli-footer-1-en", "items": 4, "assigned": [{ "location": "footer_1", "lang": "en" }] },
{ "id": 166, "name": "Project", "slug": "pomelli-footer-3-en", "items": 3, "assigned": [{ "location": "footer_3", "lang": "en" }] },
{ "id": 167, "name": "Гид", "slug": "pomelli-footer-1-ru", "items": 4, "assigned": [{ "location": "footer_1", "lang": "ru" }] },
{ "id": 169, "name": "Проект", "slug": "pomelli-footer-3-ru", "items": 3, "assigned": [{ "location": "footer_3", "lang": "ru" }] },
{ "id": 168, "name": "Статьи", "slug": "pomelli-footer-2-ru", "items": 4, "assigned": [{ "location": "footer_2", "lang": "ru" }] }
]
}
items — число опубликованных пунктов, невалидные тоже считаются. assigned сводит обе карты: собственную карту темы (она показывается с языком по умолчанию) и карту Polylang.
POST /menus
Заводит меню и, если попросить, сразу назначает и наполняет его.
| Поле | Тип | Что делает |
|---|---|---|
name | строка | обязательно |
slug | строка | из имени, если не задан; делается уникальным |
location | строка | сразу назначить: header, footer_1, footer_2, footer_3 |
lang | строка | язык для карты Polylang |
theme | строка | тема, в карту которой писать; по умолчанию stylesheet сайта |
items | массив | сразу наполнить, в той же форме, что у PUT …/items |
allow_invalid | bool | принять пункты, ведущие на неопубликованные страницы |
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/menus" -d '{
"blog_id": 233,
"name": "Quick links ru",
"items": [ { "page": "/ru/docs-ru/vvedenie/" }, { "page": 6235, "title": "Старт" }, { "url": "https://onpress.pro/", "title": "OnPress" } ]
}'
PUT /menus/{ref}/items
Заменяет всё дерево пунктов в одной транзакции. Это основной путь для агента, и форма та же, что у сайдбара документации.
Пункт — {"page": …} с id, слагом или путём страницы этого сайта либо {"url": "https://…"} для всего остального; путь вида /pricing/ в url разворачивается относительно адреса сайта. title перебивает подпись (пункт-страница без него показывает заголовок страницы, как это делает ядро), target, classes и children делают ровно то, что называют. Голое значение вместо объекта считается ссылкой на страницу. lang и theme важны, только когда {ref} — это ~область.
curl -s -X PUT -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/menus/~header/items" -d '{
"blog_id": 233, "theme": "onpress-v2",
"items": [
{ "page": 6183, "title": "Документация", "children": [ { "page": 6186 }, { "page": 6187 } ] },
{ "url": "https://onpress.pro/", "title": "OnPress", "target": "_blank", "classes": "btn btn--primary" }
]
}'
{
"success": true,
"blog_id": 233,
"menu": { "id": 123, "name": "Шапка", "slug": "shapka" },
"total": 4,
"invalid": [],
"items": [
{ "id": 6202, "at": "0", "title": "Документация", "type": "post_type", "page": 6183, "url": "/docs/", "parent": 0, "order": 1, "valid": true },
{ "id": 6203, "at": "0.0", "title": "Введение", "type": "post_type", "page": 6186, "url": "/docs/vvedenie/", "parent": 6202, "order": 2, "valid": true }
],
"read_items_at": "/data/menu.{location}"
}
Ответ снят при разработке ручки и приведён в справочнике API. at — координата пункта в присланном дереве ("0.0" — первый ребёнок первого пункта), parent — id родительского пункта, order идёт сквозной нумерацией по всему меню: вложенность задаёт родитель, а не номер. В ответе есть и rebaked.
Остальные ручки пунктов
POST /menus/{ref}/items добавляет один пункт: {"item": {…}}, при желании с "parent": <id пункта>. PATCH /menus/{ref}/items/{id} меняет title, page, url, target, classes или parent; передача page превращает произвольную ссылку в ссылку на страницу, а url возвращает обратно. DELETE /menus/{ref}/items/{id} удаляет пункт вместе с его веткой, иначе осиротевшие дети всплыли бы на верхний уровень. POST /menus/{ref}/reorder берёт {"order": [id…], "parent": <id или 0>} и перенумеровывает один уровень: неназванные остаются после названных, соседние ветки не трогаются.
PATCH /menus/{ref} с name и/или slug переименовывает само меню; назначения и пункты остаются как были.
PUT /menus/{ref}/location
Ставит меню в область темы.
curl -s -X PUT -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/menus/footer-docs-ru/location" -d '{"blog_id": 233, "location": "footer_1", "lang": "ru"}'
С lang пишется карта Polylang. Без lang или когда lang — язык по умолчанию, пишется и собственная nav_menu_locations темы: именно её Polylang отдаёт запросу без языка и её же читает сайт без Polylang. Каждая запись перечитывается и сверяется; если карта значение не удержала, вызов падает с 500 menu_assignment_not_stored, и ничего не фиксируется. В ответе — theme, меню и assigned с отметками о том, что записано (polylang, theme_mods).
Неизвестная область — 400 unknown_location со списком зарегистрированных в locations, неизвестный язык — 400 unknown_language со списком языков сайта, язык на сайте без Polylang — 409 polylang_inactive.
DELETE /menus/{ref}
Удаляет меню со всеми пунктами и освобождает все занятые им области в обеих картах выбранной темы: область, указывающая на исчезнувший терм, — это ещё одно тихо пустое меню.
Изменение меню перепекает фреймы
Меню внутри фрейма — это вставка, а вставка материализуется при выпечке фрейма. Поэтому каждая запись здесь, кроме переименования, заново печёт все фреймы со вставкой op/menu, по разу на каждый язык сайта, и докладывает об этом полем rebaked. Меню, поправленное в админке мимо API, этого не вызывает: фрейм держит старую копию, пока его не испекут через POST /frames/rebake с {"menus": true}. См. Фреймы и Как это устроено.
Коды ошибок
| Код | HTTP | Когда |
|---|---|---|
menu_name_required | 400 | POST /menus без имени |
location_required | 400 | назначение без области |
unknown_location | 400 | тема не регистрирует такую область |
unknown_language | 400 | такого языка на сайте нет |
bad_items | 400 | items нет или это не массив |
bad_reorder | 400 | reorder без списка order |
nothing_to_update | 400 | PATCH без изменяемых полей |
menu_ref_required | 400 | ссылка на меню не передана |
menu_not_found | 404 | меню с таким id, слагом или именем нет |
menu_not_assigned | 404 | указана ~область, но в ней ничего не стоит |
item_not_found | 404 | такого пункта в меню нет или он не на этом уровне |
page_not_found | 404 | пункт ведёт на несуществующую страницу |
ambiguous_menu | 409 | имя носят несколько меню |
menu_items_unresolved | 409 | часть пунктов не разобрана, ничего не записано; разбор с at в errors |
menu_items_invalid | 409 | часть пунктов ведёт на страницы, которых посетитель не увидит; allow_invalid пишет всё равно |
polylang_inactive | 409 | передан язык, а Polylang на сайте нет |
bad_parent | 409 | пункт сделан родителем самого себя |
menu_assignment_not_stored | 500 | карта областей не удержала значение при перечитывании; ничего не зафиксировано |
Внутри menu_items_unresolved у каждой записи свой code: page_not_found и остальные коды адресации страниц либо item_target_required для пункта без page и без url.
Дерево документации слева — не меню: оно собирается из самих страниц, см. Сайдбар документации.