Wocart

Production and upgrades

A theme that renders is not a theme you can ship. This page is the list a theme has to survive before a merchant's store depends on it: accessibility, right-to-left, responsive behavior, the states nobody designs, performance, security, deployment, and the upgrade path between SDK versions.

Nothing here changes the contract; it is how the contract is used well.

Before you ship

AreaProve it
ContractvalidateThemeDefinition(theme) is empty; the manifest round-trips; every shipped document validates in every language
RenderEvery surface renders with no unexpected onIssue
LanguagesArabic renders RTL, French and English LTR, and no language shows another's copy
StatesEmpty, missing, invalid and failed data each render something sensible
ImagesEvery image has an alt (or is deliberately decorative) and a width/height or an aspect ratio
MoneyTotals come from the server; the browser never adds a fee
SecretsNo store key reaches a browser bundle
Buildnpx astro check, npm test and npm run build pass

Accessibility

The SDK gives you the pieces; the markup is yours.

  • Alt text is a field, per language. An image field's value is a MediaReference whose alt resolves to the rendered language; pictureOf(image, config.media).alt is it, "" when the merchant wrote none. A decorative image has an empty alt, never a filename.
  • Rich text renders escaped HTML. richTextToHtml escapes every text node and attribute and drops unsafe links, so set:html is safe — but the heading levels it can emit are h2–h4. A page must own exactly one h1, usually the section's title field.
  • Color contrast is checkable. contrastRatio(a, b) gives the WCAG ratio; 4.5 is the floor for body text, 3 for large text. Warn a merchant in the editor rather than silently accepting an unreadable scheme.
  • Focus and keyboard. Every interactive element is a real <a>, <button> or form control. The SDK adds no click handlers; a clickable <div> is your bug, and it does not work for a keyboard shopper.
  • Forms. The checkout form's fields and their validation come from the store's settings; label each input with the merchant's label text, not a placeholder alone.
  • Motion. A marquee, carousel or countdown should respect prefers-reduced-motion. The SDK has no motion primitives and adds none.

Right-to-left

Arabic is a first-class language here, not a stylesheet afterthought.

  • Put dir={isRTL ? "rtl" : "ltr"} and lang={locale} on <html>, from the render context. StoreConfig.locales[].direction is the store's own answer for each language.
  • The SDK's style classes are written on CSS logical properties (padding-inline, margin-block-start), so a style field mirrors with no extra CSS. Write your own CSS the same way: margin-inline-start, not margin-left.
  • Mirror only what carries direction: chevrons, "next" arrows, progress. Do not mirror a logo, a phone number, a clock or a product photo.
  • Prices, phone numbers and tracking numbers stay LTR inside RTL text: give a phone input dir="ltr". The order action normalizes an Algerian number (+213…, 00213…, spaces) to its local form before the server checks it.
  • Test with the longest Arabic string, not the shortest: Arabic copy often runs longer than its English source, and a fixed-width button clips it.

Responsive

  • Breakpoints are the SDK's: base everywhere, md from 768 px, lg from 1024 px. A style value is phone-first — a wider breakpoint inherits the narrower one, so never repeat a base value in md.
  • Steps are pixels the theme chose. s4 is 16 px only because the theme mapped it; a component that hardcodes 16px breaks the moment a merchant picks s8.
  • Images: pick the photo with productImageSource or pictureOf, and size it with mediaImageSrcset and sizes, so a phone does not download a hero image sized for a desktop. mediaImageService makes Astro's <Image> do the same through the store's media delivery.
  • Content decides height. A section that assumes two lines of title breaks in Arabic or with a long product name. Let text wrap; set min-height, not height.

Every state a section renders

StateWhat the shopper should see
Data is empty ([], null, {})An empty state, not a broken layout: no blank grid, no "undefined"
A read failedThe same empty state; the operator sees the cause through the client's onError
load threwThe base props still render; the renderer reports load-failed
The type is unknownNothing renders for that instance; the rest of the page survives (unknown-type)
A prop is invalidThe field's default applies and invalid-prop is reported — never a crash
A required field has no valuemissing-required-prop; the component must not print an empty heading
Nothing configured yetIn a preview only: a placeholder via isPreview(blocks), so a merchant can select the section
The document belongs to another themeThe shipped default renders; the merchant never sees an empty page
An editor's draft is emptyAn empty page is a real answer — the merchant emptied it

