Оболочка со шапкой и подвалом
Три фрейма — шапка, подвал и оболочка статьи, которая подключает оба, — заливка, проверка через dry_run и привязка к странице. Последующая правка шапки сама доходит до всех оболочек.
Внешний вид страницы — один фрейм, её оболочка. Фрейм — шаблон, адреса у него нет: адрес держит страница, она же называет свою оболочку. Шапка и подвал к странице тоже не привязываются: оболочка подключает их по имени через op/frame {"ref":…}, поэтому шапка одна на все оболочки, которые её используют. Порядок важен: сначала заливаются части, потом оболочка, которая на них ссылается, — op/frame со слагом несуществующего фрейма ничего не печатает, и заливка об этом не предупреждает.
Разметка ниже — упрощённые фреймы стенда example.onpress.pro (блог 235). Держите каждый фрейм в своём файле и отправляйте через jq --rawfile: так кавычки внутри разметки не нужно экранировать.
1. Шапка: site__header
Логотип, главное меню и переключатель языка. Логотип заполняют бинды: data-op-bind подставляет атрибуты и текст из словаря токенов на каждом запросе. op/menu печатает меню из области темы header, op/lang-switcher — языки сайта из Polylang.
<header class="site-header">
<a class="site-header__logo" href="/" data-op-bind='{"href":"site.home_url"}'>
<img class="site-header__logo-img" width="24" height="24" alt="" data-op-bind='{"src":"site.logo_url","alt":"site.name"}'>
<span class="site-header__logo-text" data-op-bind='{"text":"site.name"}'>Site</span>
</a>
<nav class="site-header__nav"><!-- wp:op/menu {"place":"header"} /--></nav>
<div class="site-header__lang"><!-- wp:op/lang-switcher {"display":"dropdown","show":"name"} /--></div>
</header>
data-op-bind держите в одинарных кавычках. В двойных атрибут обрывается на первой кавычке внутри JSON, и заливка отвечает предупреждением именно об этом.
Сначала проверьте через dry_run: придёт полный отчёт разбора, а сохранено ничего не будет.
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/frames" -d "$(jq -n --rawfile html header.html \
'{blog_id: 233, slug: "site__header", name: "Site: header", html: $html, dry_run: true}')"
{
"success": true,
"dry_run": true,
"applied": false,
"blog_id": 233,
"frame": {
"id": "6a3e0e2cd2689e45",
"name": "Site: header",
"slug": "site__header",
"block": "site",
"element": "header",
"modifier": null,
"bytes": 527,
"…": "…"
},
"inserts": { "op/menu": 1, "op/lang-switcher": 1 },
"inserts_total": 2,
"sources": [],
"frame_refs": [],
"binds": 3,
"renders_on_request": false,
"has_doctype": false,
"has_body": false,
"warnings": []
}
binds: 3 — три атрибута data-op-bind, inserts — то, что подставит печать. renders_on_request: false значит, что шапка печётся по разу на язык и отдаётся из испечённой копии. Тот же запрос без dry_run сохраняет фрейм; в ответе тогда "applied": true и поле bake с итогом печати.
2. Подвал: site__footer
Меню подвала колонками, строка копирайта и ссылки на соцсети. op/footer-columns читает меню в областях footer_1, footer_2 и footer_3; op/copyright подставляет {year} и {name}; op/socials печатает ссылки из настроек сайта (POST /onpress/v1/settings/socials), а если их нет — ничего.
<footer class="site-footer">
<div class="site-footer__columns"><!-- wp:op/footer-columns /--></div>
<div class="site-footer__bottom">
<!-- wp:op/copyright {"text":"© {year} Example"} /-->
<!-- wp:op/socials {"place":"footer"} /-->
</div>
</footer>
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/frames" -d "$(jq -n --rawfile html footer.html \
'{blog_id: 233, slug: "site__footer", name: "Site: footer", html: $html, dry_run: true}')"
{
"success": true,
"dry_run": true,
"applied": false,
"blog_id": 233,
"frame": { "id": "757fadef9689737e", "slug": "site__footer", "block": "site", "element": "footer", "bytes": 263, "…": "…" },
"inserts": { "op/footer-columns": 1, "op/copyright": 1, "op/socials": 1 },
"inserts_total": 3,
"binds": 0,
"renders_on_request": false,
"warnings": [],
"…": "…"
}
3. Оболочка: article
В оболочке шапка, заголовок страницы, её текст и подвал. op/content — место, куда печатается содержимое самой страницы; заголовок приходит биндом page.title.
<div class="article-layout">
<!-- wp:op/frame {"ref":"site__header"} /-->
<main class="article">
<h1 class="article__title" data-op-bind='{"text":"page.title"}'></h1>
<div class="article__body"><!-- wp:op/content /--></div>
</main>
<!-- wp:op/frame {"ref":"site__footer"} /-->
</div>
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/frames" -d "$(jq -n --rawfile html article.html \
'{blog_id: 233, slug: "article", name: "Article", html: $html, dry_run: true}')"
{
"success": true,
"dry_run": true,
"applied": false,
"blog_id": 233,
"frame": { "id": "72a69d4c3ab10da1", "slug": "article", "block": "article", "element": null, "bytes": 300, "…": "…" },
"inserts": { "op/frame": 2, "op/content": 1 },
"inserts_total": 3,
"sources": [],
"frame_refs": ["site__header", "site__footer"],
"binds": 1,
"renders_on_request": true,
"live_reasons": ["inserts: op/content", "data-op-bind uses page.* tokens"],
"has_doctype": false,
"has_body": false,
"warnings": []
}
frame_refs — список фреймов, от которых оболочка зависит: по нему реестр перепекает оболочку при правке шапки и переписывает ссылку при её переименовании. renders_on_request: true для оболочки нормально: она каждый раз печатает другую страницу, поэтому испечённой копии у неё нет, а live_reasons объясняет почему.
4. Поставить страницу в оболочку
У страницы ровно один фрейм. Либо привяжите существующую страницу:
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/frames/article/assign" -d '{"blog_id": 235, "page": "/pomelli-workflow/"}'
{
"success": true,
"blog_id": 235,
"frame": { "id": "91399e3637b7aa2b", "slug": "article" },
"page": { "id": 6703, "title": "Pomelli workflow", "url": "/pomelli-workflow/" },
"meta_key": "_onpress_frame",
"cleared": false,
"bake": { "status": "ok" }
}
либо задайте его при публикации строкой frame: article в шапке POST /pages/markdown — тогда ответ назовёт его в frame: {id, slug, meta_key}. Оба способа пишут одну и ту же мету страницы _onpress_frame с id фрейма. {"page": …, "clear": true} снимает привязку, и страница печатается собственным шаблоном темы.
Адрес тоже держит страница, поэтому лендинг — это те же два шага: разметка уезжает фреймом, а страница по /promo/ его называет (с пустым телом, если документ целиком печатает фрейм). Привязка решает и вопрос статики: в этой оболочке article стоит op/content, значит она печатается на запросе и её страницы рисует PHP; оболочка без страницезависимых вставок пекётся, и её страницы отдаются файлами. Переключать ничего не нужно.
5. Поправить шапку потом
Разметка шапки заменяется через PUT /frames/site__header (то же тело, что у POST, без dry_run). Каждый фрейм, у которого site__header в frame_refs, перепекается в том же вызове и попадает в rebaked:
{
"success": true,
"applied": true,
"updated": true,
"frame": { "slug": "site__header", "…": "…" },
"bake": { "status": "ok", "…": "…" },
"rebaked": {
"status": "ok",
"total": 1,
"baked": 1,
"frames": [
{ "id": "72a69d4c3ab10da1", "slug": "article", "because": ["nested frames: site__header"], "bake": "ok" }
]
}
}
Берутся только прямые включающие: фрейм, который вставляет article, из-за шапки не перепекается. Если фрейм никто не включает, поля rebaked нет. Меню, поправленное в wp-admin, API не видит — перепеките вручную: POST /frames/rebake {"menus": true}.
Проверка
GET /frames/site__header?blog_id=233 отвечает разметкой и "referenced_by": ["article"]. GET /frames?blog_id=233&block=site показывает обе части. Привязанная страница открывается со шапкой, своим текстом и подвалом; страница без фрейма по-прежнему печатается шаблоном темы.