Wocart

Composition

Think of a page as a tree. The page holds sections; a section can hold smaller blocks, and a block can hold blocks too. A named space where a component accepts children is a slot.

Some blocks must always be present: a product page cannot lose its buy button, for example. The SDK enforces these rules when a document is edited, so the renderer can trust the document it receives.

You can skip this page if your theme has no editable sections or nested blocks. If you are new, start with sections and add slots only when a section really needs child blocks.

Sections and blocks

PlacementWhere it can go
"section"The top level of a surface or area
"block"Only inside a slot
"both"Anywhere
absentAnywhere

The document operations enforce placement, and checkPageData — the writer's check before saving — reports a section that may only be a block (not-placeable). The renderer and validatePageData never reject a saved instance for its placement: a document already saved renders as it is.

Declare a slot

A slot is a named place a component lets children go.

TypeScriptCode
const productMain = defineComponent({ // … slots: [ { name: "info", allow: [ "product-title", "product-rating", "product-price", "product-order", ], max: 16, required: [{ type: "product-order", max: 1 }], }, ], });
RuleEffect
name"default" or a lowercase kebab-case name
multiple: falseKeep the first child, report the rest
allowOnly these component types; omitted means any
maxAt most this many instances (up to 50)
requiredThese types must be present; each has its own min / max

allow is applied before multiple: false, so a disallowed child never displaces an allowed one. Narrowing allow later is a breaking change for saved pages: children that no longer fit are dropped with disallowed-child.

A child in an undeclared slot is dropped with unknown-slot. A component with no default slot drops children pushed as default with undeclared-children.

Render the blocks

The renderer gives every component a blocks prop. A component places each slot where the design wants it, and each fixed block where it belongs.

Code
--- import Blocks from "@wocart/theme/astro/Blocks.astro"; import Block from "@wocart/theme/astro/Block.astro"; import { hasBlocks, type BlocksHandle } from "@wocart/theme/astro"; const { blocks } = Astro.props as { blocks: BlocksHandle }; --- <section> {hasBlocks(blocks, "info") && ( <div class="info"><Blocks of={blocks} slot="info" /></div> )} <Block of={blocks} name="gallery" /> </section>

<Blocks> renders a slot's children through InstanceView, so load, issues, hidden blocks and preview markers work at every depth. <slot /> still works for default children; a component can use either mechanism.

blocks is reserved. No field may use that key.

hasBlocks(blocks, slot) is true when the slot has at least one visible block, so a component can skip an empty wrapper. isPreview(blocks) is true in a Wocart Studio preview, where a section with nothing to show yet may draw a placeholder. A shopper never sees that placeholder.

Fixed blocks

A fixed block is placed by the component's own code. An editor can change its fields and fill its slots, but cannot remove, move, duplicate or hide it.

TypeScriptCode
const productMain = defineComponent({ // … fixed: [{ name: "gallery", type: "product-gallery", label: "Gallery" }], });
Code
<Block of={blocks} name="gallery" />

The block renders even when the document does not store it, using its defaults. Its id is derived from the owner and name (fixedBlockId). Stored values for a fixed block that the definition no longer declares are removed with unknown-fixed; a wrong type is replaced with a default and reported fixed-type-mismatch.

Required blocks

A required block may move, but it cannot disappear. A slot or surface lists them:

TypeScriptCode
slots: [ { name: "info", allow: ["product-order", "product-countdown"], required: [{ type: "product-order", max: 1 }], }, ]

The operations refuse to remove, hide or move out the last min of a required type, or to insert past its max. A document that lacks one — an old document, or one saved before a theme update — renders it anyway, added at the end of its slot, and the reader reports missing-required-block.

A product or landing surface must guarantee an order form. Give the order form's component role: "order-form" and require it through the surface's required sections and their fixed or required blocks. validateThemeDefinition reports a product or landing surface that does not (order-form-not-guaranteed): a merchant could otherwise remove the only way to order.

TypeScriptCode
const productOrder = defineComponent({ type: "product-order", role: "order-form", // … });

The order form's fields are not the theme's: the store's checkout form is set in the merchant dashboard and enforced by the server. The theme supplies its markup and style.

Presets

A preset is a ready-made configuration offered on insertion: props over the field defaults, plus children for slots.

TypeScriptCode
presets: [ { key: "with-grid", label: "Hero with products", props: { layout: "columns" }, slots: { aside: [{ type: "product-grid", props: { limit: 2 } }] }, }, ]

Presets are validated at assembly by building them. instantiatePreset(theme, type, key, newId) creates the instances, each with an id from newId.

Limits

LimitValue
Instances per slot50 (THEME_LIMITS.slotInstances)
Sections per surface or area50 (THEME_LIMITS.sections)
Instances per document1,000 (THEME_LIMITS.documentInstances)
Nesting below a section6 (THEME_LIMITS.depth)

A slot or surface max can set a lower number. The reader renders up to the limit and reports limit-exceeded; the operations refuse to go past it. Assembly refuses a fixed or required block that contains itself or nests deeper than the limit.

Hidden instances

JSONCode
{ "id": "faq-1", "type": "faq", "hidden": true }

The renderer skips the instance, its load and its children. The reader still checks it, so a hidden section cannot hide a contract error. A hidden required or fixed block is shown again by completePage; those are guarantees, not preferences.