Degradation is silent to shoppers by design

A storefront must not throw because the catalogue is down. If a page 500s on a failed read, that read was awaited without a fallback — products.list() already returns [].

Performance

  • One read cache per page. createReadCache() shares a read across sections (and shares its rejection). One cache per section multiplies the same request by the number of sections; one per process leaks between visitors.
  • One client per request. The storefront client keeps small opt-in caches (config, categories, communes) per instance; sharing a client across requests shares those answers across shoppers, language included.
  • Keep Zod out of the browser. Nothing in a shopper's page should import the theme SDK's validators or a schema module. Validation runs where data enters: at a route, or in the build.
  • Prerender what does not depend on the request. The manifest and the styles endpoint are build artifacts (prerender = true).
  • Images carry the weight. Resize with the store's media delivery (config.media), send srcset, and give every image intrinsic dimensions so the layout does not shift.
  • A composed page is a budget. Fifty sections render; that does not mean they should. Every section is at least one read and one component tree.

Security

  • The API key lives in a server module and a server environment variable. A PUBLIC_* key is a published key.
  • url fields accept site paths, fragments, queries, http(s)://, mailto: and tel: only; javascript:, data: and protocol-relative targets are rejected at assembly and reported on read. Render them as an href, never as raw HTML.
  • Rich text is a closed format: no HTML node exists to store, and richTextToHtml escapes what it emits. Do not set:html a raw text or textarea field.
  • An image value is a media key; the URL is derived at render. A saved document cannot point an <img> at another host.
  • The theme reads; the platform writes. There is no endpoint in a theme that saves a document, a setting or merchant text — that is Wocart Studio, and a theme that adds one has invented an authorization boundary it cannot enforce.
  • Preview tokens are signed by the editor and verified with the deployment's preview key; a theme without a key answers 404 on the preview route. Never cache a preview response.
  • Inline style is limited to the color-scheme sheet ThemeStyles.astro writes; on a page with a Content-Security-Policy it adds that sheet's hash, so style-src needs no 'unsafe-inline'.

Deploying

  1. Build for both hosts. A Wocart theme runs on Cloudflare Workers and on Vercel from the same code: @astrojs/cloudflare by default and @astrojs/vercel when DEPLOY_TARGET=vercel, with session: false (Quick start, step 2). Wocart Cloud builds a release twice, npm run build and DEPLOY_TARGET=vercel npm run build, and refuses it if either fails. The Cloudflare build must write dist/server/wrangler.json with a compatibility date, and dist/client/wocart-theme.json. The Vercel output in .vercel/output must carry no symlinks and no native binaries built for another system, which is why the image service is the passthrough or the media image service, never sharp.
  2. Read the runtime contract. Wocart Cloud sets the same variables on every storefront it deploys, on Cloudflare and on Vercel: COD_SERVER_URL, STORE_API_KEY and, when preview is configured, WOCART_PREVIEW_KEY. A theme reads exactly these names and needs no other. Declare them optional in astro.config.mjs: the build has no store, and it loads the middleware to prerender the manifest. Hosting yourself, put them in the host's secret store. COD_SERVER_URL must be a public origin the deployment can reach: a loopback value that works locally renders the unavailable store in production.
  3. Build once, serve every store. A theme build is not per store: the store is decided per request by the key and the config, images included (the config carries its media delivery). Never read a store value at build.
  4. Know which pages are static. wocart-theme.json, wocart-styles.css and any hand-written page are prerendered; a surface page is rendered per request. A prerendered page cannot read Astro.locals — a build has no store.
  5. Cache deliberately. If you cache a rendered page, tag it and drop the tag on REVALIDATE_PATH after a publish; a preview response is never cached.
  6. Verify after deploy: /wocart-theme.json parses and validates, the stylesheet is served with text/css, and a product page renders in each published language.

Versions and upgrades

Both packages follow semantic versioning: a patch fixes behavior, a minor adds backwards-compatible features, a major may remove or change an export. An export is marked @deprecated for at least one minor release before a major removes it.

FactValue at the time of writingWhy it matters
@wocart/theme4.1.0THEME_SDK_VERSION in every manifest
@wocart/theme peers@wocart/storefront ^3.0.0, astro ^7.0.0 (optional)npm refuses an install outside the range
@wocart/storefront3.1.0Reads platform v7; its root entry is browser-safe, /server and /astro are not
@wocart/storefront peerzod ^4.4.3Its schemas are part of its public API
Platformv7A theme on @wocart/storefront 2.x reads a v7 store as unreadable: a store updates its platform and its theme together
Manifest contractTHEME_MANIFEST_CONTRACT_VERSION = 1An editor reads manifests of this version
Preview protocolPREVIEW_PROTOCOL_VERSION = 1The editor and the bridge must agree

