The starter, file by file
This page walks through a project created with
npm create wocart-theme (Quick start). The project's
own README.md and AGENTS.md say the same things, next to the code.
The project map
Code
How a request becomes a page
- The middleware (
src/middleware.ts) is one call to the SDK'screateStorefrontMiddleware, givensrc/lib/store.ts'screateStoreas the one client factory,unavailableStoreConfigas the fallback, the preview route and key, and the context builder. The SDK decides which store, language and redirect the request means (a landing page has no/fr/…address), and a page'snotFound()keeps the shopper's language. The context (src/lib/context.ts) is built once and put onAstro.locals.store: the client, the store's config, the language and direction, the theme's words with the merchant's edits (resolveThemeContent), the theme settings, and anhref()that keeps the shopper's language. A form posted to the page runs its action before the page renders. - The route (
src/pages/…) reads what its address names (a product, a collection, a landing page) and answersnotFound(Astro)when it is missing. It reads the store's saved document for its surface (client.pageData.get("product")) and renders the page withsrc/components/surface/Sections.astro. Sections.astropicks the document that wins: the store's saved one, or the theme's default fromsrc/theme/documents/(resolveSurfaceDocument). It renders each section through the SDK'sInstanceView, with one read cache for the page.- A section's definition (
src/theme/sections/*.ts) reads the data it needs inload, throughreads.get(key, …), so two sections that need the same product read it once. - A section's component (
src/components/…) renders the resolved field values and the loaded data insidesrc/components/ui/Section.astro, which applies the merchant's color scheme and spacing. - The layout (
src/layouts/StoreLayout.astro) adds the<head>, the header and footer areas (src/components/surface/Area.astro), the basket drawer, the WhatsApp widget and the data the browser scripts read.
The browser side
Everything the browser runs is in src/scripts/. Each script binds through
bindEach or onEachPage from @wocart/theme/lifecycle, so it works again
when Studio's preview swaps a section.
src/scripts/order-form.tsbinds every order form and wires its parts insrc/scripts/checkout/: delivery (wilaya, commune, delivery type), quote (createQuoter, the store's price for the order), coupon, otp (the phone check), submit and tracking (unfinished checkouts and the Meta Pixel).src/scripts/cart.tskeeps the basket in the shopper's browser and has each line re-priced by the store.src/scripts/reviews.tsruns the review form (createReviewSession).
The browser never calls the store's API with the key. It calls the theme's
own routes under src/pages/api/, each a one-line file that mounts one of
the SDK's same-origin routes (createStorefrontRoutes in
src/theme/api-routes.ts). Their paths are the SDK's
STOREFRONT_API_PATHS (storefrontApiPath fills a parameter), and a test
checks that every path has its file, no file is unlisted, and no other file
writes an /api/ path.
Orders are placed with Astro actions (src/actions/index.ts): placeOrder
runs the SDK's executePlaceOrder on placeOrderInput, and orderStatus
runs lookupOrder. The browser never decides a price, a fee, a discount or
whether an order is valid. The store does.
Studio's routes
| Route | File | What it does |
|---|---|---|
/wocart-theme.json | src/pages/wocart-theme.json.ts | The manifest, built once at build time (themeManifestEndpoint) |
/wocart-styles.css | src/pages/wocart-styles.css.ts | The style classes for the theme's spacing and widths (themeStylesEndpoint) |
/wocart-preview | src/pages/wocart-preview.astro | Renders an unsaved draft for Studio, never cached (previewTarget) |
/wocart-revalidate | src/pages/wocart-revalidate.ts | Drops every cached page after a publish |
The preview and revalidation routes check a token signed by the platform
against WOCART_PREVIEW_KEY, and answer 404 without the key. The middleware
opens the preview; src/pages/wocart-revalidate.ts is the SDK's
revalidateEndpoint, which drops every page cached under
PAGES_CACHE_TAG.
Where each change goes
| You want to… | Change |
|---|---|
| Restyle the store | src/styles/tokens.css (fonts, type, corners, shadows), the starter's colors in src/theme/palette.ts, the default color schemes in src/theme/settings.ts, the fonts in public/fonts/ |
| Change a word a shopper reads | Its key in src/theme/content/, in ar, fr and en |
| Change what a page starts with | Its document in src/theme/documents/ |
| Let merchants change something new | A field in the section's definition, read by its component |
| Add a section | A definition in src/theme/sections/, registered in sections/index.ts; a component; its words; its Studio labels in labels.ts |
| Add a store-wide choice | A field in src/theme/settings.ts |
| Change what the browser does | src/scripts/ |
Make it your own walks through each one.
The rules, and the tests that keep them
The starter's tests run with npm test and need no network. Each rule below
is checked by a test you can run on its own with npx vitest run <file> -t "<name>".
| Rule | Test |
|---|---|
Import @wocart/* through published entry points only | guards.test.ts "published entry points only" |
Only src/lib/store.ts creates an SDK client | guards.test.ts "creates SDK clients in one place only" |
| A store that cannot be read renders the SDK's unavailable store | guards.test.ts "unavailable store" |
| Every platform page type is a surface, plus the header and footer areas | contract.test.ts "every page the platform defines" |
The product and landing pages always hold an order form (validateThemeDefinition checks both) | contract.test.ts "is a valid catalog", "guarantees an order form" |
| Every page renders through the SDK, for a shopper and in Studio's preview | contract.test.ts "every page, rendered through the SDK" |
| Every word is in ar, fr and en, with each language's plural forms, and every key is read | copy.test.ts |
| No word states a fact about a store: delivery reach, periods, percentages, opening hours, phone calls | copy.test.ts "states no fact about a store" |
Platform links come from storefrontPath and localizeHref | guards.test.ts "storefrontPath" |
Every WhatsApp link comes from storeWhatsApp() in src/lib/whatsapp.ts | guards.test.ts "is reached one way" |
Colors come from color schemes; every brand value lives in tokens.css | contract.test.ts "colour scheme"; guards.test.ts "keeps every brand value in tokens.css", "scheme marker" |
A photo a merchant picks is an image field, never a URL | contract.test.ts "never type an image URL" |
| Layout uses logical properties, so right to left needs no rule of its own | guards.test.ts "logical properties" |
No colour outside tokens.css and palette.ts: not in code, markup, a data URI or an SVG | guards.test.ts "holds no colour outside the tokens and the palette" |
Every section has a colour scheme and a style, and renders through Section.astro | guards.test.ts "gives every section a colour scheme and a style" |
| Browser scripts bind through the SDK's lifecycle | guards.test.ts "binds every behaviour through the SDK's lifecycle" |
| Browser code imports no Zod, no schema and no server-only module | guards.test.ts "the theme's browser code" |
Every order form posts to the placeOrder action, with its checkout key | guards.test.ts "gives every order form its checkout key" |
A missing resource answers notFound(Astro) | guards.test.ts "through the SDK's notFound" |
Every cached page carries the pages tag, written as PAGES_CACHE_TAG | studio.test.ts "caches every composed route", "reaches every cached page" |
| Every same-origin API path is the SDK's, mounted by exactly one file | guards.test.ts "mounts a route file at every API path" |
A 404 keeps the shopper's language; /fr/lp/… goes to /lp/… | middleware.test.ts |
| A landing page offers no other language | languages.test.ts |
| A merchant's text edit shows only in its language, for the theme's own words | copy.test.ts "the merchant's text edits" |
| Preview and revalidation accept only the platform's signed tokens | studio.test.ts |
| The theme builds for Cloudflare Workers and for Vercel | guards.test.ts "builds for Cloudflare Workers and for Vercel" |
When you add a rule of your own, add a test for it, and prove the test by breaking the code once and watching it fail.