Vizen Shop

Transfer a finished site into a Vizen shop

/docs/transferdraftEN· проверено 2026-09-12

Резюме по-русски. Готовый сайт переносится как есть: папка = html-виджет уровня 3 в настоящем DOM страницы (скрипты работают, position: fixed работает, Shadow DOM и iframe нет); лейаут = папка общих стилей, шрифтов и фавикона; шапка и футер = html-виджеты в группах хрома; страница товара или раздела = шаблон в слоте body. Перепривязываются только данные магазина: меню, цены, корзина, формы. Здесь — что платформа делает с твоим файлом (§7), что едет 1:1, что перепривязывается и что не едет (§8), заметки по React/Vite, Tailwind и экспортам из дизайн-инструментов (§9). Итерации — дев-ключом: прод-ключ пишет в живой сайт сразу.

Status: draft · Serves: GET /docs/transfer · Verified: 2026-09-12, live shop vezu-vezu (company 14), storefront code, stand :3970 · Owner: tokens and web-coding docs line

1. What you can do here

A site that already exists — an HTML/CSS/JS folder, a static React/Vite export, a Figma, Webflow or Tilda export — goes into the shop as it is. One folder per page becomes one level-3 widget; one folder becomes the site skeleton (the layout); header and footer folders become chrome widgets; product and category folders become templates. Nothing is cut into sections, no path is rewritten, nothing is wrapped in Shadow DOM or an iframe. What the platform adds is the page around your file (header, footer, the site's own stylesheet) and the live data you re-bind. §7 says exactly what happens to the file; §8 what cannot travel.

The one-command path is backend-3D/tools/html-transfer/transfer.mjs; every step below names the raw API next to it.

2. Decide first (forks)

If you wantTake this pathCost
a site that already exists in a browsertransfer — this document, skill vizen-transferdata that the shop owns (menu, prices, cart, forms) is re-bound, not copied
a page that does not exist yetbuild it — vizen-start and its three questionsnot this document
one page inside the shop's existing frameone folder, --page <slug>; the shop's header and footer staynone
the whole site: your header, footer, fonts, product pageslayout folder + two chrome groups + templates + N page folders, in the order of §4the order matters: chrome after pages means restyling twice
static content (text, images, prices typed in)copy as isprices typed in lie the moment the catalogue changes
live content (menu, product lists, prices, cart, forms)re-bind to the shop's data with vz- keys, or keep a platform widgetthe feed in own markup is ≤ 48 rows without filters or paging; filters mean the platform productListing
your own headeran html widget in a chrome group; menu and cart counter from vz-for menu and vz-cart-countin guarded zones it renders without its scripts
the platform header restyledsiteHeader with props.wrapperClassvizen-chrome §4athe look is bounded by its props
iterations while the site is being assembleda dev key: a draft at <slug>--dev.<zone> under the owner's sessionthe owner publishes; nothing is visible to shoppers
the final publisha prod key, or the owner's publish of the draftevery release on a prod key is live at once — one shop spent 16 header releases as 16 live edits

3. Objects and where they live

Everything you upload is a release of a document; everything you place is a binding.

ObjectCreateReadWrite path
html document (the project)POST /html-documents {"item":{"name":"…","level":3}}result.idGET /html-documents/{id}source.url = the active release's index.html, already cleanname and description only; content goes through releases (HTML_PROJECT_USE_RELEASE otherwise)
release (a snapshot of the folder)`POST /html-documents/{id}/releases {"base":"active"\"none","files":[{"path","sha256","size"}],"delete":[],"note":"…"}{release_id, upload[], reused[], inherited[]}PUT each upload[].upload_url (no Authorization) → POST …/releases/{rid}/publish{url, path, embed_prefix, seo, report}`GET …/releases, GET …/releases/{rid} (each carries preview_base)422 RELEASE_INVALID carries report.errors; 409 FILES_PENDING lists what is not uploaded; POST …/rollback moves the pointer
release from a ZIPPOST /html-documents/{id}/releases/zip?publish=1, multipart filethe document must already exist; nothing is mountedsame≤ 60 MB per request
html block (the widget on a surface)POST /content-blocks {"item":{"name":"…","sections":[{"type":"text","payload":{"v":2,"kind":"html","props":{"html":{"html_document_id":<doc>,"level":3}}}}]}}GET /content-blocksPUT /content-blocks/{id} replaces item whole; props.html.boxed: true asks for the platform box
pagePOST /categories {"item":{"name":"…","type":"page","is_published":true,"seo":{"slug":"…"}}}; the slug must be free (GET /categories/by-slug/{slug}?company_id=… → 404)GET /categories/by-slug/{slug}?company_id=…content_blocks[]PUT /categories/{id}/content-blocks {"id":<page>,"items":[{"block_id","sort_order"}]} — a replace-set
group (an ordered list of widgets)`POST /content-blocks {"item":{"name":"…","group_type":"chrome"\"product"\"category","sections":[{"type":"text","payload":{"v":2,"kind":"group","props":{"widgets":[<block ids>]}}}]}}`GET /content-blocksPUT /content-blocks/{id}
binding (which group, where)`PUT /design/bindings/{shop\category\category_children\product\article\theme\system}/{id} {"slots":{"header":<G>,"footer":<G>,"body":<G>}}shop takes id 0; slots` is a replace-setGET /design/bindings/{type}/{id}resolved with the source of every slot; GET /v1/storefronts/resolve?slug=…chrome{"slots":{}} unbinds
layout (site skeleton and shared files)exists: GET /content-blocks, rows with group_type:"theme", the earliest is the shop's defaultGET /themes/{id}/sitebase, scope_class, files[], icons_generated[]transfer.mjs ./theme --theme <id>, or POST /themes/{id}/site/releasesPUT…/publish; --theme and --page never together
page SEOtaken from <title> and <meta name="description"> on publishGET /categories/by-slug/…seotransfer.mjs writes seo.meta_title / seo.meta_description when empty (--seo-overwrite to replace); by hand: PUT /categories/{id} {"item":{"seo":{…}}}

Scopes: catalog:read, catalog:write, storage:write; the role on the shop must be owner or admin for level 3 (HTML_SCRIPT_ROLE_REQUIRED otherwise). The server flag for authored markup is on in production.

4. Recipes — in the order of a real transfer

Each recipe ends with the check that proves it. "200" proves nothing here.

4.1 Inventory

Write one table before the first call:

SourceTargetData to re-bindDoes not move
index.html (home)page widget, --page homeproduct strip → vz-for="p in products" or productListing
partials/header.htmlchrome group, slot headermenu → vz-for="m in menu", cart → vz-cart-count
css/, fonts/, faviconlayout folder, --theme
product.html (mock-up)product group, slot body{{ product.* }}, vz-add-to-cartown fetch of /api/products
app.jsx router, stateSPA runtime: export routes statically first

4.2 The key

GET /v1/account/token. contour: "dev" and writes_to_live: false — iterate freely. contour: "prod" — every publish is live: say so to the owner and ask for a dev key before the first release. store.storefront_url is the address you will open; warnings[] go to the owner verbatim.

4.3 Layout folder — site-wide CSS, fonts, favicon, scripts

curl -s -H "Authorization: Bearer $VZ" $API/content-blocks \
  | jq '[.result[] | select(.group_type=="theme") | {id,name}] | sort_by(.id)'   # earliest = default
node backend-3D/tools/html-transfer/transfer.mjs ./theme --theme <id>

index.html here is the skeleton: <head> nodes land in the head of every page, the <body> class on the page root, body content before </body>; *.css has bare html/body/:root scoped to .vz-theme-{doc}; icon.png generates the favicon set. Scripts read window.VZ_THEME_BASE. Full folder rules: /docs/own-markup §7.2.

Check: GET /themes/<id>/site answers base and scope_class; open any page and find class="… vz-theme-{doc}" on the page root and your stylesheet in <head>.

node backend-3D/tools/html-transfer/transfer.mjs ./header --name "Header"   # prints: создан HTML-проект #<doc>
node backend-3D/tools/html-transfer/transfer.mjs ./footer --name "Footer"

Without --page the command creates the document and publishes the release; nothing is mounted. Then three calls per part: the html block (§3), the group (group_type:"chrome", props.widgets:[<block>]), the binding:

curl -s -X PUT -H "Authorization: Bearer $VZ" -H 'Content-Type: application/json' \
  $API/design/bindings/shop/0 -d '{"slots":{"header":<G_header>,"footer":<G_footer>}}'
# one section only: /design/bindings/category/<id>; one layout: /design/bindings/theme/<id>

A sticky header is vz-sticky on your root node — it reserves its height; position: fixed does not, so it is not the header tool (it works, and it is fine for a side rail or a modal).

⚠️ Guarded zones strip more than scripts. On /cart, /checkout, /account and the other guarded pages the widget is served in static mode: the <script> nodes go, and a relative <link rel="stylesheet" href="header.css"> goes with them — the header renders unstyled (measured on the stand, 2026-09-12). Inline <style> survives, and the layout folder's stylesheet is linked in the page <head> everywhere, guarded zones included. So put the header's CSS in the layout folder (§4.3) or inline it in <style>; keep the <link> form for pages only.

Check: GET /v1/storefronts/resolve?slug=<shop>chrome names your documents; open the home page, a product page and /cart — the header is there on all three, on /cart without its scripts and still styled.

4.5 Pages

curl -s -H "Authorization: Bearer $VZ" "$API/categories/by-slug/home?company_id=<N>"   # 404 = free
curl -s -X POST -H "Authorization: Bearer $VZ" -H 'Content-Type: application/json' $API/categories \
  -d '{"item":{"name":"Home","type":"page","is_published":true,"seo":{"slug":"home"}}}'
node backend-3D/tools/html-transfer/transfer.mjs ./home --page home --validate-only
node backend-3D/tools/html-transfer/transfer.mjs ./home --page home

The first run creates the document and mounts the block after a successful publish; a validator error leaves the page untouched. A rerun is a new release and uploads only the files whose sha256 changed (index.html changes on every run — the converter rewrites it). --doc <id> --list shows the history, --doc <id> --rollback moves the pointer back. Older html blocks on that page are unmounted (--keep-blocks keeps them).

The page keeps the platform's technical layer above your widget — breadcrumbs and the page heading — because a page is a category and draws its bundled blocks. For a 1:1 page switch that layer off with the page flags on the zone block __page:top (/docs/webcoding §16.1): create or update it with {"kind":"zone","refs":[<your block id>],"page":{"hideSystemBlock":true}} and bind it with your block (PUT /categories/{id}/content-blocks is a replace-set). hideSiteHeader / hideSiteFooter do the same for the chrome when the page brings its own. transfer.mjs mounts the block and leaves the layers as they are.

Check: open the url the command prints — on a dev key it is the <slug>--dev host, under the owner's session — and confirm nothing of the platform shows above your first section.

4.6 Product and category templates

node backend-3D/tools/html-transfer/transfer.mjs ./product --name "Product template"
# html block → group group_type:"product" with props.widgets:[<block>] → binding:
curl -s -X PUT -H "Authorization: Bearer $VZ" -H 'Content-Type: application/json' \
  $API/design/bindings/category_children/<root category id> -d '{"slots":{"body":<G_product>}}'

category_children reaches every product of the subtree; /product/<id> is one product. A category template is a group category in the slot body; keep the platform productListing inside the group when filters and paging are needed — the own-markup feed is ≤ 48 rows. Inside the template the page's product is product ({{ product.name }}, {{ product.price_text }}, product.gallery[].url, product.attributes[]), the section is category, crumbs and menu are everywhere.

Check: open two products — one card each; the printed price equals GET /products/{id}; vz-add-to-cart puts the item in the cart.

4.7 Re-bind data

In the sourceOn the platform
a hand-written menu<li vz-for="m in menu"><a href="{{ m.url }}">{{ m.name }}</a></li> (children: c in m.children)
a product strip with typed prices<article vz-for="p in products"> … {{ p.price_text }} … <button vz-add-to-cart="{{ p.id }}">
a product page mock-up{{ product.* }}, vz-gallery, vz-variants, vz-add-to-cart, vz-fav
a contact form<a href="form:<id>"> (forms: skill forms, /forms)
internal links/path on the site, or vz:page/<id> — never the export host
a cart icon with a counter<span vz-cart-count>

vz-for and vz-if go on different nodes. Names and scope fields: GET /docs/vz-keys.json — an unknown vz- attribute is removed silently.

Check: curl <page> | grep '<a real product name>' finds it in the raw HTML; adding an item changes the counter.

4.8 Check and report

  • open by direct URL and by a click from another page of the site — two code paths;
  • screenshots at 1440 and 390;
  • console: no errors, no 404s under /_html/;
  • /cart shows your header and footer;
  • --doc <id> --list shows the active release.

The report: the links, both screenshots, what was re-bound, what was dropped and why, the rollback command. The token appears nowhere.

5. Silently ignored

You wroteWhat happensDo instead
document.addEventListener('DOMContentLoaded', init)fires on a direct hit; after an in-site click the document is already loaded and init never runs — page two looks deadcall init() immediately and on document's vz:navigate
<meta property="og:*">, <base>, <link rel="manifest">, <meta http-equiv>dropped on publish (head.stripped)og-images: the page's SEO fields; a manifest: not supported
body { overflow: hidden } from a modal scriptapplies to .vz-body-{doc}, not to the real body — no scroll locklock scroll on your own root element
fetch('data/x.json'), img.src = 'assets/a.jpg' in a .js fileresolves from the page URL → 404 on the site (js.assets warning)prefix with the widget root: dataset.vzBase / window.VZ_ASSET_BASE
<title> and <meta name="description"> when the page already has SEOkept as they were--seo-overwrite
props.wrapperClass, blockWidth, paddingSides on a level-3 block without boxedthere is no box to land on; nothing changesyour own CSS on your root; or props.html.boxed: true and then the axes
a second .html in the foldernot served (page.extra)one page = one folder
a header, footer or template folder without <title>a page.noindex warning — normal for a fragment that is not a pageignore it for chrome and templates; pages carry a <title>
a file referenced only from a .js (img.src = base + 'assets/a.svg')a file.unused warning — the validator reads HTML and CSS references, not scripts; the file is uploaded and served all the samekeep the VZ_ASSET_BASE fallback in the script and ignore the warning
a path inside a CSS comment (/* url("fonts/x.woff2") */)the validator reads url() inside comments too — ref.missing error for a file that is not in the folderdelete the example path or add the file
<script> in the header on /cart, /checkout, /accountnot executed, no errormarkup and styles only there
<form action="…"> without a handlera form.nohandler warning; the browser posts to the export hosthref="form:<id>"
a .vizenignore with one linereplaces the defaults (.*, node_modules/, previews*/, originals/, README*, *.md, qa.mjs, *.log)repeat the defaults you rely on
--theme <id> --page <slug>refused: a layout is not mounted on a pagetwo commands

6. Limits

LimitValue
files per release500
index.html2 MB
any file50 MB
release / ZIP body60 MB
own-markup feed products[]48 rows
block sections JSON64 KB — never inline HTML there
wrapperClass≤ 3 names, ^[a-zA-Z_][a-zA-Z0-9_-]*$, none starting with vz-
rate limits per company per minuteread 600, write 240, storage 180
image file (advice, not a refusal)≤ 400 KB, width/height set (img.large, img.nosize)

Validator codes — errors: ref.outside, ref.missing, ref.http, file.forbidden, sw.register, hash.mismatch, html.parse; warnings: page.extra, js.assets, head.stripped, form.nohandler; info: img.large, img.nosize. Source: internal/api/htmlrelease/validate.go.

7. The runtime contract — what the platform does to your file

  1. Wrapper. <body class="dark" data-x="1"> becomes <div class="vz-body vz-body-{doc} dark" data-x="1" data-vz-base="/_html/{doc}/{rid}/">. Clean markup gets the same wrapper. There is no .vz-box/.vz-inner around it unless props.html.boxed: true. The div sits in the page <main> between the header and the footer.
  2. Head. link, style, script, noscript are kept in a hidden div.vz-head-{doc} before the body — stylesheets load, scripts run; title and meta description go to the page SEO; everything else is dropped.
  3. CSS scoping. Bare body, html, :root in <style> and in the release's .css files are rewritten to .vz-body-{doc}; compound selectors (body.dark) are left alone. src, href, srcset, url(), @import are rewritten to the widget root on output — the files themselves are not touched.
  4. The cascade is shared. Level 3 is real page DOM without style isolation. The storefront's stylesheet applies to your markup, and it ships Tailwind v4 preflight in @layer base (measured on the live CSS, 2026-09-12): *{margin:0;padding:0;border:0 solid} · h1,…,h6{font-size:inherit;font-weight:inherit} · a{color:inherit;text-decoration:inherit} · ol,ul,menu{list-style:none} · img,svg,video,canvas{display:block;vertical-align:middle} · img,video{max-width:100%;height:auto} · button,input,select,textarea{font:inherit;background:transparent;border-radius:0} · button{appearance:button}. Your CSS is unlayered and wins on equal specificity, so everything you styled explicitly survives; what you left to browser defaults — heading sizes, bullets, link underlines, button chrome — is reset. Ship your own base styles under your root class. The page font is ui-sans-serif, system-ui at 16 px; your body { font-family } lands on .vz-body-{doc} only. A stylesheet linked from any level-3 widget reaches the whole page (the product card of vezu-vezu takes its font from the header's global.css); the editor canvas, however, loads only the layout's <head> — site-wide CSS belongs in the layout folder (§4.3).
  5. Scripts. <script> and <link> run when the browser parses the page. After an in-site click the platform re-creates every <script> node of the widget so it runs again; a CustomEvent('vz:navigate', {detail:{path}}) fires on document after every navigation and after the first load. Write code that works when executed right now, and re-init on vz:navigate. External and defer scripts have no document.currentScript: read the root from .vz-body-<id> or window.VZ_ASSETS[<id>]; two widgets on one page overwrite the bare window.VZ_ASSET_BASE for each other.
  6. Guarded zones. On /cart, /checkout, /account, /wishlist, /orders, /deals, /dashboard your markup and styles render and your scripts are stripped server-side; the layout's head_html/foot_html are not emitted there either. Moving into such a page is a full reload.
  7. Paths. /_html/{doc}/{rid}/… is immutable and served byte-for-byte — no resizing of your own images, so size them yourself. Shop images written as <img src> are rewritten to the resizer with srcset; set sizes. Only the active release is served; old rids answer 404 by design.
  8. Geometry. The page <main> drops CSS layout containment when it holds an unboxed level-3 block, so position: fixed, 100vw and 100dvh resolve against the viewport (a fixed side rail measured 900 px tall in a 900 px window; 844 in 844). The price: .vz-vis-* visibility ranges of other blocks do not work on such a page.
  9. Layout folder deltas. Scope class .vz-theme-{doc} on the page root, real <head> tags, window.VZ_THEME_BASE, favicons from icon.png/docs/own-markup §7.2.

