Vizen Shop

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 pathCost
a tidy, branded header in minutesplatform widgets siteHeader/siteMenu/siteFooter (widgets.md) in a chrome groupthe 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 yoursown markup (own-markup.md) as a block inside the same groupfull freedom; sticky needs a workaround (§4.2), and a dropdown cannot overhang the block
the new chrome on the whole shopbind to shop, id 0every page changes at once
the new chrome on one section onlybind to category / category_childrenthe rest of the shop keeps the old one — verify that, do not assume it
a page with no header at allbind 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_typeFits the slotHolds
chromeany slot but the feedwidgets of the frame
product · category · news · articleany slot but the feedthe template that draws the resource
designany slot but the feedanything — a promo strip, a cover
layoutfeed onlyother 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 · footer

The 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 type

The 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_typeApplies to
shop (id must be 0)shop-wide default
categorythat category page — or a type:"page" page
category_childrenproducts inside — inherited down the whole subtree
productone 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 writeStored asThe page gets
slot absent from slotsno rowinherited from above
"header": 0row with an empty referencenothing — the ladder stops here
"header": 1712row with the referencethat 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 · banner

Drop 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 shop

So 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.ico

It 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

  1. write the markup and upload it (file upload → POST /html-documents, level 1) — see own-markup.md;
  2. wrap each document in a block, kind: "html";
  3. create two groups (§3.1), group_type: "chrome" — one with the header block, one with the footer block;
  4. 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

WhatWhat actually happens
vz-sticky on a block inside a kitthe 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.layoutthe 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.columnsthe 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: 13clamped: 600 and 15 render
binding slots.layout to the home page categoryaccepted, 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 explicitlytotal: 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

ThingValue
slots per write12 pairs max, replace-set semantics
resource_typeshop (id must be 0) · category · category_children · product · article · theme (id = layout group) · system (id from the registry, §3.6)
slot namesheader · 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 slotfeed — a group labelled layout and nothing else, because its list holds groups and the body marker rather than widgets; header, body, footerany 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 slot0 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 tokensDESIGN_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 nestingone level: feed → groups → widgets. A feed inside a feed is refused
deleting a grouprefused 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 rate240/min — the ceiling when rewriting product zones in bulk
after a successful PUTa 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/ answers 200 with public, max-age=300 and an ETag for a file of the active layout and 404 for 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 header and footer to 0 on one product removes both from that page and leaves every other page alone; restoring brings them back. A shop still on chromeKit was 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 (shop kiberpank, company 11, PAT on the prod contour, writes_to_live: true) plus measurements in real Chrome on kiberpank.vizen.shop.
  • The kind:"html" in a kit fact is a probe result: an html block was written into kit.top of a live kit and the page re-read — it rendered, with data-vz-kit stamped on it. No guide predicted it.
  • Sticky: getBoundingClientRect on the header after scrolling; the fixed workaround re-measured at seven widths, spacer against header height. Two cards, the empty page after hideSystemBlock and 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 slots were 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 the usage finding (block 2089) come from neighbouring runs on the same shop the same week.

Исходник: https://api.vizen.shop/docs/chrome

Работает в демо-режиме