NUMU Docs
Contact
APIThemes
Partner Apps
APIThemes
Partner Apps
  1. Theme engine
  • 🗂️ Themes overview
  • 🚀 Start here
  • Glossary
  • Theme engine
    • Architecture
    • BYOT contract
    • Customizer
    • Federation runtime
    • Page data contract
    • Theme manifest
  • Theme SDK
    • Components
    • Federation helpers
    • Hooks
    • SDK overview
    • Type definitions
  • CLI & Vite plugin
    • CLI commands
    • CLI overview
    • Lint rules
    • Section library
    • Vite plugin
  • Storefront host
    • API proxies
    • Built-in fallbacks
    • BYOT fork
    • Routing
    • Storefront overview
  1. Theme engine

Page data contract

The shape of the object the storefront passes to a BYOT theme's mount(ctx) call.

Top-level#

interface MountContext {
  store: StoreData;
  themeSettings: ThemeSettingsV3;
  page: PageContext;
  locale: "en" | "ar";
  direction: "ltr" | "rtl";
  currency: string;             // e.g. "EGP", "USD"
}
The fields below are the values you'll actually destructure most often.

store#

interface StoreData {
  id: string;
  name: string;
  subdomain: string;
  default_language: "en" | "ar";
  currency: string;
  logo_url: string | null;
  favicon_url: string | null;
  contact_email: string | null;
  contact_phone: string | null;
  settings: Record<string, unknown>;   // free-form, includes payment + tax + analytics
}

themeSettings#

Resolved customizer state. The shape mirrors the merchant's customizer JSON after resolveThemeSettings() normalizes legacy fields:
interface ThemeSettingsV3 {
  theme_id: string;                    // e.g. "bazar" for built-in, or your published theme id
  external_theme?: {
    bundle_url: string;
    css_url?: string;
  };
  settings: Record<string, unknown>;   // resolved per settings_schema.json
  templates?: {
    home?: PageTemplate;
    product?: PageTemplate;
    collection?: PageTemplate;
    cart?: PageTemplate;
    [k: string]: PageTemplate | undefined;
  };
  // ... locales, presets, variants (theme color palettes), etc.
}

interface PageTemplate {
  sections: Record<string, SectionInstance>;
  order: string[];                     // section ids in render order
}

interface SectionInstance {
  type: string;                        // matches schemas/sections/<type>.json
  settings: Record<string, unknown>;
  blocks?: Record<string, BlockInstance>;
  block_order?: string[];
  disabled?: boolean;
}
Themes don't usually iterate templates directly — they receive resolved data through context (e.g. useSection() provides current section settings). But you can read themeSettings directly for global values like color schemes.

page per route#

The storefront sets page.type per route so themes can dispatch. page.data carries everything else the bundle needs to render that route without an extra API call.

Home#

{ type: "home", title?: string }
No data. Themes call useProducts(), useCollections(), etc. for the home layout.

Product detail#

{
  type: "product",
  title: product.name,
  handle: slug,
  data: { product: Product }   // includes options[] + variants[] post-Phase 8.1
}
The product object includes options (axis definitions) and variants (per-combination price/SKU/stock).

Collection / category#

{
  type: "collection",
  title: collection.name,
  handle: slug,
  data: { collection: Category, products: Product[], pagination: {...} }
}

Cart#

{ type: "cart", title: "Cart" }
No pre-fetch. Themes call useCart() which hits /api/cart from the bundle.

Checkout (multi-step, Phase 7)#

page.typepage.data
checkout_contact{ cart, customer }
checkout_shipping{ cart, address, rates, pickup_locations }
checkout_payment{ cart, address, methods, saved_cards }
checkout_review{ cart, address, rate, method }
checkout_processing{ order_id }
checkout_thank_you{ order }
Themes that want to drive checkout programmatically use useCheckout() from the SDK instead of routing through these page types.

Account routes#

page.typepage.data
account (dashboard){ customer }
account_login{}
account_register{}
account_recover{}
account_reset{ token: string }
account_profile{ customer }
account_orders{ customer, orders: Order[], pagination }
account_order{ customer, order: Order }
account_addresses{ customer, addresses: Address[] }
account_gift_cards{ customer }
account_wishlist{ customer, items }

Content routes#