Anchors: packages/sections/src/sections/SectionHtmlAuthored.tsx, storefront/src/widgets/html/Slot.tsx (hasUnboxedAuthored), storefront/src/lib/render/PageShell.tsx and storefront/src/app/globals.css (vz-has-authored, @import 'tailwindcss'), pkg/htmlrefs/fragment.go (SetBodyBase, scoping), internal/api/catalog/get_html_document_content.go.

8. copy · remap · drop

In the finished siteVerbWhere it lands
page markup, CSS, JS, fonts, imagescopypage folder → level-3 widget on a type:"page" page
header, footercopy + remap menu and cart counterchrome group, bound to the shop, a section or a layout
shared CSS, fonts, favicon, a site-wide scriptcopythe layout folder
product page, category pagecopy the markup, remap the dataproduct/category group in the slot body; {{ product.* }}, {{ category.* }}, products[]
menu, product lists, prices, add-to-cart, wishlistremapvz-for, price_text, vz-add-to-cart, vz-fav — or the platform widgets
formsremaphref="form:<id>", the forms skill
internal linksremap/path, vz:page/<id>
<title>, <meta name="description">remap (automatic)page SEO
browser-default styling (unstyled headings, bullets, links)remapyour own base styles under the root class (§7.4)
cart, checkout, account, ordersdropsystem pages of the platform; yours are the header, footer and layout only
React router, state, own fetch, SPA shelldropexport statically, one route = one folder (§9)
other <head> nodes, service worker, server code, http:// resourcesdrop

