Wocart

Pages and menus

Store pages, menus and landing pages are merchant content. The SDK reads them; the theme decides where they sit and how they look.

Store pages

pages.get(slug) returns a published legal or custom page:

TypeScriptCode
import { notFound } from "@wocart/storefront/astro"; const page = await store.pages.get(Astro.params.slug!); if (page === null) return notFound(Astro); page; // { // id, // kind: "terms" | "privacy" | "refund" | "shipping" | "custom", // slug, // locale, // title, // bodyHtml, // metaTitle, // metaDescription, // }

bodyHtml is already sanitised by the server. Render it with set:html and do not re-sanitise it into a different shape:

Code
<article> <h1>{page.title}</h1> <div set:html={page.bodyHtml} /> </article>

A draft, unpublished or unknown slug returns null. That is a real 404, not a page with no body.

Resolve legal pages by kind when the rule names one. A merchant may rename a slug, but the Terms page is still kind: "terms".

TypeScriptCode
const terms = config.pages.find((item) => item.kind === "terms");

pages.get is fetched fresh on every call. A merchant's edit reaches the next server render, subject to your route cache.

menus.get(handle) returns a menu tree:

TypeScriptCode
const menu = await store.menus.get("main-menu"); menu; // { // handle: "main-menu", // title: "Main menu", // items: [ // { // label: "Women", // href: "/category/women", // type: "category", // children: [], // }, // ], // }

Every item has:

FieldMeaning
labelIn the client's language when translated
hrefA store path without language prefix, or the merchant's absolute URL
typeThe destination: home, catalog, search, trackOrder, contact, category, collection, product, page, landing or url
childrenNested items; a menu has at most three levels and 100 items

The server turns every destination into its path, so a theme never builds a menu link itself: render href as it is. A url item is the merchant's own address: a path of this store, an http(s) address, a mailto: or tel: link, or a #/? fragment; the server refuses anything else. Items that lead nowhere public are already removed. Do not re-check visibility.

A new store's footer menu links order tracking and the contact page, so a theme that renders the store's menus reaches those pages without a link of its own.

Localize only store paths; localizeHref leaves external URLs alone:

Code
--- import { localizeHref } from "@wocart/storefront"; --- <a href={localizeHref(item.href, config.locale, routing)}>{item.label}</a>

An unknown menu or a failed read returns null. Render your own fallback navigation, not an empty header.

A menu is a tree, so render it recursively and preserve RTL/LTR with the page's dir. The SDK does not render it for you.

Landing pages

pages.landing(slug) returns a published landing page with an ordered image stack, optionally one linked product, and its own page document:

TypeScriptCode
import { notFound } from "@wocart/storefront/astro"; const landing = await store.pages.landing(Astro.params.slug!); if (landing === null) return notFound(Astro);

The shape:

TypeScriptCode
type LandingPage = { id: string; slug: string; name: string; status: "draft" | "published" | "archived"; imageGap: number; metaTitle: string | null; metaDescription: string | null; publishedAt: string | null; images: Array<{ id: string; key: string; url: string; altText: string | null; position: number; width: number | null; height: number | null; }>; product: Product | null; tracking: { pixelId: string | null; conversionEvent: "Lead" | "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered"; }; document: PageDocumentPayload | null; };

images is ordered by position; imageGap is the space between them in pixels. Width and height are captured at upload, so reserve the box and avoid a layout shift.

product is a full product, not a list card. The order form can use it unchanged. It is null when the product is no longer public; in that case show the landing content without an order form.

A draft, archived or unknown landing page returns null. Answer it with notFound(Astro): a real 404 that is never cached, so the page shows the moment the merchant publishes it. A redirect to the home page would answer an ad's link with a 200, a soft 404 for search engines and ad reviews.

The page's tracking is already resolved. Use that pixel and event, not the store's default pixel, when the landing page fires its browser event.

document is this landing page's own composition for the client's theme (its lp:<id> page document), or null when it has none or the client names no theme: the theme then renders its landing surface's default. Render it with resolveSurfaceDocument(theme, landingSurface(landing.id), landing.document, scope) from @wocart/theme; Pages and rendering covers landing pages.

pages.landing(slug, { preview: true }) also answers a draft and counts no visit. It is for an editor's preview only; a shopper's page never passes it.

The saved page document

An editable theme reads the store's composition document with pageData.get(surface):

TypeScriptCode
const document = await store.pageData.get("home");

The transport shape is deliberately loose:

TypeScriptCode
type PageDocumentPayload = { version: number; surface: string; sections: unknown[]; [key: string]: unknown; };

sections stays unknown[] so a newer document reaches your theme unchanged. The meaning of a section belongs to the theme schema. Do not cast it to your local section type; the Theme SDK reads it.

Render it with resolveSurfaceDocument from @wocart/theme, which picks the saved document or the theme's default, repairs and completes it, and reports what it changed:

TypeScriptCode
import { resolveSurfaceDocument } from "@wocart/theme"; const { page, source, issues } = resolveSurfaceDocument(theme, "home", document, scope);

See Pages and rendering.

With theme set on the client, the server returns that theme's document for the surface. Without it, the server returns the most recently saved document for the surface. A store with no saved document returns null; render your shipped default.

Read side only

Page documents are written by Wocart Studio, never by a storefront: the public API has no endpoint that writes one. Read pageData, render it, and keep your theme's editing contract in @wocart/theme.

A page-rendering checklist

  • A store page uses pages.get and answers a real 404 on null.
  • A legal page resolves its link through kind, not a hardcoded slug.
  • A menu recurses through children and localizes only internal paths.
  • A landing page uses its own tracking, not config.pixelId.
  • An editable surface uses pageData.get(surface) and resolveSurfaceDocument.
  • A missing document renders the theme's default, not an empty page.
  • The route cache is aware of how often each of these can change.