---
name: vizen-troubleshoot
description: Diagnose Vizen storefront problems where the API answered 200 but nothing changed on the site — silently ignored props, filters without the filter. prefix, clipped dropdowns, a header that will not stick, two product cards on one page. Also holds the acceptance checklist for any storefront change.
---

# When the API said yes and the site says no

Our platform accepts things it then ignores. This skill is the list of known
cases and the way to check anything at all.

## 0. Ask before you write — it costs nothing

```
POST /docs/validate
{"sections": [ … the same list you would PUT to /content-blocks/{id} … ]}
```

Answers per path: `applied`, `ignored` (accepted and does nothing) or `unknown`
(value not understood — a default will be drawn), with the allowed values when
the miss is an enum. Nothing is written, no rights are needed.

It does **not** check that ids exist or that your token may write — those are
shop data, and this endpoint deliberately never touches them.

## 1. Already wrote it? Then: is it really ignored?

A 200 means the write was stored, not that the value is used. Two checks, in
order:

1. **Re-read the object.** `GET /content-blocks` and look at your value. Gone or
   changed → the write was rejected in substance.
2. **Open the page in a browser.** Present in the object but absent on screen →
   the widget does not read that prop.

⚠️ **Level-1 selectors must pierce Shadow DOM.** Level-1 own markup renders
inside a shadow root; a plain `querySelector` does not see it — walk
`element.shadowRoot` recursively. A transferred folder (level 3) is real page
DOM: plain `querySelector` works.

## 2. Known silent failures