9. Framework notes

  • React / Vite / Next. A client-rendered SPA ships an empty index.html and draws everything by script: no text for search engines and a page that looks dead until the bundle runs. Export statically (SSG or prerender), one route = one folder = one widget, base: './' so the asset paths are relative. The router, global state and data fetching do not move — the shop's data comes through vz- keys or platform widgets. Scripts must start immediately and re-init on vz:navigate. CORS is open for localhost, so a local dev server may call the API with the token to preview real data; the published widget does not fetch content on its own.
  • Tailwind in the project. Compile it into the folder; do not load the CDN runtime. Your compiled CSS sits above the platform's @layer base and wins.
  • Figma-to-code, Webflow, Tilda exports. Ordinary folders. Typical fixes: assets that point at the export host (download them into the folder — http:// is refused), a <base href> (dropped anyway), jQuery $(document).ready and window.onload (run once; re-init on vz:navigate).
  • Fonts. woff2 in the page folder or in the layout folder with a relative url() in @font-face; <link rel="preload"> in the layout's <head>.
  • Images. Release files are served as they are — resize before upload. Catalogue images arrive pre-sliced through the scope fields (p.preview).

10. How this was verified

  • 2026-09-12, live shop vezu-vezu (company 14), prod key of the owner: GET /v1/account/token; GET /html-documents — 15 documents in three generations (level 1 → level 1 → level 3), 31 releases in total; the header document's global.css styles the product page (h1 computed font Montserrat 26 px 700 from the header's stylesheet); a position: fixed rail measured 900 × 161 px in a 1440 × 900 viewport and 844 px tall at 390 × 844 (playwright-core, Chromium).
  • Live storefront CSS (/_next/static/chunks/*.css) grepped for the preflight rules listed in §7.4; Tailwind 4.3.2 in vizen-market/node_modules.
  • Code read: SectionHtmlAuthored.tsx, Slot.tsx, PageShell.tsx, globals.css, htmlrefs/fragment.go, htmlrelease/validate.go, tools/html-transfer/transfer.mjs.
  • 2026-09-12, stand local-stand.sh (:3970/:5621), shop proverka-limita-1, starter kit tools/html-transfer/examples/: layout folder (favicons generated), header + footer bound to the shop, page demo, product template bound to category_children; measured with playwright-core: fixed rail 900 px in a 900 px window, h1 36 px from base.css, menu from vz-for, the widget root /_html/48/207/ in the script's note, /cart with header and footer markup but without the header's relative stylesheet, page breadcrumbs and heading above the widget until hideSystemBlock. The document stays draft until the owner's first external transfer passes.

11. Negations in this document (rule §3.9)

The claimKept true by
level 3 is not sanitised and not wrapped in Shadow DOM or an iframeSectionHtmlAuthored.tsx renders light DOM; get_html_document_content.go skips the sanitiser for level 3
position: fixed is not contained by the page on an unboxed level-3 blockglobals.css rule main.vz-visibility-scope.vz-has-authored { container-type: normal }; measured rail (§10)
a ZIP release does not create the documenthtmlrelease/zip.go resolves an existing document id; only transfer.mjs creates documents and mounts blocks
a header widget's CSS is not confined to the header at level 3measured on vezu-vezu (§10); no gate — after-wave pass
scripts do not run in the guarded zones — and a relative <link rel="stylesheet"> of the widget is not emitted there eitherstorefront/src/lib/forbiddenZones.ts, static mode in Slot.tsx; measured on the stand 2026-09-12 (/cart: header markup present, header.css absent, layout global.css present)
release files are not resizedhtmlgw serves bytes as stored; no gate — after-wave pass
npx serve . does not show what the site showsby construction: no page, no cascade, no substitutions locally; no gate

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

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