Pages and rendering
A surface is a place where a document can be used. home fills the home
page, product fills every product page, and area:header fills the header on
every page. You do not need all of them at once; most themes start with home
and product.
Each surface has a document: a JSON list of the sections that surface shows and the values those sections hold. The theme ships a default document; a store can save its own. The SDK chooses which one renders and repairs the parts that no longer match the theme.
Surfaces
Code
| Field | Meaning |
|---|---|
key | The surface key stored in the document |
label | Editor-facing name |
pageType | The platform page type the surface renders |
document | The theme's shipped default |
required | Top-level sections that must exist |
max | At most this many top-level sections (1–50; absent: 50) |
hideableAreas | The theme's areas a page of this surface may leave out (below) |
A surface key is lowercase and starts with a letter or digit. It can never
start with lp: or area:: those prefixes name a landing page's own
document and an area's.
Every platform page type
pageType is one of the platform's page types, the same names, with the same
URLs, as STOREFRONT_PAGE_PATHS in @wocart/storefront. A theme composes
each of them as a surface with one required main section: a page type the
theme declares no surface for cannot be composed in Studio, and its preview is
a 404 (previewTarget answers null). The SDK accepts a theme with fewer
surfaces; the theme starter declares all twelve, and
its contract test fails on a missing one.
pageType | Path | What the route reads and passes to its sections | The starter's surface and main section |
|---|---|---|---|
home | / | pageData.get("home") | home, no required section |
catalog | /products | ?page=, ?sort= (from PRODUCT_SORTS) | catalog, listing-main |
category | /category/:slug | The category by slug; ?page=, ?sort=; 404 when unknown | category, listing-main |
collection | /collections/:handle | collections.get(handle); ?page=, ?sort=; 404 when unknown | collection, listing-main |
product | /products/:handle | products.get(handle); 404 when unknown | product, product-main (its info slot requires one product-order block) |
page | /pages/:slug | pages.get(slug); 404 when unknown | page, page-main |
landing | /lp/:slug | pages.landing(slug) with its own document; 404 when unknown | landing, landing-order |
checkout | /checkout | The browser basket; only when config.cartEnabled (the starter redirects to the home page otherwise) | checkout, checkout-main |
thankYou | /thank-you | The order the query names (the theme's choice of keys) | thank-you, thank-you-main |
trackOrder | /track-order | lookupOrder from a posted form; ?order=<number> fills the number in | track-order, track-order-main |
search | /search | products.search(q); ?q=<words>, ?page=, ?sort= | search, search-main |
contact | /contact | config.contact and whatsAppContact | contact, contact-main |
The section names are the starter's choice; one required main section per page
type is the rule. On the product and landing surfaces the required section
must always render an order form, or validateThemeDefinition reports
order-form-not-guaranteed (Composition).
What the contact page may say
The contact page shows only what the store has:
- its phone, email and address from
config.contact, each shown once the merchant set it (each isnulluntil then); - its WhatsApp from
whatsAppContact, when the store has the App; - opening hours and a note as the merchant's own section fields, per language, shown only once written.
The theme's words label these (Phone, Opening hours) and never fill one
in. A default such as "Open every day 9:00–18:00" in a content pack is a store
fact written as a theme word: it shows on every store that never set its
hours.
Templates
A page type with a resource — product, category, collection, page,
landing — makes the surface a template. One document renders every product,
category or page of that type. The route reads the resource first and passes
the route parameters:
Code
Sections read the resource through context.params and a read key that
includes it, so the page and its sections share one read.
Landing pages
A landing page is composed per page. The theme declares one landing
surface (the key must be landing, its pageType landing): its rules, its
sections and its default document apply to every landing page. Each page
stores its own document under landingSurface(page.id), lp:<id>, and the
storefront receives it with the page itself, as LandingPage.document:
Code
resolveSurfaceDocument, the document operations, isInsertableOn and
previewTarget read an lp: surface by the surface it follows:
templateSurface("lp:<id>") is "landing" (LANDING_TEMPLATE). No other
surface may use pageType: "landing".
Alternates
A surface can be an alternate of another: product.compact is a second
product layout. Its base (product) must be declared and render the same page
type. capabilities.surfaces: ["product"] covers the alternate too.
Areas
An area is a part every page shares — the header, the footer. It is composed of sections like a page and edited once for the whole store.
Code
An area's document is the surface area:<key>; areaSurface("header") builds
that string. A component stays in the header by declaring
capabilities: { surfaces: ["area:header"] }. An area key is lowercase
kebab-case and unique among the theme's areas.
A page without its header or footer
A surface may let its pages leave areas out. A landing page that sends an ad's visitor straight to its order form is the case:
Code
The page's document lists the areas it hides in hiddenAreas. Read it with
isAreaShown(theme, page, key), never from the array: only an area the surface
lists in hideableAreas can be hidden. Studio offers the choice
(hideableAreas(theme, surface)) and writes it with setAreaHidden.
Code
While Studio edits an area over a page, show that area even if the page hides
it, so the merchant can see what they edit; the starter's preview route does
(target.area === "area:header" || isAreaShown(…) in
src/pages/wocart-preview.astro).
The document
A page document is JSON:
Code
version is 1. surface is the surface key, area:<key> or lp:<id>.
sections is the top level. An instance has id, type, optional props,
children, slots, fixed and hidden. hiddenAreas lists the areas the
page leaves out.
A document may also name its provenance:
Code
A document that names another theme is reported (theme-mismatch) and the
shipped default renders instead.
Unknown top-level keys are carried through verbatim. Unknown keys inside an instance are stripped. The envelope is forward-compatible; the instance is not.
What renders
resolveSurfaceDocument(theme, surface, saved, scope) applies the policy:
| Saved state | Renders |
|---|---|
| No saved document | The theme's shipped default |
Envelope invalid (page: null) | The shipped default |
| Surface or theme does not match | The shipped default |
| No saved section maps to a catalog type | The shipped default |
| Partial overlap | What maps; unknown types render nothing |
| Empty saved document | Empty — the merchant emptied it |
It returns the document, its source and the reason for a fallback:
Code
The result is completed with required and fixed blocks (completePage), so
what you render is what the contract requires.
Render a page
One InstanceView per section, one read cache per page render.
Astro.locals.store below is the theme's own request store, built by its
middleware — the quick start shows the
whole thing:
Code
InstanceView is the reference renderer. For one instance it:
- skips a hidden instance,
- finds the definition by
type, - resolves defaults, translations, fields and composition,
- runs the optional
loadhook, - renders the theme's own component with
propsand ablockshandle.
An unknown type renders nothing; the rest of the page survives. A load that
throws is reported as load-failed and the base props still render.
One catalog per route
A route imports its catalog, and with it every component the catalog names.
A theme with many sections can give each route a catalog of its own surface
and the components that surface uses, so the thank-you page does not load the
home page's sections. Each family is a defineTheme with the same key and
version, one surface and its components, and a contract test checks that
the families together are exactly the full catalog the manifest describes.
That is a performance choice, not a rule; the starter renders every route
with its one catalog (src/theme/theme.ts).
Issues
A page has two report channels:
- Page-data issues come from parsing and resolution:
unknown-type,unknown-prop,invalid-prop,missing-required-prop,unknown-slot,disallowed-child,duplicate-id,limit-exceeded,theme-mismatchand others. - Renderer issues add
missing-component,load-failedandmultiple-slots-unsupported.
Pass onIssue to collect them in tests or logging:
Code
The default reporter is console.warn with a stable prefix. An issue is a
degradation with a report, not a thrown error.
One named slot per instance
The reference renderer fills at most one named slot per instance. Astro's
compiler can lift only the first dynamic slot attribute into the slots object.
A second named slot with content is reported as multiple-slots-unsupported
and is not rendered. A component can declare several slots; it can also
transfer Astro's own slots inside itself when it needs more than one filled.
Preview is a separate render mode
A preview in Wocart Studio uses the same InstanceView with preview set.
The markers it adds are read by the preview bridge, not by shoppers. See
Preview.