Site chrome: header, menu, footer and the page skeleton
/docs/chromecurrentEN· проверено 2026-08-21
Резюме по-русски. Хром и скелет страницы. С 2026-08-21 состав оформления хранится ОДНИМ способом — группой ссылок на виджеты, — а страница собирается из четырёх слотов: шапка · лента · тело · футер. Шапка и футер разделены: заменить одну, не трогая другую, теперь можно. У слота три ответа, а не два: «наследовать», «нет» и «эта группа» (§3.3) — именно так гасится шапка, и флагов-выключателей для этого больше не заводится. С 2026-09-12 над хромом появился ЛЕЙАУТ (§3.6): группа-контейнер со своими слотами, своей папкой файлов и кода и оформлением системных страниц (корзина, кабинет…). С 2026-09-13 у папки лейаута есть НЕверсионированный адрес /_site/<путь>, статья стала ресурсом оформления, а хром (включая панели меню уровня 2) печатает сервер.Status: current · Verified: 2026-08-21, live calls on the stand + browser; §3.6 — 2026-09-12, по коду (домен, резолвер, хендлеры); §3.6 хвост (/_site/, лейаут у статьи, серверный хром и меню уровня 2) — 2026-09-13, по коду обоих репо Owner: agent-api · Serves: GET /docs/chrome Related guide: /docs/webcoding §16.2 (own header, one page) Skill: vizen-chrome
Магазины старой модели. КомплектchromeKitи пара слотовchrome/layoutпродолжают работать — их читает та же ручка, и магазин, где они настроены, ничего не теряет. Новый код пишет группы; старое описание оставлено в §3.5 как справка по тому, что вы можете встретить в чужих данных.
1. What you can do here
Build the parts that repeat on every page — header, menu, footer — and decide which pages get which. The unit is a group: an ordinary content block holding an ordered list of references to other blocks. A group becomes visible by being bound to a resource: the whole shop, a category, everything inside a category, one product. Binding one section leaves the rest of the shop as it was, and that is what makes a second design possible inside a live shop.
2. Decide first (forks)
| If you want… | Take this path | Cost |
|---|---|---|
| a tidy, branded header in minutes | platform widgets siteHeader/siteMenu/siteFooter (widgets.md) in a chrome group | the look is bounded by props (~30 on siteHeader); sticky works, siteMenu.level2 gives real dropdowns — they open on click, not on hover |
| a header that is unmistakably yours | own markup (own-markup.md) as a block inside the same group | full freedom; sticky needs a workaround (§4.2), and a dropdown cannot overhang the block |
| the new chrome on the whole shop | bind to shop, id 0 | every page changes at once |
| the new chrome on one section only | bind to category / category_children | the rest of the shop keeps the old one — verify that, do not assume it |
| a page with no header at all | bind slot header to 0 on that resource (§3.3) | stops the ladder for that slot only; the footer stays |
A group takes any block, including `kind:"html"`. That is how a fully custom header is built. The fact was originally a probe result on a live shop — no guide predicted it — and it survives the new model unchanged: the list is references, and a reference does not care what the block draws.
3. Objects and where they live
3.1. Widget, group, binding — the whole model
WIDGET — what to draw (one block = one widget)
GROUP — a named list of widgets (block + label + ordered references)
BINDING — which group, where (shop / category / product, per slot)The group is a content block with one section, kind: "group", whose composition lives in props.widgets — an ordered list of block ids — plus a label in group_type. The label belongs to the *set*, not to its contents, so reordering the widgets inside never changes what the group is:
POST /content-blocks
{ "item": { "name": "Neon header", "group_type": "chrome", "sections": [
{ "type": "text", "payload": { "v": 2, "kind": "group",
"props": { "widgets": [3146, 3147] } } } ] } }group_type | Fits the slot | Holds |
|---|---|---|
chrome | any slot but the feed | widgets of the frame |
product · category · news · article | any slot but the feed | the template that draws the resource |
design | any slot but the feed | anything — a promo strip, a cover |
layout | feed only | other groups plus the body marker (§3.4) |
The label names a set; it does not gate where the set may stand. The one exception is the feed, and that is structural, not editorial: its list holds groups and the body marker, so a widget list put there would be misread. A cover group as somebody's header is legitimate.
The label set is open: a new kind is one row in the core registry (domain.DesignGroupTypes), no migration and no changes inside sections.
3.2. The page skeleton — four slots
header · feed · body · footerThe order is structural: it is not in the data and you cannot move the slots. Each slot inherits on its own ladder, so overriding the feed of one page does not drag its body along:
this resource → the category chain up the parents → the LAYOUT → the shop → the earliest group of that typeThe last rung replaces the old "default template" as a special entity. It takes the earliest group of the type, not the latest: creating a new variant must not silently repaint every page of the shop that never made an explicit choice.
The LAYOUT rung is new (2026-09-12) and optional: a shop without layouts, or with a layout that binds nothing, resolves exactly as before — see §3.6.
PUT /design/bindings/{resource_type}/{id}
{ "slots": { "header": 1712, "footer": 1713, "body": 1716 } }resource_type | Applies to |
|---|---|
shop (id must be 0) | shop-wide default |
category | that category page — or a type:"page" page |
category_children | products inside — inherited down the whole subtree |
product | one product |
theme (id = the layout group) | the layout's own slots — what its pages get when nothing closer is bound (§3.6) |
system (id from the registry in §3.6) | a platform page: cart, checkout, account… — theme, header, footer only |
`slots` is a replace-set. {"slots":{"header":X}} on a resource that also had a body drops the body; {"slots":{}} unbinds everything and the resource goes back to inheriting.
3.3. Three answers per slot, not two
| You write | Stored as | The page gets |
|---|---|---|
slot absent from slots | no row | inherited from above |
"header": 0 | row with an empty reference | nothing — the ladder stops here |
"header": 1712 | row with the reference | that group |
0 is the supported way to say "this page has no header". There is no hideSiteHeader-style flag to pair it with, and adding one back would be a second mechanism for a thing that already has one.
3.4. The feed and the body marker
A layout group is the page feed: its list holds other groups and one special entry, -1, marking where the resource body lands. Ids are positive, so the marker cannot be confused with one.
"props": { "widgets": [2100, -1, 2101] } // cover · body · bannerDrop the marker and the body is gone from that page — the author keeps a clean canvas of their own groups. No feed bound at all means the page is just its body. A feed inside a feed is refused: one rule instead of cycle detection and a depth limit.
3.5. What you may still meet: the legacy kit
Shops set up before the group model carry a kind:"chromeKit" block with props.kit.{top,bottom} bound through the slot chrome, and a body template bound through layout. Both are still read and rendered. GET /v1/storefronts/ resolve?slug=<shop> answers with the materialized kit — every part's sections inlined and tagged payload.zone: "top"|"bottom" — which is a read shape, not something you can write back.
Reading the current state is three questions. What the shop uses — GET /v1/storefronts/resolve?slug=<shop>. What *this* resource uses and from where — GET /design/bindings/{type}/{id} → own[] plus resolved{slot:{block_id, source}}, source being own / category:<id> / shop / default; a resolved block_id: 0 with a non-default source is the explicit "nothing" of §3.3. What is on the page — curl <page> | grep -o 'data-vz-kit="[^"]*"'.
3.6. Layouts (theme) and system pages
A layout ("Лейаут" in the admin, theme in the API) is a group-container above the header and footer: group_type:"theme", an empty widgets list, and its slots are ordinary bindings with resource_type:"theme" and the group's id as resource_id. Nothing new to learn: PUT /design/bindings/theme/{id} with header, feed, body, footer — plus a body per page kind, body_product · body_category · body_news · body_article, which beats the layout's plain body for that kind. The layout itself does not inherit: its panel shows only what it binds.
Which layout a page uses is its own short ladder on the slot theme:
this resource → the category chain → the shop → the earliest layout of the shopSo PUT /design/bindings/category/648 {"slots":{"theme": 1837}} switches a whole section to another design; bind theme on shop (id 0) to pick the shop-wide one. GET /design/bindings/... answers with resolved.theme and marks slots that came from the layout with source:"theme". Compatibility is by construction: a shop with no layout, or a layout that binds nothing, resolves through the same rungs it always did (theme_id: 0, theme_source: "default" in design.page). Deleting the earliest layout hands its role to the next one by id — the site may change; assign another layout first.
System pages — the pages the storefront draws itself — are design resources too: resource_type:"system", resource_id from the registry cart=1 · checkout=2 · account=3 · orders=4 · wishlist=5 · search=6 · deals=7 · dashboard=8 · coupons=9 · notfound=10, slots theme, header, footer only (no body, no feed — the storefront owns the body). Unbound, they inherit the shop's layout and chrome. The storefront reads them through a public call:
GET /design/system/{code}?company_id=N → { "design": { "page": { header, footer, theme_id, … } } | null }null means the shop predates the group model — the storefront then draws its legacy chrome. On checkout, cart, account and the other guarded zones an author-owned (level 3) header renders without its scripts.
A layout also owns a folder — the site's shared CSS, fonts, scripts and favicon, the things that must not be buried inside the header's html widget (where only the header can see them). It is the release mechanism of own-markup.md §7.1 addressed by the layout instead of a document id, and index.html there is the skeleton of every page: <head> nodes, the <body> class onto the page root, the body content before </body>; CSS is scoped to .vz-theme-{doc}, the class of the page root, so writing body {} and :root {} is how you style the site. Seven endpoints, with catalog:write + storage:write and owner/admin: GET /themes/{id}/site · POST /themes/{id}/site/releases · …/releases/zip · …/releases/{rid}/publish · …/releases/{rid}/validate · GET …/releases[/{rid}] · POST /themes/{id}/site/rollback. Fields, limits, favicon generation, the guarded zones and vz:navigate: own-markup.md §7.2; the Russian walkthrough is /docs/webcoding §2б.
Stable addresses for the site's files. /_html/{doc}/{rid}/… carries the release number on purpose: a release is immutable, so that address can be cached forever. The same thing makes it unusable wherever a link outlives the release — an email to a buyer, a mail signature, an organisation card, somebody else's site. A layout's folder therefore has a second, unversioned address:
GET /_site/<path inside the layout folder> # /_site/logo.svg, /_site/favicon.icoIt resolves the way the site's header does — the shop's theme slot → the earliest layout → its folder → the active release — so it always points at what is on the site *now*. Three differences from /_html/, and all three follow from the missing release number: the cache is public, max-age=300 plus an ETag instead of immutable; the tenant host is mandatory (addressing starts at the host, and without one "the active layout" is undefined — a bare call to the API is a 404); HTML pages of the release are not served, exactly as in /_html/. The CSS scope is the same (.vz-theme-{doc}) — it is one file of one document, and a second behaviour on a second address would be a trap. Use /_html/ for links inside the site and /_site/ for links that leave it.
An article gets a layout of its own. article is a design resource like a product or a category, so one article can be put on another design without touching its rubric:
PUT /design/bindings/article/<id> { "slots": { "theme": 1837 } }The ladder is the article → its rubric and the rubric's ancestors → the layout → the shop → the default. The rubric branch is category, not category_children: an article lies inside a rubric the way a subsection lies inside a section, so a binding made on the rubric *itself* does reach its articles. Read the result back together with the article — GET /articles/by-slug/{slug} answers with design; the admin GET /articles/{id} does not resolve design and never carries it. Until an article has bindings of its own the answer is what it always was, so nothing moves under existing sites.
body_article is not a slot of the article. Bodies per page kind stay slots of the layout (§3.6 above), because which body a kind gets is decided above the article rather than by it — PUT /design/bindings/article/{id} with body_article answers DESIGN_SLOT_NOT_FOR_RESOURCE.
Level-2 menu panels come in the HTML. The platform chrome is rendered on the server, and the dropdown panels of siteMenu are printed into the page already — closed, as data-open="0" — rather than built by JavaScript after hydration. They are printed within a node budget (the storefront spends 60 nodes on them): roots that fit arrive with the page, and a root that does not fit loads its panel on the first click from GET /categories?view=menu — one request per tab, shared with the search suggest. What this means for you: the menu is in the curl output and therefore indexable; a panel that opens after a short request on a huge catalogue is normal, not a bug; and your own header is untouched — the budget and the toggle belong to the platform siteMenu widget, a custom header built from your own markup is printed whole, as before.
4. Recipes
4.1. Custom header and footer on one section only
- write the markup and upload it (file upload →
POST /html-documents, level 1) — seeown-markup.md; - wrap each document in a block,
kind: "html"; - create two groups (§3.1),
group_type: "chrome"— one with the header block, one with the footer block; PUT /design/bindings/category/<id>with{"slots":{"header":<header group>,"footer":<footer group>}}.
Header and footer are separate on purpose: bind only header and the shop's footer keeps working underneath.
Verify on two pages, not one. GET /design/bindings/category/<id> must show resolved.header.source: "own"; then curl your page and the home page and compare — your markup on yours, the original everywhere else.
4.2. Making a custom header stick — a workaround, and you must say so
vz-sticky does not work here (§5). What holds is a spacer plus a fixed header:
.hdw { --hdh: 126px } /* the header's real height, per breakpoint */
.hdw__spacer { height: var(--hdh) }
.hd { position: fixed; left: 0; right: 0; top: 0; z-index: 2147482000 }The height is measured, not guessed — on the measured run 126/165/153/91 px across five breakpoints. It is a crutch: position: fixed reserves no height, so the spacer rots the moment the header's contents change. Say so to the owner.
Verify: at scroll 2000 the header's top reads 0 at every width tested (375/520/700/900/1024/1440/1600) and the spacer matches the header's height to the pixel. A mismatch shows as a jump or a gap — never as an error.
4.3. Drawing the product page with your own card
Build a product group whose list is your widgets (card, breadcrumbs, whatever you want in whatever order) and bind it to slot body:
PUT /design/bindings/category_children/<id> { "slots": { "body": <group id> } }category_children reaches every product in the subtree without touching a single product record — that is the point of the slot. One product only: bind to product/<id> instead.
The two-cards problem is gone. Before the group model the page merged a template with the product's own zone blocks, and the first own card *replaced* the template one — so a hand-built selection on a product page could not be assembled at all. Now the body is one group and the page is assembled server-side: a card with an explicit card.productId draws that product anywhere, including on another product's page, while a card without one takes the page's product.
Nothing needs rewriting per product any more, so the old recipe — clearing refs: [] in every product's __page:top, 107 products against a 240 writes/min ceiling — is retired. If you still meet those manifests in an unmigrated shop, they are read for compatibility and nothing else.
5. Silently ignored
| What | What actually happens |
|---|---|
vz-sticky on a block inside a kit | the key honestly applies class="sticky top-0 z-30" — to a div inside the block wrapper, whose parent is exactly as tall as the header. Nowhere to travel: measured, at scroll 2200 the header's top read −2200 |
hideSystemBlock with your card bound via the legacy slots.layout | the whole technical layer goes, your bound block included — the page renders empty. On the group model this is not how you hide a body: drop the marker from the feed (§3.4) or bind body to 0 (§3.3) |
siteMenu.level2 with source: "categories" | no dropdowns at all: a flat strip of root items, and a flat list of every category without parents reaches the client. A tree appears only with source: "custom" and hand-built customItems[].children |
href inside siteHeader.links / siteFooter.columns | the label renders, as a <span> — a dead link. The element must be {label, url} or {label, target:{type,id}} |
siteHeader.nameStyle.weight: "800", siteMenu.labelStyle.size: 13 | clamped: 600 and 15 render |
binding slots.layout to the home page category | accepted, stored, 200 — and the render does not change: the home body is drawn outside the block system (measured on category 106) |
GET /content-blocks/{id}/usage on a group that nothing binds explicitly | total: 0 for a group that draws every page of its kind. usage counts bindings, and the last rung of the ladder (§3.2) is not one. A group with total: 0 may still be the one drawing your shop — check resolved on a real resource before deleting anything |
6. Limits
| Thing | Value |
|---|---|
| slots per write | 12 pairs max, replace-set semantics |
resource_type | shop (id must be 0) · category · category_children · product · article · theme (id = layout group) · system (id from the registry, §3.6) |
| slot names | header · feed · body · footer · theme — on shop, category, category_children, product and article alike; on a theme resource also body_product · body_category · body_news · body_article; on system only theme · header · footer (DESIGN_SLOT_NOT_FOR_RESOURCE otherwise); legacy chrome · layout |
| what fits a slot | feed — a group labelled layout and nothing else, because its list holds groups and the body marker rather than widgets; header, body, footer — any group except a feed or a layout; theme — a layout group and nothing else. Legacy: chrome needs a kind:"chromeKit" block, layout a block whose name does not start with __ |
| empty slot | 0 on the four new slots, theme and body_*; the legacy pair refuses it (DESIGN_SLOT_EMPTY_UNSUPPORTED) because the old resolver has no third state |
| rejection tokens | DESIGN_RESOURCE_TYPE_UNKNOWN · DESIGN_SHOP_ID_NOT_ZERO · DESIGN_SLOT_UNKNOWN · DESIGN_SLOT_NOT_FOR_RESOURCE · DESIGN_THEME_UNKNOWN · DESIGN_SYSTEM_PAGE_UNKNOWN · DESIGN_SLOT_NEEDS_GROUP · DESIGN_SLOT_WRONG_GROUP_TYPE · DESIGN_SLOT_EMPTY_UNSUPPORTED · DESIGN_GROUP_TYPE_UNKNOWN · DESIGN_CHROME_NOT_KIT · DESIGN_LAYOUT_SERVICE_BLOCK · BLOCK_NOT_FOUND (foreign resources are masked, not reported) |
| group nesting | one level: feed → groups → widgets. A feed inside a feed is refused |
| deleting a group | refused while a binding points at it (DESIGN_GROUP_IN_USE) or while it is the last of its label (DESIGN_GROUP_LAST_OF_TYPE) — the shop always keeps something to draw a page with. Assign another group first, then delete |
| write rate | 240/min — the ceiling when rewriting product zones in bulk |
after a successful PUT | a design.changed webhook fires from the prod contour; dev edits fire nothing until published |
7. How this was verified
- 2026-09-13 (the §3.6 tail), by source in both repositories plus the stand run recorded in the layouts tech plan:
/_site/answers200withpublic, max-age=300and an ETag for a file of the active layout and404for a foreign host, a missing file, a bare API call without the tenant host and a path that climbs out of the folder; an article with no bindings of its own resolves to the shop's layout, a binding on its rubric moves it, and a binding on the article itself beats the rubric; the chrome's markup is frozen by a snapshot test, so the move to the server changed bytes only inside the islands'data-attributes. - 2026-08-21 (group model), live calls against the stand plus page reads in a browser: a shop born on groups renders header, breadcrumbs, heading, listing and footer; binding
headerandfooterto0on one product removes both from that page and leaves every other page alone; restoring brings them back. A shop still onchromeKitwas re-read in the same run and its chrome was untouched. The two-cards fix was measured on a product carrying both its own card and a card pinned to another product — both render, in order. - 2026-08-19 (everything below), live calls against
api.vizen.shop(shopkiberpank, company 11, PAT on theprodcontour,writes_to_live: true) plus measurements in real Chrome onkiberpank.vizen.shop. - The
kind:"html"in a kit fact is a probe result: an html block was written intokit.topof a live kit and the page re-read — it rendered, withdata-vz-kitstamped on it. No guide predicted it. - Sticky:
getBoundingClientRecton the header after scrolling; the fixed workaround re-measured at seven widths, spacer against header height. Two cards, the empty page afterhideSystemBlockand the 107-product blast radius come from the same run, which kept a rollback snapshot of every zone touched. - Error tokens, resolution order and the replace-set semantics of
slotswere read from the handler source (internal/api/catalog/set_design_bindings.go,internal/core/services/design/resolve.go), not from live calls. The home-page finding (category 106) and theusagefinding (block 2089) come from neighbouring runs on the same shop the same week.
Исходник: https://api.vizen.shop/docs/chrome