Store config and languages
config.get() is the first read of most pages. It returns the store's public
settings or null when they cannot be read.
Code
A null config is a real failure — the API is unreachable, the key is wrong — not an empty store. You have two honest answers:
- Render the SDK's unavailable store,
unavailableStoreConfig({ serverUrl, locale }), the wayresolveStoreRequestdoes with itsfallbackoption (below). The page stays up with nothing to buy. - Answer an error page (
503) for a page that cannot work without the store.
Either way, the cause is in the client's onError.
What the config contains
| Group | Fields | Use it for |
|---|---|---|
| Identity | id, name, domain, logo | Page title, header, sharing. id is "" only for the unavailable store |
| Languages | lang, locale, locales | <html lang> and dir, switcher, links |
| Currency | currency, currencySymbol | formatMoney; currencySymbol is in locale (دج in Arabic, DA in French) |
| SEO | metaTitle, metaDescription, ogImage | Head metadata |
| Media | media | Where every image key is served from and how it is resized: pass it to the image helpers |
| Features | reviewsEnabled, cartEnabled, otpEnabled, turnstileEnabled, turnstileSiteKey | Whether to render a surface; the public widget key |
| Cart | freeShippingThreshold | Copy and display only; the quote owns the fee |
| Checkout | checkoutForm | Which fields to render and which are required |
| Tracking | pixelId, conversionEvent | Meta pixel installation and event choice |
| Pages | pages | Footer and consent links |
| Contact | contact | The store's phone, email and address, each null until the merchant writes it |
whatsapp | The WhatsApp button App; read it through whatsAppContact | |
| Theme | themeSettings, contentOverrides | Editable themes |
logo and ogImage are MediaImages, { key, url, width, height }, or
null. Every field above is always present; a field the merchant has not set
is null, false, [] or {}, never missing.
Colours and fonts are theme settings
The config carries no colours, font or announcement bar. Those are the
theme's own settings: an editable theme declares them in
defineTheme({ settings }) and reads them from themeSettings.
Feature switches
Render a feature only when the store has it:
Code
A store with cartEnabled: false must ship no cart markup at all. This is not
cosmetic: the switch is the rollback path for the cart feature.
The same rule applies to reviews, OTP and the WhatsApp button (whatsapp is
null when the store has none). Their settings exist so a store can turn the
surface off without a deploy.
Checkout policy
checkoutForm is the merchant's form policy, already resolved:
Code
Read it; do not re-decide it. In particular:
hiddenmeans render no input, not a disabled or empty one.customFieldsare merchant-authored. Renderlabelas text, never as HTML.- Send answers as
customFieldResponses, keyed by the field's server-mintedid. - The server validates the same policy on order creation. A client-side shortcut cannot make a required field optional.
Code
The order action accepts one JSON field for these answers, so a merchant can add a custom field without a theme deploy.
Languages
A store has a primary language (lang) and a list of published languages
(locales):
Code
locales is ordered with the primary first. It is the only authority on which
languages exist; do not hardcode a language list in your theme.
servedLanguage(config, code) gives the language object for <html lang> and
dir:
Code
URL shape
The primary language has no prefix. Every other published language has one:
Code
The helpers that implement this are pure:
Code
resolveLocalePath only treats a first segment as a language when the store
publishes it. An unknown prefix is an ordinary path and 404s normally.
A landing page exists in the primary language only: it is one campaign,
written once for the ads that point at it. SINGLE_LANGUAGE_PAGE_TYPES
(["landing"]) names such page types and isSingleLanguagePage(pathname)
tells one from any other page. resolveLocalePath redirects any prefix on
it, localizeHref leaves its links unprefixed, and alternateLinks returns
no alternates for it. A language menu should list no languages there.
One request, one language
resolveStoreRequest from @wocart/storefront/astro applies the whole
language policy for a request: which store config, which language, which
client, and whether to redirect. createStorefrontMiddleware calls it for
every page, so a theme's middleware is one call
(Astro integration):
Code
| What it decides | How |
|---|---|
| The language | From the path's first segment when the store publishes it (/fr/…); else the primary |
| The primary's own prefix | /ar/x on an Arabic-primary store redirects 301 to /x |
| A landing page under a prefix | /fr/lp/x?fbclid=… redirects 301 to /lp/x?fbclid=…, the query kept |
| A language the store stopped publishing | Redirects 302 to the primary page — it may come back |
| The route to render | rewrite: "/products/robe" for /fr/products/robe, so one route file serves every language |
| The client | client(locale) for a non-primary language, client() for the primary — one client per language, each with its own caches |
| An endpoint or server island | /api/… and /_server-islands/… take the language of the page that asked (its same-origin Referer), and are never redirected |
A rewrite to /404 | createStorefrontMiddleware keeps the store the request already has, so the 404 is in the shopper's language |
| An unreadable config | fallback renders instead |
config.locale is the language that actually arrived. A page's links go
through localizeHref(path, locale, routing); ResolvedStore.pathname is the
path without its prefix, which is what matchStorefrontPath and
showsWidgetOn expect. The theme quick start
builds a full request store on top of this, including the theme's text.
unavailableStoreConfig({ serverUrl, locale }) is the fallback: an empty
id, nothing to buy (no cart, checkout delivery options, OTP, Turnstile,
reviews or WhatsApp), locale as its only language, and images served
directly from <serverUrl>/media. Never write a fallback config by hand; the
theme quick start shows it
in a full project.
If you need the pieces without the policy, they are pure functions:
localeRouting(config) → { primary, published },
resolveLocalePath(pathname, routing) → a page or a redirect, and
localizeHref(href, locale, routing).
Links and alternates
Every internal link should go through localizeHref, including links returned
by menus:
Code
In the head, emit one alternate per published language. Pass the page's path
without its language prefix (ResolvedStore.pathname): on /fr/x,
Astro.url.pathname is /fr/x, and every alternate would point at the
French page.
Code
Theme text and settings
Two fields only hold data when the client is created with a theme key:
Code
contentOverrides[locale]is the merchant's edits to your theme's text in that language. Merge only that language's map over your own copy.themeSettingsis the merchant's saved values, exactly as stored. In an editable theme, resolve them against the theme declaration withresolveThemeSettingsfrom@wocart/theme.- Without
theme, both are empty/absent. A plain custom storefront can ignore them and use its own copy and styling.
Read Theme SDK if you are building an editable theme.
Pages and contact details
config.pages lists the published pages that opted into the footer, titled
in locale and in display order:
Code
Use kind when the rule names a page: "you agree to the Terms" means the
terms kind, not a hardcoded /pages/terms slug. A merchant may rename a
slug. Build the link with storefrontPath, then localize it with
localizeHref.
config.contact is how a shopper reaches the store, besides WhatsApp:
Code
Each value is null until the merchant writes it. Show each one only once it
is set, and never write a store fact in your theme's own words: a phone
number, an address, opening hours or a delivery promise is true of one store
and false of the next. Delivery times belong to the store's own pages (its
shipping page), not to the config.
config.whatsapp is the WhatsApp button App's settings, or null when the
store has none. Each of its texts arrives in the page's language, or null
where the merchant wrote none in that language. Never read it field by field:
whatsAppContact(config, page, words) resolves the link, the message and
every text, with your theme's words where the merchant left one empty.
Code
showsWidgetOn(surfaces, pathname) decides only the floating launcher: a
WhatsApp link elsewhere on the page (a footer, a contact card, an order
button) does not depend on it. Pass the path without its language prefix:
/fr/products/robe is not a storefront path, /products/robe is.
| Switch | Pages |
|---|---|
home | The home page |
catalog | All products, categories, collections |
product | Product pages |
pages | Store pages, order tracking, search and contact |
thankYou | The order confirmation |
checkout | The basket checkout |
landing | Landing pages |
Any other path shows no launcher. words and the message rules are covered
in Money, images, WhatsApp and Meta events.