Wocart

Fields

A field is one control in the editor: a text box, an image picker, a number, a color choice. If something is not a field, a merchant cannot change it from saved data.

The component definition declares the field. The manifest shows it to the editor. The page document stores its value. The value is checked again when the page is read, so a saved document cannot bypass the rules.

Most themes begin with text, image, number and select. Add the other kinds when the component needs them.

Scalar and text fields

KindStored valueNotes
text, textareastring or per-language mapOne of default or contentKey, never both
numberfinite numberOptional min, max; with step, values lie on a grid starting at min or 0
booleanboolean
selectone of optionsdefault must be an option; optionLabels names each option
urlsafe link targetSite path, #fragment, ?query, http(s)://, mailto: or tel:; script and protocol-relative tricks are rejected
color#rgb or #rrggbbA color name or url(…) is rejected

display changes how an editor shows a field, never what is stored:

TypeScriptCode
{ key: "align", kind: "select", label: "Alignment", options: ["start", "center", "end"], optionLabels: { start: "Start", center: "Center", end: "End" }, default: "center", display: "segmented", }

Store references

These fields store a handle or key, not a copy of the data.

KindStored valueRead with
menumenu handle (main-menu)storefront.menus.get(handle)
collectioncollection handle (best-sellers)storefront.products.list({ collection })
productproduct handlestorefront.products.get(handle)
categorycategory slugstorefront.categories.list()
imagemedia key, never a URLpictureOf(image, config.media)
productListordered product handlesstorefront.products.get(handle) per item
collectionListordered collection handlesstorefront.collections.get(handle) per item

productList and collectionList cap at 50; their handles must be distinct. A deleted product or collection reads as absent, not as an error.

Structured fields

KindStored valueNotes
richtext{ type: "doc", version: 1, content } or one per languageFixed shape; formats narrows it; render with richTextToHtml
list[{ id, props }]Item fields of any kind except list and colorSchemes; itemLabel names the title field
style{ spacing?, layout?, size? }, per breakpointSteps and choices, never CSS; see Style and color
colorSchemea scheme idThe store must have that scheme
colorSchemes[{ id, name, colors }]A theme setting, at most one; at most 24 schemes

A rich-text document contains paragraphs, headings 2–4, bullet and ordered lists, line breaks, bold, italic, underline, strike and safe links. Nothing else is representable, so a stored value cannot carry markup or scripts.

Code
--- import { richTextToHtml } from "@wocart/theme"; const { body } = Astro.props; const store = Astro.locals.store; --- {body && <div set:html={richTextToHtml(body, { href: store.href })} />}

href turns each stored link into the href to render; passing the request store's localizer keeps an internal link in the shopper's language.

richTextToPlainText gives the words for a meta description or an aria-label. A field with inline: true is one line: a single paragraph with marks and links, rendered without a block element.

A list item's id is unique within its list, so reordering never changes which item is which. Item fields cannot use contentKey: every item would read the same key. A list default carries its own ids.

Rules for every field

  • key is the exact prop name the component receives. Keys are unique within a component (and across all settings groups).
  • blocks can never be a field key: the renderer gives every component its blocks under that prop.
  • A list item field cannot be keyed id (the item's own identity), cannot be a list or colorSchemes, and cannot use contentKey.
  • Every default is checked at assembly with the same rules as a saved value. An out-of-range number or an option that is not in options is an invalid-field from validateThemeDefinition, never a silent bad default.
  • Labels and descriptions are a string, or a map that covers every language the theme declares in locales.

Shared field options

OptionApplies toWhat it does
keyevery fieldThe exact prop name the component receives
labelevery fieldEditor-facing name; a string or per-language map
descriptionevery fieldEditor help text
requiredevery fieldThe value must exist after content/default fallback
visibleIfevery fieldEditor-only condition on neighbouring fields
defaultmost fieldsCreation-time value; images have none
contentKeytext, textareaInherited copy from the theme's content pack
optionLabelsselectEditor words for each option
min, max, stepnumber, listsBounds and grid / list length
displayselect, numberdropdown, radio, segmented, input or slider (a slider needs min, max and step)
formatsrichtextAllowed marks and blocks
inlinerichtextOne-line mode
itemLabellistItem field an editor shows as its title

visibleIf is a small fixed JSON condition, not an expression:

TypeScriptCode
{ key: "picked", kind: "productList", label: "Picked products", visibleIf: { field: "source", equals: "picked" }, }

Conditions support { field, equals }, { field, in }, { all }, { any } and { not }, nested at most four levels. The renderer ignores the condition, and a hidden field keeps its value.

How a value resolves

For a field on an instance, the order is:

  1. the stored instance value — for a translatable value, the entry for the rendered language, then each language in fallbackLocales,
  2. content[contentKey] for text and textarea fields,
  3. the field's default (a translated default in the same language order),
  4. omitted — the component's own prop default applies.

Translatable kinds are text, textarea, url and richtext: their stored value may be one string (or document) for every language, or a map from language tags to values. An image's alt is translatable too. Every other kind stores one value for every language.

A missing translation is not an error. A malformed map is invalid-prop; an unknown key is unknown-prop; a required field with no value is missing-required-prop.

A writer can check one value before saving:

TypeScriptCode
import { checkFieldValue } from "@wocart/theme"; const result = checkFieldValue(field, value); if (!result.ok) console.error(result.message);

The writer check is stricter than the reader: an image reference or list item with an undeclared key is refused here, while the reader ignores it.

Typed props

Derive the component's props from its definition. Do not write them twice.

Code
--- import type { InferProps } from "@wocart/theme"; import type { bestSellers } from "./best-sellers.def"; const { products = [], limit } = Astro.props as InferProps<typeof bestSellers>; ---

InferProps:

  • makes a field with a default always present,
  • makes every other field optional,
  • narrows a select to its option literals,
  • turns image into ResolvedImage (with alt already in the rendered language),
  • turns list into { id } plus the item fields,
  • merges whatever load returns over the fields.

The renderer does the same merge at runtime: { ...resolved.props, ...loaded }. If a definition's component reads a prop from load, keep that name out of fields.

Images and lists in practice

An image field has no default because the theme cannot know a store's media. The component renders its own fallback when no image was chosen.

Code
--- import { pictureOf } from "@wocart/storefront"; import type { InferProps } from "@wocart/theme"; import type { banner } from "./banner.def"; const { image } = Astro.props as InferProps<typeof banner>; const picture = pictureOf(image, Astro.locals.store.config.media); --- {picture && <img src={picture.src} alt={picture.alt} width={picture.width} height={picture.height} />}

An image value also carries optional width, height and focal (a point from 0 to 1 on each axis). See Images for the store's media delivery, resizing and the Astro image service.

A list receives [{ id, ...fields }]. The reader drops a broken or repeated item and keeps the rest, up to max.