
import { Callout } from "zudoku/ui/Callout";

# 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

| Area | Prove it |
| --- | --- |
| Contract | `validateThemeDefinition(theme)` is empty; the manifest round-trips; every shipped document validates in every language |
| Render | Every surface renders with no unexpected `onIssue` |
| Languages | Arabic renders RTL, French and English LTR, and no language shows another's copy |
| States | Empty, missing, invalid and failed data each render something sensible |
| Images | Every image has an `alt` (or is deliberately decorative) and a `width`/`height` or an aspect ratio |
| Money | Totals come from the server; the browser never adds a fee |
| Secrets | No store key reaches a browser bundle |
| Build | `npx 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

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

<Callout type="caution" title="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 `[]`.
</Callout>

## 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](/sdk/theme/build#2-configure-astro)). 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](/sdk/theme/images#astros-image-on-any-media-host),
   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.

| Fact | Value at the time of writing | Why it matters |
| --- | --- | --- |
| `@wocart/theme` | `4.1.0` | `THEME_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/storefront` | `3.1.0` | Reads platform v7; its root entry is browser-safe, `/server` and `/astro` are not |
| `@wocart/storefront` peer | `zod ^4.4.3` | Its schemas are part of its public API |
| Platform | v7 | A theme on `@wocart/storefront` 2.x reads a v7 store as unreadable: a store updates its platform and its theme together |
| Manifest contract | `THEME_MANIFEST_CONTRACT_VERSION` = `1` | An editor reads manifests of this version |
| Preview protocol | `PREVIEW_PROTOCOL_VERSION` = `1` | The 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:

| Breaking | Additive |
| --- | --- |
| The theme `key` changes | A surface, area, component, slot, fixed block, field, option or content key is added |
| A language is dropped from `locales` | A language is added |
| A surface, area or component is removed, or a surface renders another page type | A component becomes insertable again, or allowed on more surfaces |
| A slot or fixed block is removed, or a fixed block changes type | A 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

| Mistake | What it costs |
| --- | --- |
| Renaming `defineTheme({ key })` | Every merchant's text edits and page documents for the theme are orphaned |
| Renaming a component `type` | Saved instances of it stop resolving (`unknown-type`) |
| Narrowing `slots[].allow` | Children that no longer fit are dropped from saved pages (`disallowed-child`) |
| Changing a shipped document's `id` | A merchant's saved edit stays attached to the old id |
| A field key that collides with a `load` key | The loaded value wins and the field silently does nothing |
| A `contentKey` typo | Caught 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 CSS | The merchant's `style` / `colorScheme` choice has no effect |
| Storing a URL in an `image` field | The image never resolves; a media key is what the field holds |
| Reading `Astro.locals.store` in a prerendered page | A build error, or an empty page |
| One read cache per section | The same product is fetched once per section |
| A theme route that saves a document | The theme has no authority to write; Wocart Studio owns writes |
| Treating a theme's own choices as contract | Section 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 |
