Vizen Shop

Header, menu, footer, layouts

/docs/skill/vizen-chromecurrentEN

1. Two paths — decide before writing

Platform widgetsOwn markup
lookbounded by props (siteHeader has ~30) — plus your own class on its band, see §4aanything
sticky headersiteHeader.sticky — worksneeds the sticky key, with a caveat below
dropdown menusiteMenu.level2 — works, opens on clickyou build it
effortminuteshours

⚠️ The single most expensive omission we have measured: a chrome group takes any block, including `kind:"html"` (as did kit.top/kit.bottom before it). That is how a fully custom header is built. An agent spent a probe — a write to a live shop — to discover this, because no guide said it.

2. How chrome is assembled

GROUP (a content block, group_type:"chrome")   props.widgets = [block ids]
   ↓ bound by
PUT /design/bindings/{resource_type}/{id}   { "slots": { "header": <group id> } }

Four slots make a page — header · feed · body · footer — and each has its own ladder: this resource → the category chain → the layout (§2a) → the shop → the earliest group of that label. resource_type is shop (id 0), category, category_children, product, article, plus theme and system (§2a). category_children inherits down the whole subtree.

A slot has three answers, not two: absent from the map = inherit, 0 = nothing here (the ladder stops — this is how a header is switched off, there is no flag), an id = that group.

Read the current state first: GET /v1/storefronts/resolve?slug=<shop>chrome, and GET /design/bindings/category/<id>resolved with the source of each slot.

⚠️ Shops built before 2026-08-21 still run on the legacy kit — a kind:"chromeKit" block in the slot chrome, props.kit = {top:[…], bottom:[…]}. It keeps working; write groups for anything new. Full model: GET /docs/chrome §3.1–3.5.

2a. Layouts and system pages

A layout ("Лейаут" in the admin, theme in the API) sits above the chrome: a group-container group_type:"theme" whose "content" is not widgets but bindingsPUT /design/bindings/theme/{id} with header, feed, body, footer, plus a body per page kind (body_product, body_category, body_news, body_article). Which layout a page uses is its own short ladder on the slot theme: this resource → the category chain → the shop → the earliest layout. So one call gives a whole section its own design:

PUT /design/bindings/category/648   {"slots": {"theme": 1837}}   # a section
PUT /design/bindings/shop/0         {"slots": {"theme": 1837}}   # the whole shop