Pin exact versions in a theme (npm install --save-exact). A theme is rendered by its own deployment, so a floating range only means two builds of the same commit can differ.

A theme's own versions

A theme release is breaking when a merchant's saved data stops meaning what it meant. diffThemeManifests(previous, next) lists every such change between two manifests:

BreakingAdditive
The theme key changesA surface, area, component, slot, fixed block, field, option or content key is added
A language is dropped from localesA language is added
A surface, area or component is removed, or a surface renders another page typeA component becomes insertable again, or allowed on more surfaces
A slot or fixed block is removed, or a fixed block changes typeA section becomes required: a document that lacks it gains one
A field is removed or changes kind; a select option or a rich-text format is removed
A field becomes required, or a required field is added, with no default or contentKey
A surface's or area's max is lowered
A content key is removed in any language
A style size the theme gave a step or width is removed
A component stops being insertable, or is allowed on fewer surfaces

A breaking change needs a new major version of the theme. Wocart Cloud refuses to release a theme whose manifest is breaking against the previous release under the same major. Prefer additive changes: add the new field or component and keep the old one rendering.

What a merchant saves for a theme is kept under its key: page documents, settings and edits to the theme's words. Store data (catalog, orders, menus, pages, languages) is shared by every theme. A store that moves to another theme sees that theme's own defaults over the same store data, and a store that moves back finds the first theme as it was left. Changing a theme's key leaves every merchant's saved work for it behind.

The upgrade checklist:

  1. Read both packages' CHANGELOG.md for every version you cross, not only the latest.
  2. Bump the pins, then run npx astro check, npm test and npm run build.
  3. Run diffThemeManifests(previous, next) with the manifests of both releases and read every entry it calls breaking.
  4. Run validateThemeDefinition again: a newer SDK may check more than the older one did.
  5. Re-run the preview and a shopper render, in Arabic as well as the primary.

What changed if you are coming from @wocart/theme 2.x and @wocart/storefront 1.x:

  • Images (storefront 2.0, theme 3.0): every image is a MediaImage ({ key, url, width, height }) and the config carries media. The image helpers take that delivery; the media-domain presets (getCardImage, createImageHelpers, optimizeImage, generateSrcSet…) are gone, and mediaImageService takes a function answering the delivery.
  • Landing pages (theme 3.1): one landing surface composes every landing page; each page's own document is lp:<id>. A landing surface must guarantee an order form like a product surface. Surfaces may let a page hide areas (hideableAreas).
  • The v7 payloads (storefront 3.0): the config no longer carries colours, font, announcement, themeId, cartShippingMode or legalContact (contact replaces it); a fallback config is unavailableStoreConfig; the store's WhatsApp is read only through whatsAppContact; productTags, STOREFRONT_ROUTES and resolveActionRedirectDecision are gone.
  • Search and contact (theme 4.0): two new page types, so declare a surface for each; every document operation takes the theme first.

Common mistakes

MistakeWhat it costs
Renaming defineTheme({ key })Every merchant's text edits and page documents for the theme are orphaned
Renaming a component typeSaved instances of it stop resolving (unknown-type)
Narrowing slots[].allowChildren that no longer fit are dropped from saved pages (disallowed-child)
Changing a shipped document's idA merchant's saved edit stays attached to the old id
A field key that collides with a load keyThe loaded value wins and the field silently does nothing
A contentKey typoCaught by validateThemeDefinition (unknown-content-key) when the theme declares content, or when you pass contentKeys; otherwise the field silently falls back to its default
Hardcoding a color, spacing step or font in CSSThe merchant's style / colorScheme choice has no effect
Storing a URL in an image fieldThe image never resolves; a media key is what the field holds
Reading Astro.locals.store in a prerendered pageA build error, or an empty page
One read cache per sectionThe same product is fetched once per section
A theme route that saves a documentThe theme has no authority to write; Wocart Studio owns writes
Treating a theme's own choices as contractSection names, the content layout and the design system are each theme's decisions, the starter's included; the contract is what the SDKs declare and check