Skill 16 · Wix Headless Templates
Subchapter 16.685
restaurants/INSTRUCTIONS.mdMarkdown37 KBView on GitHub
The restaurant machinery ships as files — the assembled menu tree (menus → sections → items
with variants, modifiers, labels, every price formatted in the site’s currency), the modifier
rule engine and the dish-ordering cart on the eCom current cart with the exact restaurant
catalogReference (context ids AND the visitor’s choices), the hosted checkout, and the
table-reservation flow (hold → reserve, or a request when the restaurant approves by hand)
driven by the location’s own form configuration, typed end-to-end. : you design and implement the dish card, the dish sheet, the menu surface, the
order-cart chrome, and the reservations surface on the shipped hooks/DTOs, plus the home page
and the brand. You never write ordering or reservation logic; you never skip designing.
On Astro and React the shipped files are tested and work as they are — this table and the
contracts below are everything you need to use them, so don’t spend the run reading their source;
wire them and build your surfaces. Reading them is the right move when something is off (a runtime
error, a field this playbook doesn’t cover) or when the brief wants a behaviour they don’t offer —
then read the file that owns it and change or extend it. On lib, static, and a port the
components don’t deploy at all, and each wiring section below opens with the files to read before
writing their equivalents. Files you edit: SiteLayout.astro, styles/global.css, and the two
pages’ island imports. Files you create: your menu, dish-sheet, cart-chrome, and reservation
components, plus your home page.
| file | what it is |
|---|---|
wix/config.ts · wix/sdk.ts | shared auth seam (deploy configures it — nothing to set by hand) |
wix/media.ts · wix/money.ts | imgAttrs(url, sizes) — every <img> attribute for a DTO image (src, srcSet, sizes, lazy): <img {...imgAttrs(item.imageUrl, "6rem")} alt={item.name} />; imgSrc() / imgSrcSet() / formatMoney() underneath, already used by everything shipped |
wix/restaurants/types.ts | the DTOs (MenuData, MenuSection, MenuItem, OrderSelection, OrderCart, OrderingStatus, ReservationLocationInfo, ReservationSlot, …) — contracts below |
wix/restaurants/menu.ts | fetchMenus — the whole display-ordered menu tree, every price formatted with the site’s currency + locale (read once from the eCommerce settings, fetchSiteMoney); the transport only — the id-array stitching, the item mapping, the money formatting, and the modifier rule engine (initialSelection, toggleModifier, validateSelection, itemPrice, needsSelection, ruleLabel) are in menu-core.ts beside it (shared with the REST layer) |
wix/restaurants/ordering.ts · order-store.ts | the operation’s state (resolveOrdering), each menu’s ordering settings under it (fetchMenuOrdering), the operation’s fulfillment methods and times, dish add-to-cart (Orders-app catalogReference with the selection) + eCom Cart V2, the order’s pickup/delivery + time (the cart’s delivery method) + the hosted checkout, and the shared cart state (module store — spans Astro islands); the request shapes, the availability-window check, and the refusal checks are in ordering-core.ts |
wix/restaurants/reservations.ts | locations (enabled for online reservations, with name/address/timezone/approval/form config), AVAILABLE slots in the location’s timezone, holdReservation → completeReservation (automatic approval), requestReservation (manual approval, no hold); the mappers, the form validation, and the reservee body are in reservations-core.ts |
wix/restaurants/time-core.ts | wall-clock arithmetic in an IANA zone with Intl only (zonedIso, zonedDayKey, zonedTimeLabel, zonedDateTimeLabel) — what the two cores and the stores use for the restaurant’s timezone |
wix/restaurants/menu-store.ts · reservation-store.ts | the menu-browsing and reservation state machines, framework-free (createMenuStore(), createReservationStore() — getState/subscribe + actions, one instance per surface); the hooks below bind them to React, every other stack uses them directly |
hooks/restaurants/useMenus.ts | React binding of menu-store.ts: menu browsing + menu switching — contract below |
hooks/restaurants/useOrderCart.ts | React binding of order-store.ts: order-cart state, the ordering gates, and actions — contract below |
hooks/restaurants/useReservation.ts | React binding of reservation-store.ts: the whole reservation state machine — contract below |
components/restaurants/MenuView.tsx (+ MenuItemCard, MenuItemSheet) · OrderCartButton.tsx · OrderCartDrawer.tsx · ReservationView.tsx | reference implementations — tested, plain, on the tokens; the layout mounts the cart chrome as shipped, and you replace all four with your own designs (new file names) |
styles/global.css | the design system: Tailwind v4 + the @theme token block (colors, radii, fonts — shared across verticals). Everything, shipped and yours, styles from these tokens |
The same data layer ships a second time as REST in templates/restaurants/rest/ (menu.ts,
ordering.ts, reservations.ts — the same exports over fetch, importing the same *-core.ts);
deploy.mjs --stack static composes it with the stores into js/wix/ (wiring below).
Astro stack additionally gets:
| file | what it is |
|---|---|
layouts/SiteLayout.astro | site chrome — yours to brand (keep the seo-tags slot, the global.css import, and one OrderCartButton + one OrderCartDrawer, both client:only). If another vertical is also deployed, its layout won — merge the Menu/Reservations links + cart mounts there |
pages/menu.astro | SSR menu — keep the frontmatter, swap the island import to YOUR component |
pages/reservations.astro | reservations — the island stays client:only="react" (availability is time-specific); swap the import to YOUR component |
singleSelect, checkboxes
otherwise — with its ruleLabel, quantity, the special-request note when
acceptsSpecialRequests, the live itemPrice), and your menu layout (menu tabs when >1,
section navigation, section rhythm), with skeletons while loading and an honest empty state —
on useMenus + useOrderCart + the menu-core selection helpers.descriptionLines — variant, modifiers, note — quantity stepper,
remove, subtotal, checkout CTA) — on useOrderCart, which owns ALL cart logic; you own how it looks.location.form (first name + phone always; last name / email / custom fields as the owner
configured; the marketing checkbox; terms and privacy links), the 10-minute countdown on the
automatic path, and the confirmed vs pending states — on useReservation.fetchMenus() in frontmatter →
your cards; item.featured marks highlights), hours/location story, reserve CTA. Every card
is a link to its dish on the menu: /menu#item-<item.id> (the shipped MenuItemCard renders
that id; #section-<section.id> lands on the section). When the brief includes ordering and
the item is orderable, the card may also carry the add-to-order control, on useOrderCart
exactly as MenuItemCard does; a card the visitor cannot act on is a dead end.Plus the theme (@theme block, one edit) and the chrome (SiteLayout, one pass).
Defaults for a brief that says nothing about them; the prompt wins when it asks for something else. Look at the seeded menu before designing (how many menus, sections, photos, variants, modifiers).
initialMenus);
sections in the menu’s own order; a dish card shows name, description, price (or its
variants, or “Market price”), labels, and the add control; skeletons while menus === null,
an honest empty state for []. Prices render as given — they are already formatted in the
site’s currency ("" only when the site’s currency couldn’t be read; then render nothing, never a
guessed symbol).menuOrderable(menuId) === false (not enabled for online ordering, or outside its hours —
say so once at the menu level); “Sold out” (item.soldOut: the dish, or a required group with
nothing in stock); the operation’s reason when ordering === false (“Ordering unavailable”, or
“paused” with orderingStatus.pausedUntilIso rendered in orderingStatus.timeZone); else “Add to
order”. A dish with needsSelection(item) opens your sheet; a plain dish adds at once. Adding
opens the cart drawer on its own; the header badge is the live itemCount.initialSelection(item) (pre-selected in-stock modifiers, the cheapest
variant); every change through toggleModifier; the CTA shows itemPrice(item, selection, quantity, menu.money).formatted; on submit validateSelection → per-group messages beside the
group, then addToOrder(item, { menuId, sectionId }, quantity, selection); out-of-stock
modifiers disabled.descriptionLines under the name, subtotal
as given, checkout as a button in the drawer; the order survives a reload (same visitor token) —
nothing to wire. Pickup/delivery and the time are chosen in the drawer, before checkout: a
Pickup/Delivery switch when fulfillment has both types, a time select from fulfillmentTimes
of that type (ASAP first, labelled in orderingStatus.timeZone), and for delivery street, city
and postal code (country and subdivision default to the restaurant’s; on blur the drawer reloads the
times with that address, so delivery times are only those of methods that deliver there) with the
delivery fee and minimum; the CTA calls chooseFulfillment(choice) then checkout().approval === "AUTOMATIC" → hold → details form with
the holdSecondsLeft countdown → confirm; approval === "MANUAL" (or slot.manualApproval) →
pick → details form → request, no hold; confirmed.outcome “confirmed” reads as confirmed,
“pending” as “pending the restaurant’s approval”; the toggle-off and not-set-up cases read as
“call us to book”, never as a broken form. A location picker only when locations.length > 1
(each has name, address).OrderCartDrawer does.// MenuData → sections → items, all display-ordered. EVERY money value is a formatted string in the
// site's currency + locale ("" when the currency couldn't be read); the decimal rides in *Amount.
// MenuData = { id, name, description, slug, sections, money: { currency, locale } }
// MenuItem = { id, name, description,
// price /* formatted; null when variant- or market-priced */, priceAmount,
// marketPrice /* true → "Market price", not orderable */,
// variants: [{ variantId, name, price, priceAmount }], // the cheapest is the default
// imageUrl, gallery: string[], labels: [{ id, name, iconUrl }],
// modifierGroups: [{ id, name, required, minSelections, maxSelections, rule, singleSelect,
// modifiers: [{ id, key /* `${id}~${index}` — the selection key */, name,
// preSelected, additionalCharge /* "" when free */, additionalChargeAmount, inStock }] }],
// inStock, soldOut /* !inStock or a required group with too few in-stock modifiers */,
// orderable /* !marketPrice && !soldOut */, acceptsSpecialRequests, featured }
//
// The selection helpers (wix/restaurants/menu-core.ts) — the rule engine, framework-free:
// OrderSelection = { variantId: string|null, modifiers: Record<groupId, key[]>, specialRequest }
// initialSelection(item) → preSelected && inStock modifiers (first only when singleSelect), cheapest variant
The class names in the shipped components are the Astro/React spelling of layout rules that hold
on every stack; on a stack where they don’t deploy, keep the rule and write it in your own CSS.
Hooks first, branches after (an early return above a hook changes hook order and React throws).
The menu island renders on the server too (client:load); nothing in a render path may throw.
// src/components/restaurants/YourMenu.tsx — YOU build it; pages/menu.astro mounts it with initialMenus.
import { useState } from "react";
import { useMenus } from "../../hooks/restaurants/useMenus";
import { useOrderCart } from "../../hooks/restaurants/useOrderCart";
import { imgAttrs } from "../../wix/media";
import { initialSelection, itemPrice, needsSelection, ruleLabel, toggleModifier, validateSelection } from "../../wix/restaurants/menu-core";
import type { MenuData, MenuItem, OrderSelection } from "../../wix/restaurants/types";
export default function YourMenu({ initialMenus }: { initialMenus?: MenuData[] }) {
const { menus, activeMenuId, setActiveMenuId, activeMenu, error } = useMenus({ initialMenus });
const { addToOrder, ordering, orderingStatus, menuOrderable, pendingItemId } = useOrderCart();
// …you implement the render:
// • menus === null → skeletons; [] → your empty state (error when set)
// • menu tabs when menus.length > 1 (setActiveMenuId); a section nav when activeMenu.sections.length > 1;
// one notice when menuOrderable(activeMenu.id) === false ("not available for online ordering right now")
// • per section, YOUR dish card: <img {...imgAttrs(item.imageUrl, "6rem")} alt={item.name} /> when
// item.imageUrl, name, description, item.marketPrice ? "Market price" : item.price ?? variants, labels
// • the add control: hidden for item.marketPrice or menuOrderable(menuId) === false; "Sold out" when
// item.soldOut; the operation's reason when ordering === false; else "Add to order" — which, when
// needsSelection(item), opens YOUR sheet (below) and otherwise calls
// addToOrder(item, { menuId: activeMenu.id, sectionId: section.id }) — the ids of the menu and section
// the card is rendered under, never looked up again; the rejection message rendered beside it;
// disabled (and reading "Adding…") only while pendingItemId === item.id — never on the cart's busy,
// which is the drawer's flag and would dim every dish in the grid for every add
// • YOUR sheet: const [sel, setSel] = useState<OrderSelection>(() => initialSelection(item)); variant radios
// (sel.variantId), per group a fieldset titled `${g.name} · ${ruleLabel(g)}` with radios when g.singleSelect
// else checkboxes, keyed by m.key, onChange → setSel(toggleModifier(item, sel, g.id, m.key)), disabled when
// !m.inStock, m.additionalCharge beside it; a textarea when item.acceptsSpecialRequests → sel.specialRequest;
// a quantity stepper; the CTA reads itemPrice(item, sel, qty, activeMenu.money).formatted; on submit
// validateSelection(item, sel) → errors per group, else addToOrder(item, ctx, qty, sel)
}// src/components/restaurants/YourReservations.tsx — YOU build it; pages/reservations.astro mounts it client:only.
import { useReservation } from "../../hooks/restaurants/useReservation";
export default function YourReservations() {
const r = useReservation(); // full contract above — the flow lives HERE
// …you implement the render, in this order:
// r.locations === null → loading; [] or !r.location → "call us to book";
// !r.location.onlineReservationsEnabled → "online reservations aren't open yet";
// r.confirmed → outcome "confirmed" as confirmed (r.location.form.submitMessage when set), "pending" as
// pending approval, "other" as not completed; r.reset() to start over;
// else: a location picker only when locations.length > 1 (name, address); guests (bounded by
// partySizeMin/Max), date, time → r.findSlots(); a note when r.approval === "MANUAL"; r.slots as pills →
// r.holdSlot(slot); r.selectedSlot → the details form: first name + phone always, last name / email with
// an asterisk when r.location.form.lastNameRequired / emailRequired, one input per
// r.location.form.customFields (r.setCustomField), the marketing checkbox when form.marketingCheckbox
// (r.setMarketingConsent), terms/privacy links when form.terms / form.privacy; the countdown from
// r.holdSecondsLeft when r.held; r.fieldErrors beside the fields; the CTA gated by r.canConfirm reading
// "Complete reservation" (held) or "Request reservation" (manual); r.error inline
}On lib, static, and a port, nothing under components/ or hooks/ arrives. The state
machines behind the hooks do arrive — wix/restaurants/menu-store.ts, reservation-store.ts,
order-store.ts — so you never rewrite them: create a store per surface, subscribe, render from
getState(), call its actions. Their *State interfaces are the render contract; read those.
What you write is the rendering — dish card, dish sheet, menu, cart chrome, reservation form — and
for that read these first; they are tested code for exactly that behaviour:
components/restaurants/MenuView.tsx — the menu tabs / section nav / dish card / dish sheet,
and the load-bearing wiring: each card threads its menuId + sectionId from the render
context into addToOrder; the add control’s states; the sheet on the menu-core selection
helpers; the rejection message beside it.components/restaurants/OrderCartDrawer.tsx — the cart overlay as working code: lines with
stepper and remove, status !== "IN_STOCK" flagged, subtotal, checkout, error rendered.components/restaurants/ReservationView.tsx — the state ladder in order: loading → not set up →
toggle off → confirmed / pending → query → slots → hold or pick → the form from location.form;
the confirm gate.All under templates/restaurants/app/.
@theme tokens (one edit); brand SiteLayout.astro (one pass — merge into the
winning layout instead if another vertical is also deployed).src/components/restaurants/ (new names — don’t overwrite the
references), swap the island imports in pages/menu.astro and pages/reservations.astro.
Menu island: client:load with the SSR props; reservations island and the cart chrome:
client:only="react". Author your surfaces in as few messages as possible — batch
multiple Writes per message.pages/index.astro (home) — it exists from the scaffold; Read it before overwriting.Read the reference files listed above before writing any surface.
deploy.mjs restaurants --stack lib put the data layer in src/wix/ and nothing else: sdk.ts
(the visitor client, configured with the public client id), media.ts, money.ts, and
wix/restaurants/ — menu.ts, ordering.ts, reservations.ts, types.ts, the *-core.ts
rules (including time-core.ts), and the three stores menu-store.ts, order-store.ts,
reservation-store.ts. None of it is React. The hooks and components don’t ship on this stack;
the stores replace the hooks, and you write the components in your framework to the contracts on this page:
shallowRef updated in
subscribe; Svelte: readable(store.getState(), (set) => store.subscribe(() => set(store.getState())));
Solid: a signal set in subscribe). createMenuStore({ initialMenus? }) per menu surface
(start() when mounted, stop() when unmounted), createReservationStore() per reservation
surface, the order store as-is (module-level: subscribeOrderCart/getOrderCartState,
isMenuOrderable, addOrderLine(item, { menuId, sectionId }, quantity?, selection?),
updateOrderLineQuantity, removeLineFromOrder, chooseFulfillment, loadFulfillmentTimes,
goToOrderCheckout, setOrderCartOpen).
State in, actions out — exactly the hooks’ contracts above; the dish sheet’s local state is an
OrderSelection driven by the menu-core helpers;MenuView.tsx, OrderCartDrawer.tsx, ReservationView.tsx are readable as behaviour
specs.Routes /menu, /reservations; dev server on 4321; a static build goes through
npx @wix/cli@latest release with site.outputDirectory pointing at the build folder, an SSR
build is hosted by you.
Read static.md in this folder: it is the whole playbook for this stack, in one file. The pages
import the layer from the site’s js/wix/ folder and write every control on the stores;
static.md shows how to drive each store, with the menu, the dish sheet, the order drawer, the
reservation flow, the routes, the rules and the seed.
Read the reference files listed above before writing any surface.
Run deploy.mjs restaurants --stack static in the project folder anyway: js/wix/ is both the
browser-side code and the readable spec. Then split by where the call runs. The menu on the
server: port js/wix/menu.ts and menu-core.ts to your language — the eCommerce-settings GET
for the site’s currency + locale, three cursor-paged GETs for the visible menus/sections/items
(paging.cursor until pagingMetadata.cursors.next is empty), the id-array GETs for variants,
modifier groups, and modifiers (arrays repeat the key: ?variantIds=a&variantIds=b, 100 per call),
labels, then the id-array stitching and the price formatting from the core — returning the same
MenuData tree as dicts, one anonymous visitor token per process for these public reads (mint and
refresh per client.ts) — and render the menu and the home page’s featured dishes in your
templates, so dish names are in the HTML. Ordering and reservations in the browser: the cart
button and drawer on ./js/wix/order-store.js, each dish card’s add control calling
addOrderLine(item, { menuId, sectionId }, quantity, selection) with the item DTO and the ids the
template rendered it under (a data-item JSON attribute, data attributes for the ids), the
reservation form on ./js/wix/reservation-store.js — exactly as the static wiring above; the
browser owns the visitor’s token, so the server never handles per-visitor tokens. Routes stay
/menu, /reservations. Add your public https origin to the OAuth app’s allowed domains before
checkout can return.
Pre-rendered (Frozen-Flask, Pelican, any static-site generator) → Wix-hosted. Same port for
the menu read, run at build time with one anonymous token; the generator emits the menu page(s)
and the home page. Run deploy.mjs restaurants --stack static --out <build dir> so js/wix/ is
inside the output the pages import from, point site.outputDirectory at that folder,
wix release. Pages sit at different depths: give the templates one base path to js/wix/ (a
template variable, or root-relative /js/wix/…), never a relative ./js/wix/ — it breaks one
level down. The frozen menu is the first paint; the add controls, cart, and reservation flow run
client-side on it from the same modules, so the contracts above apply. Close with the live URL,
the rebuild + release command, and one line for the owner: dashboard edits to the menu reach the
site when that command runs; ordering and reservations are live regardless.
Import ./styles/global.css once at the app entry (needs @tailwindcss/vite in the vite
plugins — deploy added the dep). Routes: /menu → your menu surface (useMenus() fetches
client-side when no initialMenus); /reservations → your reservation surface. Mount your
order button in the header and your drawer once. Deploy wrote the public client id into
wix/config.ts; nothing else to configure.
Routes on Wix hosting: the host serves files only, so a clean route answers 404 when loaded directly — hash routes, or one HTML file per route, decided before the first route is written; any URL handed to Wix as a return target must be one the host serves (SKILL.md step 1).
useOrderCart/addToOrder
own the restaurant catalogReference (the Orders app id + operationId/menuId/menuSectionId
— all three — plus the visitor’s choices as options.priceVariant, options.modifierGroups,
options.specialRequests), the line-refusal checks, and the checkout redirect. Never
re-derive any of it, never hand-build a checkout URL; extend by adding a function in
wix/restaurants/ for what they don’t cover (API contracts: the wix-docs skill).item + menuId + sectionId from the render context — each dish is rendered
inside a known section of a known menu (the fetchMenus tree); pass the item DTO and those ids
to addToOrder. Never look them up again.initialSelection → toggleModifier → validateSelection → addToOrder(item, ctx, quantity, selection). Never send a modifier id as a selection key (keys are MenuModifier.key),
never invent an option shape, never compute a price outside itemPrice.price, additionalCharge, fee, subtotal as given;
arithmetic uses the *Amount decimals; nothing prefixes a symbol, nothing assumes a currency.ordering === true (the
operation), menuOrderable(menuId) !== false (the menu’s settings and hours), item.orderable
(the dish). Each false state has its own copy; a market-priced dish has no control.useReservation (or its store) — AVAILABLE-only slots in the
restaurant’s timezone, the two approval paths (hold → reserve; request without a hold), the
hold’s revision, the form rules from location.form, and the real status all live in the data
layer. Render the form from location.form (don’t hardcode which fields exist or are required),
gate the CTA on canConfirm, surface error and fieldErrors (holds expire in 10 minutes —
the store counts down and restarts the flow).confirmed, and only
outcome === "confirmed" as confirmed — “pending” (REQUESTED, PAYMENT_INFORMATION_PENDING) as
“pending the restaurant’s approval”, “other” as not completed. A visitor returning from the
hosted checkout is NOT an order-success signal.ordering === false → the operation’s reason on the add button
(orderingStatus.status, pausedUntilIso); menuOrderable(id) === false → no add controls and
one notice for that menu; onlineReservationsEnabled === false → a “call us to book” notice (the
toggle is premium-gated). Never mock menus, dishes, prices, slots, or availability.chooseFulfillment (times from fulfillmentTimes, the
address for delivery); checkout() falls back to pickup ASAP only when nothing was chosen, and
says “Choose pickup or delivery” when the restaurant has no pickup. Never build the delivery
method code yourself.cart — never summed or hardcoded in the client; fees, tax, and
delivery are shown on the hosted checkout. The sheet’s itemPrice is a preview of one line, not a total.@theme tokens, and your
markup uses Tailwind utilities on the same tokens — one design system across shipped and written
code. No parallel theme files, no hardcoded palette values. Where they don’t (lib, static,
a port): style with whatever your stack does well, on one token set of your own; the rule that
survives is the token set, not Tailwind.Hand the owner these links — {siteId} is siteId in wix.config.json (the deploy JSON prints it as
dashboardUrl); a menu id fills the placeholder from the seed result.
| page | https://manage.wix.com/dashboard/{siteId}/ + |
|---|---|
| Menus | wix-restaurants-menus-new |
| Edit a menu | wix-restaurants-menus-new/menu/{menuId} |
| Items | wix-restaurants-menus-new/items |
| Online orders board | wix-restaurants-orders-new |
| Ordering settings (pickup/delivery hours, fees, menu hours) | wix-restaurants-orders-new/settings |
| Reservations | wix-table-reservations/table-reservations |
| Floor plan | wix-table-reservations/floor-plan |
| Accept payments — connect a payment method | wix-cashier/payments |
| Upgrade the plan — online payments need premium | full URL: https://www.wix.com/upgrade/website?metaSiteId={siteId} |
Real paid orders need a connected payment method and a premium plan, and holding, completing, or requesting an online reservation is premium-gated. Until both are done, a visitor who reaches hosted checkout sees “We can’t accept online payments. Contact us for help with your order.” Hand both links above in the close and name the reservations gate; don’t treat either as a code failure.
Per seed/SEED.md — plain-data plan.json into seed-restaurants.mjs from the project
root. Seed a menu that exercises the UI (a few sections, an image per dish, at least one dish
with a size or a modifier group when the restaurant takes orders) and turn on the ordering +
reservations add-ons when the restaurant takes orders/bookings.
A fresh Menus install carries Wix’s sample “Dinner Menu”, and the menu page lists it next to the
seeded menus. Never delete it, or anything else on the site: the result’s preexistingMenus[] names
what is there, and the closing message says so with the menus dashboard link so the owner removes it
there — never release a restaurant that serves the sample “Dinner Menu” without telling the owner.