| Symptom | Cause | Fix |
|---|---|---|
| a filter returns the whole catalogue | parameter written without the `filter.` prefix — `?category_id=168` is swallowed, `total` stays at the full count | `filter.category_id` |
| a value outside the enum draws the default | unknown enum values are accepted silently | take values from `GET /docs/widgets` |
| a link renders as text | `href` in `siteHeader.links` is not supported | `{label, url}` or `{label, target:{type,id}}` |
| a dropdown or modal is cut at the block edge | the shadow host has `overflow: clip` | see `vizen-own-markup`, wrapper section |
| the site looks unlike the local folder — small headings, no bullets, no underlines, bare buttons | the storefront's Tailwind preflight resets browser defaults, and a level-3 widget shares the page cascade | ship a base.css under your root class; `/docs/transfer` §7 |
| a `position: fixed` element is positioned against the block, not the window | the block is boxed (`props.html.boxed: true`) or level 1 — layout containment stays | an unboxed level-3 block: the page `<main>` drops containment |
| a stylesheet from the header shows on the live site but not in the editor canvas | the canvas loads only the layout's `<head>` | move site-wide CSS into the layout folder (`transfer.mjs ./theme --theme <id>`) |
| the header does not stick | either the block sets `props.overflow: "clip"` (a clipping ancestor defeats sticky), or you asked for stickiness inside your own markup instead of on the block | `vz-sticky` on the root node, or `props.position: "sticky"`; drop any `position: fixed` spacer — that workaround is obsolete since 2026-08-19 |
| a **platform** `siteHeader`/`siteMenu`/`siteFooter` ignores `props.position` | those kinds draw their own band; the block wrapper never runs for them | `siteHeader` has its own `props.sticky: true`; the other two have none — own markup in the kit instead |
| a block sticks on the live site but not in the editor preview | the editor canvas does not ask the wrapper for stickiness | check on the shop URL. Nothing to fix |
| your own class on a block is missing from `class=` | names are validated whole and dropped whole: max 3, `^[a-zA-Z_][a-zA-Z0-9_-]*$`, nothing starting with `vz-` | rename it; see `vizen-own-markup` §4 |
| you set `props.wrapperClass` and your CSS does nothing | the CSS is in a **level 1** document, so it is inside a shadow root and cannot see the wrapper | move that stylesheet into a **level 3** document |
| deleting fails with `invalid request body` | `DELETE` needs a body | send `{}` |
| ~~an image URL 404s on `HEAD`~~ | **closed since 2026-08-20** — the resizer answers `HEAD` and `GET` alike; `HEAD` is a fine way to check that an image URL resolves | — |
| two product cards on one page | a bound `layout` block plus the resource's own bundled card | see `vizen-chrome`, card replacement |
| a **level-1** widget renders without the site's fonts and colours | it used to: a shadow root isolates in both directions, so the layout's stylesheets never crossed it. Since 2026-09-13 they are put inside the shadow root and your markup is wrapped there in `.vz-theme-{doc}` — the theme applies, and your own rules still win. Still bare? the page has no layout at all, or the rule is compound (`body.dark`), which is never rescoped | bind a layout (`vizen-chrome` §2a), or move that class onto your own root element |
| a level-2 menu panel opens only after a short pause | **not a bug.** The server prints the panels into the HTML within a node budget (60 nodes); 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` |
| your CSS styles the header and nothing else on the page | a **level-1** header keeps its CSS inside its shadow root (a level-3 header's stylesheet reaches the whole page — but not the editor canvas) | move it into the **layout's folder**, where it lands on every page and in the canvas: `vizen-own-markup` §6.2 |
| a style, font or `<head>` tag appears on one page only | it was uploaded as a page widget instead of the layout folder | same fix — `transfer.mjs ./theme --theme <id>` |
| layout styles apply but the layout's script never runs | you are on `/cart`, `/checkout`, `/account`, `/wishlist`, `/orders`, `/deals` or `/dashboard` — author code is not executed in the guarded zones by design | nothing to fix; do not plan scripted behaviour there |
| your script worked on a direct hit and stopped after an in-site click | navigation is soft; the code ran once on load | re-init on `document`'s `vz:navigate` event (`detail.path`), not on `DOMContentLoaded` |
| `body {}` / `:root {}` from the layout folder did not apply | the selector was compound (`body.dark`, `body[data-x]`) — only bare `html`/`body`/`:root` are scoped onto the page root | put that class on your own root element, or use the bare selector |
| `PUT /html-documents/{id}` on a layout's folder answers `THEME_DOCUMENT_PROTECTED` | the folder's document is a service row; it is changed through releases only | `POST /themes/{id}/site/releases` → PUT bytes → `…/publish` |

Something silently ignored that is not listed here belongs in the area document —
`backend-3D/docs/api-areas/` — as a named case.

## 3. Acceptance checklist

Run all of it before telling the owner you are done.

- [ ] the pages you did **not** touch look unchanged — check the home page and
      two sections;
- [ ] the page opens by direct URL **and** by clicking a link inside the site —
      these are different code paths, and scripts behave differently;
- [ ] after an in-site click: dropdowns open, modals open, timers run;
- [ ] the header holds at several widths, not just yours;
- [ ] no horizontal scrolling on the page body;
- [ ] the cart really accepts an item — press the button, watch the counter;
- [ ] the text is present in the raw HTML (`curl` and grep for it), otherwise
      search engines will not see it;
- [ ] images are served through the resizer, not as originals;
- [ ] the owner is told plainly what is a workaround and what is not.

## 4. Measuring, not guessing

Install a browser in the scratchpad and drive it. Status codes cannot tell you
whether a dropdown opens or a header sticks. One run spent 18% of its steps on
this, and it was the cheapest 18% in the whole build — every finding that
survived came from a measurement, and the one finding that did not survive came
from reading code instead.

## HTML widget (folder / release) — what "nothing changed" usually means

- **`PUT /html-documents/{id}` answers `HTML_PROJECT_USE_RELEASE`.** The document has an active release: change it only through a release (`POST …/releases` with `base:"active"` + the changed files → PUT bytes → `POST …/releases/{rid}/publish`). Name/description still go through PUT.
- **Files 404 on the site right after publish.** Look at the page source: the asset root must be the NEW release (`/_html/{doc}/{rid}/`). Only the active release is served; old rids answer 404 by design. Hard-reload the page (the html is not cached, the files are immutable per rid).
- **Second widget on the same page breaks the first one's images.** The script used the global `window.VZ_ASSET_BASE`, which the last widget overwrites. Read the root from the wrapper instead: `document.currentScript?.closest('.vz-body')?.dataset.vzBase` (inline) or `document.querySelector('.vz-body-<id>')?.dataset.vzBase` (external/defer).
- **`:root`/`body` styles apply on the site but not in your local file, or vice versa.** On the site they are rewritten to `.vz-body-{doc}` (both inline `<style>` and the release's `.css` files). Selectors like `body.dark` stay untouched — put such classes on your own root element.
- **Publish answers 422 `RELEASE_INVALID`.** Read `report.errors`: `ref.missing` (a path not in the release — check case and folder), `ref.outside` (`../` outside the folder), `hash.mismatch` (bytes differ from the declared sha — re-upload, the server does not "fix" the digest), `file.forbidden`, `sw.register`. Warnings (`page.extra`, `js.assets`, `img.large`) do not block.
- **`FILES_PENDING` (409).** Some `upload_url`s were never PUT. Upload them and publish again — the release id stays.
- **ZIP rejected as too large.** The limit is 60 MB per request; split media into a first release and send the rest with `base:"active"`.
- **The page did not change after `transfer.mjs`.** Since revision 6 the page is mounted only after a successful publish; a validator error leaves the page untouched — fix the report and rerun.
- **A component release is refused `422` with `component.manifest` or `COMPONENT_MANIFEST_INVALID`.** A `kind=component` release must carry `component.json` next to `index.html`; the code names the offending field in `ref` (`params[0].key`). Keys are `^[a-z][a-z0-9_]{0,31}$`, ≤ 32 of them, the file ≤ 16 KB, `select` needs non-empty `options`, and a `default` must be of the parameter's own type. An unknown **type** is an error on purpose — the type is what decides the escaping.
- **A component release is refused with `COMPONENT_TS_ERROR` / `COMPONENT_TS_CONFLICT`.** `*.ts` in a component folder is compiled on publish into a sibling `*.js`: the error carries `file:line:col`, the conflict means a hand-written `app.js` already sits next to `app.ts` (drop one). Import paths are not rewritten — write `"./util.js"`, the file the browser fetches.
- **A component is on the page and shows only defaults.** The write path is `props.component.ref` / `props.component.params` and nothing else; `props.params` or `ref` next to `kind` is stored, answers 200 and renders defaults. A key that is not in the schema, or a value of the wrong type, is dropped the same silent way — read the page, not the status code.
- **A component cannot be placed at all.** `active_release_id` on the document is `0`: no published release means no markup and no schema. Publish the folder first; the parsed schema comes back as `manifest` on the document and on each release, so never read `component.json` through the gateway.
- **Admin canvas shows the block without styles.** The canvas loads files through a signed preview root; if you see raw markup, the block was read by an old client — reopen the page editor. On the live site the same document is fine.
