Frames
A frame is ready-made markup you upload once and place anywhere: a shell, a header, an FAQ block, a whole landing. It is a template with no address of its own — the address belongs to the page.
What a frame is
A frame is ready-made markup that you upload and then place wherever you want. It has no implied size: a whole page exported from a page builder is a frame, and so are an article shell, a header, a footer and an FAQ block. A frame has no type. What it is for is said by its name, and where it goes is said by an explicit reference: a page names one frame, and a frame pulls in other frames by name.
Next to publishing markdown this is the second way to set content. Markdown is text that gets converted; a frame is finished markup served as it is. Nothing in a frame is rewritten, reformatted or sanitised: an inline <script> from a page builder survives the upload byte for byte, which is the whole point.
Do not confuse a frame with an iframe. An iframe mounts an app in a separate document; a frame is printed into the page itself. Dynamic parts inside a frame are inserts (a block comment that drops a piece of markup into the flow) and bindings (a data-op-bind attribute that sets a value inside a tag).
A frame has no address
A frame is a template, and it never opens on its own. Everything the site serves at an address is a page; the page says which frame prints it. So a frame has no url field and no link in the answers, and a url in the body of POST, PUT or PATCH /frames is 400 frame_url_not_supported.
A landing exported from a builder is not an exception. Upload the markup as a frame, then publish a page at the address and name that frame in it — the page holds the address, the frame prints the document. Step by step: A landing: a page and a shell frame.
Publishing statically follows the same rule: it is a property of the page, not of the frame. A page with a frame that is printed from a baked copy is written into the apps tree and served by nginx as a file; a page whose frame is printed on request is rendered by the theme. You do not switch this anywhere — see Printing.
Naming: block, element, modifier
The slug is the frame’s identity for people and for references, and it follows BEM: block__element--modifier. Inside a part a hyphen stands for a space.
| Slug | block | element | modifier | Recommended name |
|---|---|---|---|---|
article | article | — | — | Article |
article__faq | article | faq | — | Article: FAQ |
article__faq--simple | article | faq | simple | Article: FAQ — simple |
site__header | site | header | — | Site: header |
site-simple__header | site-simple | header | — | Site simple: header |
docs__code-cards | docs | code-cards | — | — |
The block site holds what is common to the whole site: header, footer. Other blocks are named after the entity they serve: article, docs, landing. An element is a part of its block (article__faq), a modifier is a variant of that part (article__faq--simple).
The slug is lowercased and then checked against this pattern:
/^[a-z0-9]+(?:-[a-z0-9]+)*(?:__[a-z0-9]+(?:-[a-z0-9]+)*)?(?:--[a-z0-9]+(?:-[a-z0-9]+)*)?$/
A single underscore, a second __ or --, a leading or doubled hyphen, anything outside latin letters and digits is 400 invalid_slug. Capitals are not an error: Site__Header is stored as site__header. The answer names the slug and carries pattern and hint:
{
"success": false,
"error": "invalid_slug",
"message": "slug \"site_header\" does not match the naming scheme block__element--modifier: lowercase latin letters and digits, a single hyphen joins words inside a part, \"__\" separates the element, \"--\" the modifier; e.g. article, article__faq, article__faq--simple, site__header, docs__code-cards",
"slug": "site_header",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*(?:__[a-z0-9]+(?:-[a-z0-9]+)*)?(?:--[a-z0-9]+(?:-[a-z0-9]+)*)?$",
"hint": "block__element--modifier: lowercase latin letters and digits, a single hyphen joins words inside a part, \"__\" separates the element, \"--\" the modifier; e.g. article, article__faq, article__faq--simple, site__header, docs__code-cards"
}
Old transliterated slugs such as shapka-sajta or obolochka-dokumentaciya pass the pattern but say nothing about where the frame goes; rename them with PATCH. name is free text for people; mirroring the slug in it, as in the table, keeps the list readable. Without name the frame is named after its slug. block, element and modifier are derived from the slug and returned in every frame description (null where the part is absent); they are never written separately.
Where a frame lives
In the options of its own network site. Nothing is stored as files.
| Option | Contents |
|---|---|
onpress_frames | the registry, JSON keyed by frame id: slug, name, bytes, sha256, inserts, sources, frame_refs, binds, renders_on_request, dates and author of the last write |
onpress_frame_<id> | the markup exactly as uploaded; autoloaded when under 64 KB |
onpress_frame_<id>_baked, onpress_frame_<id>_baked_<lang> | baked copies, see Printing below |
onpress_apps | the apps registry, which also holds statically published pages: the entry page-<id> with the page’s path in custom_url |
The stored markup is always the source, with every <!-- wp:op/… /--> insert and every data-op-bind attribute intact. Substitution happens when the frame is printed, never on the way in, so a frame can always be rebuilt after a menu or a dataset changes, and printing twice gives the same result as printing once.
How frames are linked
Every link is an explicit reference by name. Nothing is implied by a type or a site default.
A page gets exactly one frame. It is the page meta _onpress_frame holding the frame id: an id rather than a slug, so renaming a frame never touches pages. It is written by POST /frames/{ref}/assign or by frame: in the frontmatter of POST /pages/markdown — one and the same meta. The frame is the whole look of the page: the article shell with the header and footer inside it. There are no site-wide defaults: a page without _onpress_frame is printed by the theme’s own template.
A frame pulls in other frames with op/frame. The reference is a slug (readable) or an id. This is the real article frame of site 235, shortened:
<div class="opdoc__layout opdoc__layout--article">
<header class="opdoc__navbar"><!-- wp:op/frame {"ref":"site__header"} /--></header>
<div class="opdoc__content">
<h1 class="opdoc__title" data-op-bind='{"text":"page.title"}'></h1>
<div class="opdoc__body"><!-- wp:op/content /--></div>
<!-- wp:op/frame {"ref":"article__faq"} /-->
</div>
<div class="opdoc__aside"><!-- wp:op/outline /--></div>
<!-- wp:op/frame {"ref":"site__footer"} /-->
</div>
The registry keeps the nested slugs in frame_refs, and a reference written by id is stored there as the slug. That list is how a change to site__header finds every frame that includes it, and how a rename finds whom to rewrite. A different header on one kind of page is a different shell that references a different header frame (landing pointing at site-simple__header), not a per-page override.
A frame can vanish when its list is empty. "empty":"frame" on op/repeat means: if the list is empty, the nearest frame this repeater stands in prints nothing. So article always references article__faq, and a page without questions has no FAQ block at all rather than an empty one with a heading. This is the real article__faq of site 235:
<section class="opfaq" id="faq">
<h2 class="opfaq__title">FAQ</h2>
<div class="opfaq__list">
<!-- wp:op/repeat {"source":"faq","match":{"page":"page.slug"},"empty":"frame"} -->
<details class="opfaq__item">
<summary class="opfaq__q"><span class="opfaq__q-text" data-op-bind='{"text":"item.q"}'></span></summary>
<div class="opfaq__a" data-op-bind='{"html":"item.a"}'></div>
</details>
<!-- /wp:op/repeat -->
</div>
<!-- wp:op/faq-schema {"source":"faq","match":{"page":"page.slug"}} /-->
</section>
The scope is exactly one frame: an empty article__faq leaves an empty place where its op/frame stood, and article itself prints. If the page’s own frame turns out empty, the theme prints its own template. Outside any frame, straight in page content, the repeater only hides itself. Without the attribute an empty list prints a comment, see Inserts.
Addressing a frame
{ref} in a path, and any frame reference in a body or in op/frame, is either the frame’s id (16 hex characters, assigned on creation) or its slug. A page is referenced as everywhere in v2: numeric id, slug or path.
Printing
Expanding inserts and bindings is PHP’s job, not the service’s: inserts are parse_blocks() plus render_block() over registered block types, data-op-bind is rewritten with WP_HTML_Tag_Processor, and analytics counters come from the same code that publishes apps.
The page’s frame is printed on every request. The onpress-v2 theme calls onpress_page_frame_html(), which renders the frame from its source for the requested page: inserts, nested frames, repeaters and bindings, in the page’s language. Nested frames are also printed from their sources, never from someone else’s copy, so contextual lists always belong to the page being shown.
Baking is the same printing done ahead of time, outside a page request. Every endpoint that changes a frame asks PHP to bake it and reports the result in bake. The call is synchronous: you learn whether it happened.
bake.status | Meaning |
|---|---|
ok | PHP baked the frame and republished the static copies of its pages; its report is in bake.response |
unavailable | the bake endpoint is not deployed on that site; the source is saved, bake later |
failed | PHP answered with an error or did not answer in time; bake.reason says which |
skipped | baking was switched off for this call ("bake": false) or in the service config |
A failed bake never cancels the write: the source is already in the database. The bake report is where you see unresolved binding tokens, repeaters that came out empty and nested frames that did not print.
A static page is made from a baked copy. A page is published statically when it has a frame and that frame is not printed on request: PHP writes the bake into the apps registry as page-<id> with the page’s path in custom_url, and nginx serves the file. The key is the page id rather than its path, so moving the page (a new slug, a new parent, another language) rewrites custom_url of the same entry instead of leaving a second copy behind; unpublishing the page, sending it to the trash, taking its frame away or making that frame page-dependent removes the entry. A copy is written only when everything holds; reason says which condition did not: the page is not publish or is password-protected (not_public), it has no frame (no_frame), its frame is printed on request (renders_on_request), that frame has no baked copy yet (no_baked_copy), or the address does not fit a static tree (bad_path) — a Cyrillic or percent-encoded segment, a first segment WordPress reserves, an address another registry entry already holds. Any of these removes the copy rather than leaving a stale one.
The pages touched by a bake come back in bake.response:
"bake": {
"status": "ok",
"response": {
"baked": true,
"bytes": 4120,
"published": {
"published": true,
"pages": [
{ "id": 6412, "url": "/home/", "published": true, "path": "page-6412/index.html", "bytes": 4120, "reason": "" },
{ "id": 6501, "url": "/pomelli-workflow/", "published": false, "path": null, "bytes": 0, "reason": "renders_on_request" }
]
}
}
}
A frame whose output depends on the page gets renders_on_request: true in the registry and no baked copy at all, because one copy would show one page’s text on another — and therefore its pages are never static. The reasons are listed in live_reasons of the upload answer:
- a page-dependent insert:
op/content,op/pager,op/docs-nav,op/outline,op/share-ai,op/featured-image,op/terms,op/assistant,op/faq-schema,op/partial; - a
data-op-bindwith apage.*token; - an
op/repeatover a contextual source (pages.children,pages.siblings,pages.adjacent,breadcrumbs) or with apage.*token inmatch; - an
op/framethat nests such a frame.
Other frames are baked once per site language. Changes to menus, datasets and nested frames made through the API rebake the dependent frames automatically; the mechanics and POST /frames/rebake are in How it works.
GET /frames
Lists the site’s frames without markup, sorted by slug.
| Parameter | Type | Default | Description |
|---|---|---|---|
block | string | — | only frames of this block: block=article gives article, article__faq, article__faq--simple |
q | string | — | case-insensitive substring of the slug or the name |
include_html | bool | false | include each frame’s markup; the list gets heavy |
blog_id / domain | int / string | — | the site, or the X-OnPress-Blog-Id header |
type is ignored and answered with "warnings": ["type_ignored"]. An entry carries pages — the ids of the pages printed in that frame — when there are any: a template has no address, but you can see where it shows up.
curl -s -H "Authorization: Bearer $OP_TOKEN" -H "X-OnPress-Blog-Id: 235" \
"$BASE/frames?block=article"
{
"success": true,
"blog_id": 235,
"home_url": "https://example.onpress.pro",
"total": 2,
"frames": [
{
"id": "c33c7d69d5a35456",
"name": "Статья",
"slug": "article",
"bytes": 1356,
"sha256": "f46576af386c8f79a7f4dd2d7532aa8fcc233a50d4491e6e66fb1fb8a3f826bf",
"inserts": { "op/frame": 3, "op/content": 1, "op/outline": 1 },
"sources": [],
"frame_refs": ["site__header", "article__faq", "site__footer"],
"binds": 2,
"renders_on_request": true,
"created_at_gmt": "2026-09-21 19:26:31",
"updated_at_gmt": "2026-09-21 20:40:56",
"updated_by": 1,
"block": "article",
"element": null,
"modifier": null,
"option": "onpress_frame_c33c7d69d5a35456"
},
{
"id": "4ce5ec819bf84f15",
"name": "Статья: FAQ",
"slug": "article__faq",
"inserts": { "op/repeat": 1, "op/faq-schema": 1 },
"sources": ["faq"],
"frame_refs": [],
"binds": 2,
"renders_on_request": true,
"block": "article",
"element": "faq",
"modifier": null,
"…": "…"
}
],
"registry_option": "onpress_frames",
"page_meta": "_onpress_frame",
"slug_pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*(?:__[a-z0-9]+(?:-[a-z0-9]+)*)?(?:--[a-z0-9]+(?:-[a-z0-9]+)*)?$"
}
inserts counts the inserts found in the markup by name, binds counts data-op-bind attributes, sources lists the datasets the repeaters read, frame_refs the nested frames. Together they tell you in advance what printing will substitute and what a change elsewhere will rebake. renders_on_request is absent when false. There is no url and no link — a template has no address — and no type, types, defaults or is_default any more. An old entry that still carries url loses it on the next write.
POST /frames and PUT /frames/{ref}
POST /frames uploads a new frame. PUT /frames/{ref} takes the same body and replaces the markup of an existing one; it can change slug and name in the same call, and a slug change there works exactly as in PATCH.
| Field | Type | Default | Description |
|---|---|---|---|
html | string | — | the markup, required; markup and content are accepted as aliases |
slug | string | from name | block__element--modifier; without it the slug is transliterated from name and validated the same way; taken by another frame is 409 slug_taken |
name | string | the slug | human name, free text |
dry_run | bool | false | validate and roll back: the full report comes back, nothing is stored |
bake | bool | true | ask the PHP side to bake and publish |
blog_id / domain | int / string | — | the site |
The markup is stored verbatim. The only limit is size, 512 KB; above it the call is 413 frame_too_large: a whole site with images and scripts is an app, a frame is markup. type in the body is ignored with the warning type_ignored, default and is_default with default_ignored.
url is a refusal rather than an ignore — 400 frame_url_not_supported:
{
"success": false,
"error": "frame_url_not_supported",
"message": "a frame has no address of its own: the address belongs to the page, a frame is a template. Create a page at that path (POST /pages/markdown with frontmatter \"frame: <slug>\") — it will be printed inside this frame",
"hint": "адрес задаёт страница, фрейм — шаблон"
}
Silently ignoring it would leave you sure the landing is published while nothing serves it.
The answer carries an inspection report instead of a list of things that were stripped, because nothing is stripped. A dry run on site 235:
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/frames" -d '{
"blog_id": 235,
"dry_run": true,
"slug": "docs__example",
"name": "Docs: example",
"html": "<nav class=\"docs__example\"><a data-op-bind='"'"'{\"href\":\"site.home_url\"}'"'"'>Home</a><!-- wp:op/menu {\"place\":\"header\"} /--></nav><!-- wp:op/frame {\"ref\":\"site__footer\"} /-->"
}'
{
"success": true,
"dry_run": true,
"applied": false,
"blog_id": 235,
"frame": {
"id": "5cede5d5aca83706",
"name": "Docs: example",
"slug": "docs__example",
"bytes": 188,
"block": "docs",
"element": "example",
"modifier": null,
"option": "onpress_frame_5cede5d5aca83706",
"…": "…"
},
"inserts": { "op/menu": 1, "op/frame": 1 },
"inserts_total": 2,
"sources": [],
"frame_refs": ["site__footer"],
"binds": 1,
"renders_on_request": false,
"has_doctype": false,
"has_body": false,
"warnings": []
}
A real write also returns updated (false for a new frame), bake and rebaked — the frames that include this one through op/frame and were baked again because of it. A dry run has neither. When the frame depends on the page, the report adds live_reasons, for example ["data-op-bind uses page.* tokens"].
Warnings are plain sentences and worth reading. The service warns about three things:
"warnings": [
"the frame contains a whole document (<!doctype>/<body>): that is right for a frame that wraps a page, but inserted into another frame with <!-- wp:op/frame /--> it produces a second <body>",
"data-op-bind is written with double quotes: JSON inside an attribute must be kept in single quotes, otherwise the attribute breaks at the first double quote of the value",
"op/frame {\"type\":…} is legacy (footer): link frames by name, <!-- wp:op/frame {\"ref\":\"site__header\"} /-->"
]
A warning never blocks the upload: the frame is saved, and the broken part simply does not work. An op/frame pointing at a slug that does not exist is not reported here; it prints a comment at render time.
GET /frames/{ref}
One frame with its markup in html, plus referenced_by with the slugs of frames that insert it and pages with the ids of the pages printed in it.
curl -s -H "Authorization: Bearer $OP_TOKEN" -H "X-OnPress-Blog-Id: 235" \
"$BASE/frames/site__header"
{
"success": true,
"blog_id": 235,
"frame": {
"id": "164ec859ae2d9cfa",
"name": "Сайт: шапка",
"slug": "site__header",
"bytes": 601,
"inserts": { "op/lang-switcher": 1 },
"sources": [],
"frame_refs": [],
"binds": 3,
"block": "site",
"element": "header",
"modifier": null,
"option": "onpress_frame_164ec859ae2d9cfa",
"referenced_by": ["article"],
"html": "<div class=\"site-header site-header--pomelli\">…</div>\n",
"…": "…"
}
}
PATCH /frames/{ref}
Changes a frame’s fields without touching its markup; the markup is replaced only with PUT.
| Field | Type | Description |
|---|---|---|
slug | string | new slug, same scheme; taken by another frame is 409 slug_taken |
name | string | human name |
url here is 400 frame_url_not_supported as well. A published page is moved by moving the page: republish it with another url, and the static copy follows — the registry entry is keyed by page id, and only its custom_url changes.
Renaming rewrites the references. In every frame of the site whose frame_refs holds the old slug, <!-- wp:op/frame {"ref":"<old>"} /--> gets the new slug (the keys frame, slug, id and the legacy op/partial are rewritten too), the registry entry is updated and the frame is rebaked, all in the same call. Only the string inside the block comment changes; the rest of the markup stays byte for byte. Pages are untouched because they hold the id, and references written by id need no rewrite. Step by step: Rename a frame.
curl -s -X PATCH -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/frames/faq" -d '{"blog_id": 235, "slug": "article__faq", "name": "Статья: FAQ"}'
{
"success": true,
"blog_id": 235,
"frame": { "id": "4b0e7c21d9a3f610", "slug": "article__faq", "name": "Статья: FAQ", "block": "article", "element": "faq", "modifier": null },
"renamed": {
"from": "faq", "to": "article__faq",
"frames_updated": ["article", "landing"],
"details": [{ "id": "91399e3637b7aa2b", "slug": "article", "replaced": 1 }, { "id": "…", "slug": "landing", "replaced": 1 }]
},
"bake": { "status": "ok" },
"rebaked": { "status": "ok", "frames": ["…"] }
}
type in the body is ignored with type_ignored. A body with only is_default or default is 410 gone; a body with nothing to change is 400 nothing_to_update.
DELETE /frames/{ref}
Deletes the frame, its markup and its baked copies, and clears _onpress_frame (and the legacy _onpress_frame_<type>) on every page bound to it: a page left bound to a missing frame would print nothing. Those pages also lose their static copy — with no frame there is nothing to publish, and the theme prints them again. Frames that still insert it are not edited; they are listed in referenced_by with a warning, and until a frame with that slug appears again the insert prints a comment instead of markup.
curl -s -X DELETE -H "Authorization: Bearer $OP_TOKEN" -H "X-OnPress-Blog-Id: 235" \
"$BASE/frames/site__footer"
{
"success": true,
"blog_id": 235,
"deleted": { "id": "9a6e28632df819dc", "slug": "site__footer", "name": "Сайт: подвал" },
"unbound_pages": [102],
"referenced_by": ["article", "landing"],
"warnings": ["frames article, landing still insert \"site__footer\"; the insert prints nothing until a frame with this slug appears again"],
"bake": { "status": "ok" }
}
POST /frames/{ref}/assign
Puts a frame on one page by writing _onpress_frame with the frame id. A page has one frame, so assigning another replaces it.
| Field | Type | Description |
|---|---|---|
page | int / string | the page: id, slug or path; required, otherwise 400 page_required |
clear | bool | remove the page’s frame instead of setting it (also clears the legacy _onpress_frame_page) |
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" }
}
The same binding is set at publish time with frame: article in the frontmatter, see Pages and markdown. frame there takes one frame; a list or the old type=ref form is 400 frame_single.
Binding also decides static publishing: a page that gets a frame printed from a copy is written into the apps tree, and {"clear": true} takes it back out.
A landing: a page and a shell frame
A whole page exported from a builder used to be “a frame with a url“. Now it is two ordinary objects.
- Upload the markup as a frame, without any address:
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/frames" -d "$(jq -n --rawfile html landing.html \
'{blog_id: 235, slug: "landing", name: "Landing", html: $html}')"
- Publish the page that holds the address. Its body may be empty — the frame prints the whole document:
curl -s -X POST -H "Authorization: Bearer $OP_TOKEN" -H 'Content-Type: application/json' \
"$BASE/pages/markdown" -d "$(jq -n '{blog_id: 235, markdown: "---\nname: Home\nslug: home\nurl: \"/home/\"\nlang: en\nstatus: publish\nframe: landing\n---\n"}')"
The site now serves /home/. Whether it is served as a static file or rendered by the theme depends on the frame alone: landing without page-dependent inserts is baked and published as a file; add an op/content, an op/assistant or a page.* binding and the same page starts being printed on request. The call does not change.
Making this page the site’s front page is PUT /site/front-page, the same as for any other page.
A whole-document frame must stay static. <!doctype> and <body> are right for a landing, and the upload only warns that such a frame must not be nested into another one with op/frame. The other half of that rule: a page printed on request is rendered inside the theme’s own document, so a whole-document frame on that path gives two <html> and two <title>. Keep page-dependent inserts and page.* bindings out of such a frame, or print the page in an ordinary shell instead.
Removed: defaults and types
GET /frames/defaults, PUT /frames/defaults and POST /frames/{ref}/default answer 410 gone:
{"success":false,"error":"gone","message":"default frames by type are gone (frames v3): a page is bound to its frame with POST /frames/{ref}/assign or frontmatter \"frame\", and frames link to each other with <!-- wp:op/frame {\"ref\":\"site__header\"} /-->"}
Links between frames are op/frame {"ref":…}, a page’s frame is assign or frame:. type is ignored everywhere: in the query of GET /frames, in the bodies of POST, PUT and PATCH. Old registry entries may still carry type; it is ignored on read.
How the theme reads a frame
The contract is entirely in the site’s options and the page meta, so a theme never talks to the service. The onpress-frames plugin wraps it in functions:
$html = onpress_page_frame_html(get_queried_object_id()); // '' — no frame, or it printed nothing
if ($html === '') {
// print the theme's own template
}
echo $html;
| Function | Returns |
|---|---|
onpress_page_frame_id(int $page_id): string | the page’s frame id: _onpress_frame (id or slug), then the legacy _onpress_frame_page and the old site default; '' when none |
onpress_page_frame_html(int $page_id, string $lang = ''): string | the page’s frame, printed for that page |
onpress_frame_html(string $ref, int $page_id = 0, string $lang = ''): string | any frame by slug or id, printed for that page: inserts, bindings, context and language in one call |
onpress_frame_find(string $ref): ?array | the registry entry with id, slug, name, block, element, modifier |
onpress_frame_renders_on_request(string $ref): bool | whether the frame is page-dependent and has no baked copy |
An empty string is the normal “no frame” answer, not an error. onpress_frame_for_type() is still there for old themes; do not use it in new code.
Compatibility until site 233 is migrated
This documentation site (233) shares its database with production, where the old PHP still reads frame types, so its frames are not renamed yet. Its shell is the frame obolochka-dokumentaciya, bound to the older pages through the old meta _onpress_frame_page (pages published since then get _onpress_frame), and it pulls the header and footer in the old way:
<div class="opdoc__navbar-content"><!-- wp:op/frame {"type":"header"} /--></div>
…
<!-- wp:op/frame {"type":"footer"} /-->
Until the migration the new code keeps a compatibility layer. _onpress_frame_page is read when _onpress_frame is empty, then the old site default onpress_frame_defaults["page"]. op/frame {"type":"header"} (also footer, partial) resolves through the page meta _onpress_frame_<type> and then onpress_frame_defaults. op/partial is an alias of op/frame {"type":"partial"}. Registry entries keep their type while the site still has onpress_frame_defaults. The migration renames the frames and removes the option; after it the layer is gone. Do not build anything new on it.
The old “frame with an address” left its own traces: entries frame-<id> in onpress_apps and a url in registry entries. PHP still reads them, so an address already taken stays taken, but nothing writes them any more; the migration moves such an address onto a page and drops the entry. On this site only the test frames tilda-demo and frames-test had one.
Errors
| Code | HTTP | When |
|---|---|---|
frame_html_required | 400 | the body has no html, or it is not a string |
invalid_slug | 400 | the slug does not follow block__element--modifier, or there is neither slug nor name; pattern and hint are in the answer |
frame_single | 400 | frame in a publish names more than one frame or uses the old type=ref form |
frame_url_not_supported | 400 | url in the body of POST, PUT or PATCH /frames: the address belongs to the page, a frame is a template |
page_required | 400 | assign without page |
nothing_to_update | 400 | PATCH with no editable field |
frame_not_found | 404 | no frame with that id or slug on this site |
page_not_found / ambiguous_page | 404 / 409 | the page reference in assign did not resolve to exactly one page |
slug_taken | 409 | another frame of the site already has that slug; to replace its markup use PUT |
gone | 410 | /frames/defaults, /frames/{ref}/default, a PATCH with only is_default, frame: default:<type> in a publish |
frame_too_large | 413 | the markup is over 512 KB; the answer has bytes and max_bytes |
The common error shape is on Errors.