Как это устроено
Что происходит за вызовом API: как работа делится между Node и PHP, где всё хранится и как фреймы пекутся, печатаются на запросе и перепекаются.
Пользоваться API можно и не читая эту страницу. Она объясняет форму ответов — почему запись фрейма возвращает bake и rebaked, почему запись страницы не ждёт кэшей — и что делать, если правка сделана мимо API.
Node владеет контрактом, PHP исполняет
Все публичные ручки v2 живут в сервисе на Node (nodejs/ в репозитории сайта). Node владеет контрактом, авторизацией по токену op_, выбором сайта сети и хранением: он пишет прямо в таблицы WordPress одной транзакцией, не поднимая WordPress, поэтому сохранение страницы не стоит полной загрузки ядра, плагинов и темы.
PHP из схемы не уходит и уйти не может: он исполняет всё, для чего нужен сам WordPress. Развернуть вставки <!-- wp:op/… /--> — это parse_blocks() плюс render_block() по зарегистрированным типам блоков; атрибуты data-op-bind переписываются через WP_HTML_Tag_Processor; меню с правилами видимости, языки Polylang и индексы Yoast — код на PHP. Node обращается к нему через внутренний контур — POST /wp-json/onpress/v1/internal/frame-bake и …/internal/content-saved на домене самого сайта, закрытые общим секретом в заголовке X-OnPress-Internal-Token и недоступные снаружи.
Сами вы этот контур не вызываете. Виден он в одном месте — в поле bake ручек фреймов, которое говорит, дошла ли печать до PHP.
bake.status | Что значит |
|---|---|
ok | PHP испёк фрейм; его ответ в bake.response |
unavailable | на сайте нет ручки печати; исходник сохранён, испечь можно позже |
failed | PHP ответил ошибкой или не уложился во время; причина в bake.reason |
skipped | печать выключена для этого вызова ("bake": false) или в настройках сервиса |
Неудачная печать никогда не отменяет запись: исходник уже в базе, и следующее сохранение или вызов перепечки его опубликует.
Только через ручку
Правило разработки v2: любая операция делается через ручку. Нет ручки — её сначала пишут, потом ею работают. Всё остальное на этой странице — следствие: сервис держит реестры, зависимости и перепечку согласованными только для тех правок, которые видит. Для невидимых правок есть страховка, она описана в конце, но это страховка, а не способ работы.
Хранение: опции одного сайта
| Опция | Что в ней |
|---|---|
onpress_frames | реестр фреймов: слаг, имя, части БЭМ, размер, хеш, inserts, sources, frame_refs, binds, renders_on_request |
onpress_frame_<id> | разметка фрейма ровно в том виде, в каком её залили |
onpress_frame_<id>_baked_<lang> | испечённая копия для одного языка; копия языка по умолчанию лежит ещё и под onpress_frame_<id>_baked |
onpress_datasets | реестр наборов данных |
onpress_dataset_<name>, onpress_dataset_<name>__<lang> | набор и его языковые варианты |
onpress_custom_css, onpress_design_tokens | CSS сайта и токены дизайна, см. Стили |
onpress_apps | реестр аппов, он же реестр страниц, опубликованных статикой: page-<id>, путь страницы в custom_url |
Страница привязана к своему фрейму метой _onpress_frame, в которой лежит id фрейма, — поэтому переименование фрейма страниц не касается. Своего адреса у фрейма нет: всё, что сайт отдаёт по адресу, — страница.
Всё хранится в опциях того сайта сети, к которому относится, и никогда — в сетевых опциях. Причина конкретная: стенд делит базу с продом, и сетевая опция выкатилась бы сразу на все сайты. По той же причине каждый запрос называет свой сайт, а запрос без него уходит в блог 1 — см. Аутентификация.
После записи страницы: content_saved
Запись страницы сначала коммитится. Только после коммита Node отправляет событие onpress/content_saved в PHP на домен самого сайта и ответа не ждёт: ответ API уходит, как только данные легли в базу. На стороне PHP событие сбрасывает объектный кэш, пересобирает индекс Yoast и прогревает кэш страниц.
Событие идёт на домен сайта, а не на общий адрес с blog_id, потому что Yoast внутри switch_to_blog() молча пропускает пересборку. Когда меняется группа переводов, событие уходит каждой странице, которая была или стала её членом, — чтобы hreflang появился и исчез у всех. Практическое следствие: сразу после записи закэшированная страница может ещё мгновение показывать старую версию.
Печать фрейма: испечённая копия или на запросе
Фрейм печатается одним из двух способов.
Испечённая копия. PHP один раз разворачивает вставки и бинды и кладёт результат в onpress_frame_<id>_baked_<lang>. Фреймы, чьи вставки зависят от языка (op/menu, op/lang-switcher, op/repeat), пекутся по разу на каждый язык сайта — русская страница получает русское меню; остальные пекутся один раз. Из этой копии публикуются страницы, привязанные к фрейму: каждая становится записью реестра аппов page-<id> с путём в custom_url и файлом storage/apps/{blog_id}/page-<id>/index.html, который nginx отдаёт без PHP. Публикация статикой — свойство страницы: фрейм только шаблон, публиковать его негде.
На запросе. Фрейм, чей результат зависит от показываемой страницы, получает в реестре renders_on_request: true и испечённой копии не имеет вовсе — прежние копии удаляются при следующей печати. Одна копия показала бы текст одной страницы на другой; так уже было, поэтому правило жёсткое. Фрейм печатается на запросе, если в нём есть хотя бы одно из:
- вставки
op/content,op/pager,op/docs-nav,op/outline,op/share-ai,op/featured-image,op/terms,op/assistant,op/faq-schema(и легасиop/partial); data-op-bindс токеномpage.*;op/repeat, у которогоmatchберёт значение со страницы (page.slugи подобные);op/repeatпо контекстному источнику —pages.children,breadcrumbsи другие системные источники, считающиеся от страницы;op/frame, вкладывающий фрейм, который сам печатается на запросе.
POST /frames сообщает об этом полями renders_on_request и live_reasons. "empty":"frame" у повторителя фрейм на запрос не переводит: пустой список прячет фрейм и в копии. Страница, чей фрейм печатается на запросе, статикой не публикуется никогда — публиковать нечего, её рисует тема.
Страницы сайта печатает тема через onpress_page_frame_html(): она разворачивает фрейм страницы из исходника на каждом запросе, вместе с вложенными через op/frame фреймами и в контексте показываемой страницы. Атрибуты data-op-bind подставляются в этом же проходе, поэтому они всегда актуальны.
Исходник не трогается
Хранимая разметка — всегда исходник, со всеми вставками <!-- wp:op/… /--> и всеми data-op-bind. Подстановка происходит при печати, а не при заливке. На этом держится пересборка: после смены меню или подвала есть из чего печь заново, и две печати подряд дают тот же результат, что одна.
Перепечка автоматом
Реестр хранит зависимости каждого фрейма: inserts по именам вставок, sources — наборы, которые читают его повторители, frame_refs — вложенные фреймы. Вопрос «что изменилось» превращается в «кого печь» одной выборкой, и сервис делает это после каждой своей записи:
| Запись | Какие фреймы перепекаются |
|---|---|
фрейм (POST, PUT, PATCH с переименованием) | сам фрейм и все, кто его вкладывает, транзитивно по frame_refs |
меню (/menus) | фреймы со вставкой меню |
набор данных (PUT, PATCH, DELETE /data/{name}) | фреймы, чьи повторители читают этот набор |
Результат приходит в rebaked: каждый фрейм, язык, причина выбора (because) и итог печати. Печать заодно обновляет статические копии страниц, которые называют этот фрейм; PHP перечисляет их в bake.response.published.pages как {id, url, published, path, bytes, reason}. Токены дизайна и произвольный CSS ничего не запускают: они попадают на страницу CSS-ом в момент запроса и в разметку не впекаются. У настроек, языков и страниц автоматического повода в сервисе нет — после таких правок перепекайте вручную.
Перепечка вручную: POST /frames/rebake
Для правок, которых сервис не видел: меню поправили в wp-admin, опцию записали руками, обновили сам печатник.
| Поле | Тип | Что делает |
|---|---|---|
all | bool | перепечь все фреймы сайта |
menus | bool | фреймы со вставкой меню (op/menu) |
languages | bool | фреймы с переключателем языков (op/lang-switcher) |
settings | bool | фреймы с op/socials, op/copyright, op/footer-columns |
pages | bool | фреймы, чьи вставки выводят списки страниц |
datasets | array | фреймы, чьи повторители читают эти наборы |
frames | array | фреймы, которые вкладывают эти фреймы, по id или слагу |
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/frames/rebake" -d '{"blog_id": 233, "menus": true}'
{
"success": true,
"blog_id": 233,
"changed": { "menus": true },
"rebaked": {
"status": "ok",
"total": 5,
"baked": 5,
"frames": [
{ "id": "aad2f51f8ea02682", "slug": "site__header", "lang": "en", "because": ["menus: op/menu"], "bake": "ok" },
{ "id": "aad2f51f8ea02682", "slug": "site__header", "lang": "ru", "because": ["menus: op/menu"], "bake": "ok" }
]
}
}
В каждой записи rebaked.frames есть ещё pages — номера страниц, которые печатаются этим фреймом и чьи статические копии обновила печать, — если такие страницы есть. rebaked.status — ok, если испеклось всё, partial, если часть, failed, если ничего, и nothing, если от названного не зависит ни один фрейм. Вызов, который ничего не называет, — 400 nothing_to_rebake. За один вызов печётся не больше 25 фреймов; если зависимых больше, в ответе будут skipped и cap, и тогда выход — {"all": true} в несколько заходов.
Правки мимо API: PHP-сторож
Реестры onpress_frames и onpress_datasets ведёт Node, но о правке мимо него узнать не может: опцию переписали руками, набор поправили в wp-admin, разметку подменили миграцией. Тогда реестр помнил бы старые размер, хеш и, хуже всего, старые зависимости, и адресная перепечка била бы мимо. Поэтому при такой правке запись реестра пересчитывает PHP (class-frames-registry.php) портом того же кода разбора, что в сервисе, — записи от Node и от PHP не отличаются.
Рядом class-frames-watch.php перепекает сами фреймы по wp_update_nav_menu, save_post и updated_option. Хук только помечает устаревшее, а печать идёт на shutdown, уже после отправки ответа, небольшими порциями из очереди. На WP-Cron он не полагается.
Эта страховка ловит обычные случаи. Для всего остального, и когда есть сомнения, — POST /frames/rebake.
Связанные страницы
Фреймы — заливка, имена, связи. Вставки и Бинды — что подставляется. Данные — наборы и источники. Переименовать фрейм — переименование с перепечкой по шагам.