page.typepage.data
page{ page: { title, body_html, handle } }
policies{ policy: { handle, title, body_html } }
blogs{ blogs: Blog[] }
blog{ blog: Blog, articles: Article[] }
article{ blog: Blog, article: Article }

Search#

{
  type: "search",
  data: { query: string, products: Product[], collections: Category[], pagination }
}

Special#

page.typeWhen
passwordStore is password-locked (pre-launch)
404Route didn't match anything

Common entity shapes#

Product#

interface Product {
  id: string;
  name: string;
  slug: string;
  description: string;
  short_description: string | null;
  product_type: "physical" | "digital" | "gift_card";
  status: "active" | "draft" | "archived";
  price: number;                       // major units, e.g. 99.00
  compare_at_price?: number;
  currency: string;
  sku: string | null;
  quantity: number | null;             // sum across variants
  in_stock: boolean;
  is_low_stock: boolean;
  is_on_sale: boolean;
  images: Array<{ url: string; alt?: string }>;
  tags: string[];
  category_id: string | null;
  attributes: Record<string, unknown>;  // free-form
  options: ProductOption[];            // Phase 8.1 axes
  variants: ProductVariant[];          // Phase 8.1
}

interface ProductOption {
  name: string;                        // e.g. "Size"
  position: number;
  values?: string[];                   // e.g. ["S", "M", "L"]
}

interface ProductVariant {
  id: string;
  option_values: Record<string, string>;  // { Size: "M", Color: "Red" }
  price: string;                       // decimal string
  compare_at_price: string | null;
  sku: string | null;
  inventory_quantity: number;
  is_in_stock: boolean;
  image_url: string | null;
}

Cart (from useCart())#

interface Cart {
  items: CartLineItem[];
  item_count: number;
  total_quantity: number;
  subtotal: number;                    // cents
  currency: string;
  applied_promotion?: { code?: string; label?: string; amount: number } | null;
}

interface CartLineItem {
  id: string;                          // line item key
  product_id: string;
  variant_id: string | null;
  product_name: string;
  variant_name: string | null;         // "Size: M / Color: Red"
  sku: string | null;
  quantity: number;
  unit_price: number;                  // cents, snapshotted at add-time
  total_price: number;                 // cents
  current_price: number;               // cents, LIVE
  price_changed: boolean;              // current vs unit
  image_url: string | null;
  in_stock: boolean;
  available_now: number | null;
  sold_out_now: boolean;
}

Order (from useOrders() / useOrder())#

interface Order {
  id: string;
  order_number: string;
  status: "pending" | "pending_deposit" | "confirmed" | "processing" |
          "shipped" | "delivered" | "cancelled" | "returned";
  payment_status: "pending" | "paid" | "refunded" | "partially_refunded" | "failed";
  fulfillment_status: "unfulfilled" | "partial" | "fulfilled";
  currency: string;
  subtotal: number;                    // cents
  shipping_cost: number;
  tax_amount: number;
  discount_amount: number;
  total: number;
  line_items: OrderLineItem[];
  shipping_address: Address;
  billing_address: Address;
  payment_method: string;
  shipping_method: string | null;
  coupon_code: string | null;
  customer_notes: string | null;
  created_at: string;
  shipped_at?: string;
  delivered_at?: string;
  metadata?: {
    gift_cards_applied?: Array<{ gift_card_id: string; last_four: string; amount_cents: number }>;
    gift_card_tender_total_cents?: number;
    tax_breakdown?: { rate: number; inclusive: boolean; added_cents: number; included_cents: number };
  };
}

Customer#

interface Customer {
  id: string;
  email: string;
  first_name: string | null;
  last_name: string | null;
  phone: string | null;
  total_orders: number;
  total_spent: number;
  created_at: string;
  default_address_id: string | null;
}

Defaults when fields are missing#

The storefront's API client (normalizeProduct, etc.) backfills certain fields:
currency → falls back to price_currency then to store.currency
in_stock → falls back to is_in_stock
images → defaults to [] so themes can iterate safely
variants / options → default to [] for products with no variants
If a field is required but missing (e.g. product.id), expect the page to 404 — themes should never see partial entities.
Modified at 2026-09-19 15:52:00
Previous
Federation runtime
Next
Theme manifest
Built with