A layout also owns a folder — the site's shared CSS, fonts, scripts and favicon. That is where global styles belong: CSS left inside the header's html widget is seen by the header alone. vizen-own-markup §6.2 has the folder, /themes/{id}/site/* and the rules. Files of the active layout also answer on a second, unversioned address — GET /_site/<path> (/_site/logo.svg, /_site/favicon.ico) — for links that outlive a release: an email to a buyer, a mail signature, an organisation card, someone else's site. It needs the tenant host (without it "the active layout" is undefined → 404) and is cached public, max-age=300 with an ETag rather than forever. Inside the site keep using /_html/{doc}/{rid}/….

An article is a design resource too (2026-09-13):

PUT /design/bindings/article/<id>   {"slots": {"theme": 1837}}   # one article, another design

The ladder is the article → its rubric and the rubric's ancestors → the layout → the shop → the default — the rubric branch is category, so a binding made on the rubric itself does reach its articles. Read it back with the article: GET /articles/by-slug/{slug} answers with design (the admin GET /articles/{id} never resolves design). body_article stays a slot of the layout, not of the article — asking for it on an article answers DESIGN_SLOT_NOT_FOR_RESOURCE.

System pages — cart, checkout, account, orders, wishlist, search, deals, dashboard, coupons, notfound — 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 (the storefront owns their body). Unbound, they inherit the shop's layout and chrome:

PUT /design/bindings/system/1  {"slots": {"header": 1975}}   # 1 = cart
GET /design/system/cart?company_id=<N>                        # what actually applies

⚠️ In the guarded zones (/cart, /checkout, /account, /wishlist, /orders, /deals, /dashboard) an author-owned (level 3) header renders without its scripts and without its relative <link rel="stylesheet"> (measured 2026-09-12), and the layout's code does not run at all. Markup, inline <style> and the layout folder's stylesheet stay — keep the header's CSS there, and never promise the owner a script in the cart.

2b. The chrome is rendered on the server — what that changes for you

Since 2026-09-13 header, menu and footer are printed by the server; only the pieces that are actually clicked ship JavaScript (the search and burger toggles, the level-2 toggle). Two consequences, and neither is a new rule to follow:

  • the menu's level-2 panels arrive in the HTML, closed (data-open="0"), within a node budget — 60 nodes on the storefront. A root whose panel does not fit loads it on the first click from GET /categories?view=menu, one request per tab, shared with the search suggest. So a panel that opens after a short pause on a huge catalogue is normal, not a defect to report;
  • nothing of this touches your own header. The budget and the toggle belong to the platform siteMenu widget. Your markup in a chrome group is printed whole, exactly as before, and the active menu item is now computed from the request path on the server instead of after hydration — highlighting appears immediately.

3. Recipe: a custom header on one section only

  1. publish the markup as a folder: node backend-3D/tools/html-transfer/transfer.mjs ./header --name "Header" (no --page — a chrome widget is not mounted on a page; the command creates the level-3 document, publishes the release and prints the document id);
  2. wrap it in a block, kind: "html";
  3. create two groups (group_type: "chrome", props.widgets: [<block id>]) — one for the header, one for the footer;
  4. PUT /design/bindings/category/<id> with {"slots": {"header": <header group>, "footer": <footer group>}}.

The rest of the shop keeps what it had. Check that: open the home page and confirm its data-vz-kit still shows the original id. Putting the same pair on the layout instead (/design/bindings/theme/<id>) gives them to every page that layout covers — that is the difference between "this section" and "this design".

4. Sticky headers

vz-sticky on the root node of your markup makes the block stick. Two ways in, both landing in the same place:

  • own markup — vz-sticky on your root node;
  • any block — props.position: "sticky", with props.stickyTop in px when something above is already sticky.

Either way the platform applies it to the block wrapper, which is the only node with room to travel: a sticky element placed inside the block cannot move, because its parent is exactly as tall as the block.

⚠️ If you have older notes prescribing position: fixed plus a spacer of the header's height — drop them. That workaround existed because the class used to land inside the wrapper; it was fixed on 2026-08-19. position: fixed is not the header tool: it reserves no height, vz-sticky does. It is fine for a side rail or a modal in an unboxed level-3 block, where it resolves against the viewport.

⚠️ The two ways in are not interchangeable everywhere. props.position and vz-sticky both work for any block in a page zone or a chrome kit — that is the custom-header case. They do not work on the platform siteHeader / siteMenu / siteFooter, whose band the wrapper never draws: the header uses its own props.sticky: true, and the menu and footer have no switch at all. Full table by surface: GET /docs/own-markup §5.7.

⚠️ props.overflow: "clip" breaks stickiness for everything inside the block — a clipping ancestor is not a scroll container the sticky element can stick to. Sticky and clip are mutually exclusive; pick one per block.

4a. Keeping the widget and restyling it anyway

Before you replace the header with own markup, check whether the third path is enough: props.wrapperClass puts your class on the band of siteHeader, siteMenu or siteFooter, and you restyle that from your own CSS. The widget keeps its live data, its search island and its cart counter; only the look changes.

{ "kind": "siteHeader", "props": { "wrapperClass": "shop-hdr", "sticky": true } }
.shop-hdr             { background: #0b0b12; border: 0 }
.shop-hdr > .vz-inner { max-width: none; padding-inline: 40px }

Two conditions, both hard: that CSS must live in a level 3 document (level-1 styles are trapped inside a shadow root and never see the band), and you must scope through your own class — never write bare .vz-inner, those names are internal and have moved before. Full recipe and the anti-path: GET /docs/own-markup §5.

5. Replacing the bundled product card

Build a product group whose props.widgets list is your widgets — your card, breadcrumbs, whatever, in your order — and bind it to the slot body:

PUT /design/bindings/category_children/<id>   {"slots": {"body": <group id>}}
# one product only: PUT /design/bindings/product/<id>

category_children reaches every product of the subtree without touching a single product record. The body is one group and the page is assembled on the server, so there is no two-cards problem any more: a card with an explicit card.productId draws that product anywhere, a card without one takes the page's product.

⚠️ Retired recipe, do not use it. Older notes say to clear refs: [] in each product's __page:top to remove the second card. Under the group model that does not fix anything — it wipes the contents of those pages. If you meet such manifests in an unmigrated shop, they are read for compatibility and nothing else. hideSystemBlock is not the answer either: it hides the whole technical layer, including your own bound card.

6. Verify

  • the home page and untouched sections show the original data-vz-kit;
  • your section shows the new one;
  • the menu opens (by click) and the header holds at several widths;
  • a product page has exactly one card.

Full checklist: vizen-troubleshoot.

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

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