Troubleshooting: the API said yes, the site says no
/docs/troubleshootingcurrentEN· проверено 2026-08-20
Резюме по-русски. Диагностика «ответ 200, а на сайте ничего»: сначала перечитать объект, потом открыть страницу браузером; таблица тихих отказов и чек-лист приёмки. Отдельно — правило «находка = замер + условия замера»: находка «siteMenu.level2 не работает» оказалась неверной, условия замера не записали. 2026-08-19 та же болезнь нашлась в ГЕЙТЕ: зелёный тест сторожил форму кода, а провода между двумя её концами не было — §7 про это.Status: current · Verified: 2026-08-20, live calls + source measurements Owner: agent-api · Serves: GET /docs/troubleshooting Skill: vizen-troubleshoot
1. What you can do here
Tell apart three states that look identical from the client: a write rejected in substance, a value stored but read by no widget, and a change that is really on the page but invisible to your selector. This area also holds the acceptance checklist every storefront change goes through before the owner is told it is done. A 200 means the write was stored — not that the value is used.
2. Decide first — the order of diagnosis
Two checks, in this order, before you change anything:
| Step | What you do | What the result means |
|---|---|---|
| 1 | Re-read the object — GET /content-blocks and look at your value | gone or changed → the write was rejected in substance: wrong nesting level, wrong shape, value outside the enum. Fix the write, not the widget. |
| 2 | Open the page in a browser | present in the object but absent on screen → the widget does not read that prop. Stop tuning the write; go to §3. |
The other order costs more: a lost value looks exactly like an ignored one.
⚠️ Level-1 selectors must pierce Shadow DOM. Level-1 own markup renders inside a shadow root (host div.vz-radius), and a plain document.querySelector does not see it. An empty result is not evidence that your markup is missing. A transferred folder (level 3) is real page DOM and needs none of this. Walk recursively:
const deepQuery = (sel, root = document) => {
const hit = root.querySelector(sel); if (hit) return hit;
for (const el of root.querySelectorAll('*'))
if (el.shadowRoot) { const r = deepQuery(sel, el.shadowRoot); if (r) return r; }
return null;
};3. Silently ignored — the known cases
| Symptom | Cause | Fix |
|---|---|---|
| a filter returns the whole catalogue | the parameter was written without the filter. prefix; ?category_id=168 is not a field of the request, so it is swallowed and total stays at the full count | filter.category_id=168 |
| a value outside the enum draws the default † | unknown enum values are accepted silently, no error | take values from GET /docs/widgets |
| a menu or footer link renders as plain text | href is not a supported key in siteHeader.links / siteFooter.columns; the label is printed inside a <span> and the link is dead | {label, url} or {label, target:{type,id}} |
| a dropdown or a modal is cut off at the block edge | the shadow host div.vz-radius clips when `props.overflow` is absent. The axis does reach that node — the block renders as clip={props.overflow !== 'visible'} — but the band's default is "visible" while the shadow host's is clip, so for an html block "key absent" is not "visible". /docs/widgets now says so in the axis text; older copies of it do not | set props.overflow: "visible" on the block. position: fixed is no longer needed for this (own-markup.md §5.1, §5.6) |
| a custom header does not stick — and it is your own markup | it should stick now: the wire was built 2026-08-19, and both props.position: "sticky" and vz-sticky reach the band on page zones, on layout zones (the chrome-kit case) and on columns children. If it still does not: the block sets props.overflow: "clip", and a clipping ancestor defeats sticky | drop overflow: "clip" on that block, or move the clipping inside your markup. position: fixed plus a hand-measured spacer is no longer needed — stop prescribing it |
a platform header, menu or footer does not stick from props.position | those kinds draw their own band and the block wrapper never runs for them (CHROME_KINDS), so no wrapper style is produced. Nothing is stored wrong; nothing reads it | the header has its own switch: props.sticky: true on the siteHeader section. siteMenu and siteFooter have none — put your own markup in the kit instead (own-markup.md §5.7) |
| a level-1 widget renders without the site's fonts and colours | it used to: a shadow root isolates in both directions. Since 2026-09-13 the layout's stylesheets are put inside the shadow root and the markup is wrapped in .vz-theme-{doc} there, so the theme does reach it. If it still looks bare — the page has no layout at all, or the rule is a compound selector (body.dark), which is never rescoped onto the page root | bind a layout to the shop (chrome.md §3.6), or move that class onto your own root element (own-markup.md §7.2) |
| a level-2 menu panel opens only after a short pause | not a bug. The server prints menu panels into the HTML within a node budget; a root that did not fit loads its panel on the first click from GET /categories?view=menu, one request per tab | nothing to fix. Your own header is unaffected — the budget belongs to the platform siteMenu |
| a block sticks on the live site but not in the admin editor | the editor canvas does not ask the wrapper for stickiness; the storefront does | check on the shop URL, not in the preview. Do not "fix" it |
DELETE fails with invalid request body † | the endpoint requires a body | send {} |
| two product cards on one product page † | a bound layout block plus the resource's own bundled card | chrome.md, card replacement |
productListing props.container.fill accepted, strip unpainted † | documented in wrapper.axes, not implemented: neither --vz-fill nor a class appears in the markup; five forms of the value were tried | none today |
| a type style renders at a different value † | siteHeader.nameStyle.weight: 800 renders 600; siteMenu.labelStyle.size: 13 renders 15 — the widget does not apply what you sent | none today; own markup if the exact value matters |
† carried over from the previous run's report and not re-measured on 2026-08-19 — reported, not proven. Anything you find that is not listed belongs here as a named case, with the configuration you measured on (§4).
Closed since the previous run, do not re-report: own markup and any other non-chrome block not sticking — props.position: "sticky" and vz-sticky both work since 2026-08-19 (§7); what survives is the narrower list in §3, and the answer is never position: fixed any more. Also closed: the resizer answering HEAD with 404. It answers HEAD and GET alike now — measured 2026-08-20, both 200 image/webp on api.vizen.shop and on the shop host. HEAD is a fine way to check that an image URL resolves. Also closed: "the page scope carries no list of products" (products[] is live — own-markup.md §2) and "the overflow axis never reaches the node that clips" (it does — row 1 of this table).
4. A finding is a measurement plus the conditions of the measurement
The rule this area exists to enforce, bought with a real mistake.
The claim. The previous run reported: "siteMenu.level2 is declared; with source: "categories" there are no dropdowns; the client gets a flat list of all 83 categories with no parents." It was filed as a platform defect, and vizen-webcoding-contract.json still lists it in silently_ignored. That entry is wrong and is superseded here.
What re-measurement showed. The menu block (id 2085) was configured source: "custom", level2: false, three hand-written items — a mode with no dropdowns by design. The run tested a switched-off feature and reported on the feature. A reversible experiment then set block 2085 to source: "categories", level2: true: the "Каталог" item rendered as <button> instead of <a> — an item becomes a button exactly when level2 && children.length > 0, so the children did arrive — and a click opened the panel with 82 sub-section links, tree complete, parents in place. The block was returned to custom / level2: false / three items and the rollback was verified.
The grain of truth is in the documentation, not the code. The widget schema says sub-sections drop down on hover; the panel opens on click, and a real mouse movement does not open it. The run checked hover, saw nothing, and condemned the mechanism. An error in a description costs what an error in code costs — this one sent a neighbouring chat into reworking a whole direction.
The rule. "X does not work" and "X does not open on hover with source: categories, level2: true" are different statements, and the first sends a whole direction into rework. Record the configuration you measured on: block id, the props as read back, the URL, the viewport width, hover or click. Of seven claims put through this review six held and one fell — and the one that fell was the only one derived from reading code instead of measuring.
5. Acceptance checklist
Run all of it before telling the owner you are done.
- [ ] the pages you did not touch are unchanged — the home page and two sections;
curlshows the samedata-vz-kitand none of your markup; - [ ] the page opens by direct URL and by a link clicked inside the site — these are different code paths and scripts behave differently on each;
- [ ] after an in-site click: dropdowns open, modals open, timers run (platform handlers must re-initialise, and that is where they usually do not);
- [ ] the header holds at 375 / 520 / 700 / 900 / 1024 / 1440 / 1600, not only at your width; top of a sticky header = 0 while the page is scrolled;
- [ ] no
position: fixedheader and no hand-measured spacer anywhere in what you shipped — sticky works throughprops.position/vz-stickysince 2026-08-19, and a spacer is now a defect, not a workaround; - [ ] no horizontal scrolling on the page body at any of those widths;
- [ ] the cart really accepts an item — press, watch the counter grow, check the button takes
vz-state="in-cart"; - [ ] your text is present in the raw HTML:
curl <page> | grep '<phrase>'. If it is not there, search engines never see it; - [ ] images are served through the resizer (
/w/<width>/webp/…), not as originals fromstorage.yandexcloud.net; - [ ] the owner is told plainly what is a workaround —
position: fixed, hand-counted spacers, prices frozen into own markup — and what is not; - [ ] at least one screenshot from a real browser.
6. Measure, do not guess
Install a browser in the scratchpad and drive it. Status codes cannot tell you whether a dropdown opens or a header sticks: three edits out of eight in the previous run looked successful by status code and changed nothing on the site.
In that run 18% of steps went on browser checks, and they were the cheapest 18% of the build — every finding that survived review came from a measurement, and the single one that did not came from reading code instead. The same run spent 21% discovering what was possible and 12% redoing work after a change of course: 33% pure loss. Diagnosis is not the expensive part. Guessing is.
7. How this was verified
Live shop kiberpank.vizen.shop, 2026-08-19:
- storefront
/neon-gridpulled and taken apart (683 KB of markup); menu, categories, products and chrome kit 2087 queried over the API; - resizer measured on one URL:
HEAD→ 404,GET→ 200image/webp(superseded 2026-08-20 — see below); - wrapper chain measured in a real browser through shadow roots —
div.vz-boxoverflow: visible→div.vz-inneroverflow: visible→div.vz-radiusoverflow: clip+ shadow root (the chain still holds; what changed is that the last node is now switchable — see below); siteMenu.level2re-measured by a reversible write to menu block 2085, 82 sub-section links counted in the open panel, block rolled back and the rollback checked;- the
filter.prefix is confirmed againstapi/catalog/catalog.proto:2768-2783:GetProductsRequestcarries a nestedFiltermessage withcategory_id, so the query parameter isfilter.category_idand a barecategory_idis not a field of the request; - rows marked † in §3 come from the previous run and were not re-measured.
Re-measurement 2026-08-20 — three rows of §3 were stale, and one of the three was stale in the opposite direction from what the checklist assumed:
- the resizer and `HEAD`.
curl -Iandcurlon the same live URL (/w/640/webp/vizen-prod-files/sunset/view/product/e3/83/….png) answer 200 `image/webp` both ways, onapi.vizen.shopand on the shop host alike. The cause of the old 404 is in the source and is fixed:imgserve.gonow registersmux.Head("/w/{width}/*", resize)next to theGet, because chi does not deriveHEADfromGET. Gate:storefront/test/image-slicing.test.mjs· "HEAD у нарезчика зарегистрирован рядом с GET". The row is deleted, not edited: there is no silent failure left to describe; - the `overflow` axis.
SectionHtmlInline.tsxandSectionHtml.tsxboth render the shadow host asclip={section.props?.overflow !== 'visible'}, so the axis reaches the node that actually clips. What remains true — and is now the row's content — is that the effective default for an html block isclipwhile/docs/widgetsadvertises"visible". Gate:storefront/test/wrapper-sticky-clip.test.mjs; - the custom header. Was still not sticking at that hour, and not for the documented reason: a repo-wide search found
htmlStickywritten instorefront/src/islands/ZoneSections.tsxand read nowhere, and<SectionBoxin eight places, none of which passedsticky. The branch existed; the wire did not. ⚠️wrapper-sticky-clip.test.mjswas green through all of it, because it asserts the *shape* of the source, not that anything connects the two ends. A shape gate cannot see a disconnected wire — the §4 lesson, applied to gates instead of to findings.
The wire was built the same day, and the row in §3 was rewritten from the result. What changed, measured on rendered markup rather than on source shape:
sectionSticky()is now asked by the section list (packages/sections/src/PageSections.tsx) and by the nested blocks of thecolumnswidget, soprops.position: "sticky"andvz-stickyboth reach the band. The chrome-kit path goes through the same list, which is why a custom header inkit.topsticks;- the order of branches inside
wrapperFrameStyle()changed with it: the background layer used to setposition: relativelast and overwrotesticky, so a band with a background never stuck even where the wire existed. That one was invisible to every gate reading source text; - three surfaces still do not ask, and §3 names each: the three chrome kinds (the wrapper does not run for them at all),
vz-stickyinside acolumnschild, and the editor canvas; - the new gate reads the
styleattribute of markup rendered throughreact-dom/server(storefront/test/wrapper-render.test.mjs), which is the only kind of gate that could have caught either defect.
8. Negations in this document (rule §3.9)
Every row of §3 is a negation — that is what the area is made of — so this table is the area's own regression list. A row without a gate is re-measured after each wave; a row with a gate falls the day the gate goes red.
| The claim | Kept true by |
|---|---|
a bare category_id is not a field of the request (§3) | source-pinned: api/catalog/catalog.proto GetProductsRequest.Filter. No gate — a rename of the nested message would leave this text standing |
| unknown enum values are accepted silently † (§3) | after-wave pass — carried over, never measured here |
href is not a key of siteHeader.links (§3) | after-wave pass — no gate |
the shadow host clips when props.overflow is unset (§3) | gate storefront/test/wrapper-sticky-clip.test.mjs — it pins the expression that *is* the default |
vz-sticky and props.position reach the band on page zones, layout zones and columns children (§3) | gate storefront/test/wrapper-render.test.mjs — it reads the style attribute of rendered markup. This is the positive claim; it replaced a negation, and the gate replaced the shape gate that could not see the wire |
the three chrome kinds, columns + vz-sticky, and the editor canvas still do not ask (§3) | after-wave pass, by search, not by gate — CHROME_KINDS early-returns in SectionBox, the columns call site passes no htmlSticky, and the canvas call sites pass no sticky at all. Nothing here goes red when one of them is wired up, so re-measure before repeating the claim |
DELETE needs a body † (§3) | after-wave pass — carried over, never measured here |
props.container.fill is documented and not implemented † (§3) | after-wave pass — carried over; owned by widgets.md |
| type styles render at a different value † (§3) | after-wave pass — carried over; owned by widgets.md |
the resizer answers HEAD (§3, closed row) | gate storefront/test/image-slicing.test.mjs · "HEAD у нарезчика зарегистрирован рядом с GET" — the only reason this correction cannot rot back |
document.querySelector does not cross a shadow root (§2) | platform behaviour of the DOM, not ours — nothing to gate |
⚠️ The lesson this table exists to carry. Two of the entries above are green gates that would not have caught the defect they guard: a gate that reads the source for a *shape* proves the shape, and a broken wire has the right shape at both ends. When a negation matters, re-measure it the way §4 demands — on the running thing, with the conditions written down — and treat the gate as a reminder, not as the proof.