Quick start
This page builds the smallest complete editable theme: an Astro storefront that connects to a store, renders its home page from a page document, lets a merchant edit the hero's text in Wocart Studio, and publishes its manifest. Every file is shown in full. Copy them in order and the theme builds.
The faster way
The theme starter creates a complete theme in one command: every page type, the order form and the basket, colors, preview, revalidation and tests, on both hosts. Build from scratch when you want to understand every piece, or need a structure of your own. The two SDKs are the same either way.
What you need
Node 22.12 or newer, and a store to read from: the URL of its API
(COD_SERVER_URL) and a store API key (STORE_API_KEY). A self-hosted Wocart
gives you both; locally the API runs on http://localhost:8787.
1. Create the project
Code
A Wocart theme runs on two hosts from the same code: Cloudflare Workers
and Vercel. Wocart Cloud deploys a store's theme to either, so every theme
installs both adapters and builds with each (step 2). wrangler runs the
Cloudflare build locally. zod is a required peer of @wocart/storefront.
Pin the two Wocart packages exactly: a theme is rendered by its own
deployment, and a floating range lets two builds of the same commit differ.
Then set the scripts and one override in package.json, and run
npm install again:
Code
| Script | What it does |
|---|---|
build | The Cloudflare build: the Worker in dist/server, the files in dist/client |
build:vercel | The Vercel build: the Build Output in .vercel/output |
preview | The Cloudflare build, served locally by wrangler dev; npm run preview -- --port 4322 chooses the port |
The override keeps @cloudflare/vite-plugin at 1.53.1. Newer releases stop
Vitest at startup ("There is already a server associated with the config")
when the Cloudflare adapter loads through getViteConfig, as the tests in
step 15 do. The theme starter carries the same pin.
The two packages move together. @wocart/theme 4.1.0 declares
@wocart/storefront ^3.0.0 as its peer, and @wocart/storefront 3.1.0
reads the payloads of platform v7.0.0. A theme on @wocart/storefront 2.x
cannot read a v7 store's config and renders every page as an unreadable
store.
2. Configure Astro
astro.config.mjs
output: "server": pages render per request, because the store, its language and its saved documents are decided per request.- One switch, two hosts.
DEPLOY_TARGET=vercelbuilds with the Vercel adapter; without it the theme builds for Cloudflare. Each adapter is imported only by its own build. Nothing else in the theme knows its host. session: false: a theme keeps no server sessions. Left on, the Cloudflare adapter makes the Worker require aSESSIONKV namespace that a storefront deployment does not have.- Images pass through. Astro's default image service is
sharp, which puts native binaries for the build machine into the Vercel output, and an output carrying them cannot be deployed. When the theme resizes store images, replace the passthrough service with the media image service (Images); it serves both hosts. - Every value is a server secret. None is
PUBLIC_*; the API key must never reach a browser. - They are
optionalon purpose. One theme build serves every store, so the build has no store values. The build also prerenders the manifest, and that loads the middleware and the module that reads these variables. A required secret would fail every build. At request time a missing value still fails loudly:createStoreClientthrowsMISSING_CONFIGURATIONrather than rendering an empty store.
Images need no variable: the store's config says where its images are served
from (config.media).
The Cloudflare build reads its runtime from wrangler.jsonc:
wrangler.jsonc
The build copies it to dist/server/wrangler.json, with the compatibility
date and flags the Worker runs under. Keep the date set: a deployment reads
it from there.
astro dev runs the theme in Cloudflare's local runtime, which reads its
values from .dev.vars:
.dev.vars
Never commit it. The Astro template ignores .env but not these, so add them
to .gitignore:
Code
3. The theme's identity
src/theme/key.ts
The key is lowercase kebab-case, at most 64 characters, and permanent: the platform stores every merchant edit for this theme under it. It lives in its own small module so server code can import it without loading every component.
4. One place that creates the store client
src/lib/store.ts
This is the theme's one seam with the platform, and it is server-only.
| Option | Why it is there |
|---|---|
theme | The store's text edits, settings and page documents for this theme arrive only when the client names it |
locale | Set for a language other than the store's primary, so the server answers in that language |
onError | Reads never throw: a failed read returns null or []. This is the only place the theme can see why |
A new client per call keeps one request's reads and caches out of another's.
5. The store that cannot be read
src/lib/fallback.ts
When the store's config cannot be read (the API is down, the key is wrong),
the theme renders this config instead of crashing. Never write a fallback
config by hand: unavailableStoreConfig always has the config's current
shape, and it decides once what an unreadable store offers.
- Nothing to buy. The cart, the checkout form's address and delivery options, the code by SMS, Turnstile, reviews and the WhatsApp button are all off, so the page can never look like a working checkout.
- An empty
id.config.id === ""is how a page tells this config apart from a real store. - One language.
locale(default"ar") is its only language. Its name and direction come from the runtime'sIntldata, because the platform's list of languages lives on the server. - Images from
serverUrl. Its media delivery isdirect, from<serverUrl>/media.
6. The theme's own text
src/theme/content.ts
A content pack is one flat map of keys to text per language. A field can
take its starting text from a pack key, and a merchant's edit for that key, in
that language, replaces it. Every pack must have the same keys. The SDK's
resolveThemeContent merges them (next step).
A pack holds the theme's words, never a fact about a store. "Delivered to
all 58 wilayas", opening hours, a phone number or a return period is true of
one store and false of the next. Facts come from the store's data
(config.contact, its pages, its shipping rates) or from a field the
merchant writes.
7. The request's store
src/lib/context.ts
StoreContext is your type: everything a page needs about the request,
built once from the store the SDK resolves. The rules that decide that store
are the SDK's:
- The store's primary language has no prefix (
/products/robe); another published language does (/fr/products/robe). The primary's own prefix redirects permanently to the unprefixed URL; a language the store stopped publishing redirects temporarily to the primary page. - A landing page (
/lp/…) exists in the primary language only:/fr/lp/…redirects permanently to/lp/…, its query kept, andlocalizeHrefleaves landing links unprefixed. - An endpoint or server island (
/api/…,/_server-islands/…) takes the language of the page that asked for it, from its same-originReferer, and is never redirected. resolveThemeContentgives your pack for the language (else the default's) with the merchant's edits for this language only, and only text edits of keys your pack has. Merging another language's edits would show French text to an Arabic shopper.href("/products")gives/fr/productson a French page and/productson the primary language's. Use it for every internal link.
8. The middleware
src/middleware.ts
/fr/products/robe renders the route src/pages/products/[handle].astro:
the SDK rewrites to /products/robe, so you never duplicate routes per
language. A prerendered page (the manifest) has no request store, so it is
skipped. A page's notFound() rewrites to /404 and the middleware keeps
the store it already has, so the 404 stays in the shopper's language. The
same call opens the editor's preview when WOCART_PREVIEW_KEY is set, and
runs a form posted to the page before it renders. Pass the key as a
function: a secret can be set after the module loads.
Tell TypeScript what the middleware puts on locals:
src/env.d.ts
9. Your first editable component
The component is ordinary Astro. Its props come from its definition.
src/components/sections/Hero.astro
src/theme/sections/hero.ts
| Property | What it does here |
|---|---|
type | The component's identity in saved documents. Never rename it once stores use it |
component | The real .astro component the renderer draws |
label | What Wocart Studio shows, in each of the theme's languages |
placement: "section" | It goes at the top level of a page, never inside another component |
contentKey | The field starts with the pack's text in the page's language |
required: true | A value must exist after the pack and the default; the pack supplies it |
default | The value until a merchant chooses another |
InferProps makes align the literal union "start" | "center" and always
present (it has a default), and title and subtitle optional strings.
10. The home page's default document
src/theme/documents/home.json
This is what the home page renders until the store saves its own. The id is
unique in the document and stable: a merchant's edit is stored against it. No
props, so every field takes its pack text or default.
11. The catalog
src/theme/theme.ts
- Each key in
componentsequals its definition'stype. Always. localesdeclares the theme's languages: every translated label must cover all three, and every pack must exist.contentdeclares the packs, so the SDK checks everycontentKeyagainst them.- The
as PageDatacast is only because an imported JSON file is typed loosely; the SDK checks the document for real when the theme is validated.
12. Render a surface
One component renders any surface: it picks the document, then renders each section through the SDK's renderer.
src/components/surface/Sections.astro
resolveSurfaceDocument renders the store's saved document when this theme
can, and the theme's default when nothing was saved, the saved value is not a
document, or it belongs to another theme. It also adds any required or fixed
block the document lacks. createReadCache() is one cache per page render:
sections that read the same resource share one request.
13. The layout and the page
src/layouts/StoreLayout.astro
src/pages/index.astro
src/pages/404.astro
pageData.get("home") returns the store's saved document for this theme, or
null when there is none or the read failed. Both cases are handled by
resolveSurfaceDocument.
14. Publish the manifest
src/pages/wocart-theme.json.ts
The build writes /wocart-theme.json: the JSON description of everything your
theme makes editable, which Wocart Studio reads. The manifest is validated when
this module loads, so an invalid theme fails the build instead of shipping.
15. Prove it
vitest.config.ts
tests/unit/contract.test.ts
Run the checks every change must pass:
Code
Both builds must pass: a theme that builds for one host only cannot be deployed to the other.
Start the theme with npm run dev and open http://localhost:4321. You see
the store's name in the tab and the hero in the store's primary language;
/fr shows it in French when the store publishes French.
What you have
| Piece | Where | Its job |
|---|---|---|
| The platform seam | src/lib/store.ts | The only module that creates a store client |
| The request store | src/lib/context.ts, src/middleware.ts | Language, redirects, the client and the text for each request |
| A definition | src/theme/sections/hero.ts, rendered by src/components/sections/Hero.astro | What a merchant may change on the hero |
| A default document | src/theme/documents/home.json | The home page before the store saves one |
| The catalog | src/theme/theme.ts | Everything editable, under one theme key |
| The renderer | src/components/surface/Sections.astro | Saved or default document → your components |
| The manifest | /wocart-theme.json | What Wocart Studio reads |
| The checks | tests/unit/contract.test.ts | Proof the contract holds |
From here, a complete theme adds a header every page shares, every page type with its main section, a product page that takes cash-on-delivery orders, color schemes and spacing controls, Studio preview, publish revalidation and render tests. The theme starter has all of them; The starter, file by file shows where each one lives, and the pages of this guide explain each piece.