Stop shipping one order form
I built a checkout I was proud of, in one file. Merchants could not move a field. That is the whole story.
The first custom checkout I was proud of was one file. Gallery, offers, name, phone,
wilaya, city, address, quantity, and the buy button, all closed over the same useState.
It matched the design. I sent it out.
Then a merchant asked me to put the phone above the name. Another wanted email, but only on one product. A third wanted the address to wait until the shopper had picked a city, because an empty address line above the wilaya box made people type a street into the state. I was in the loop for all three. That is a bad job. Those are edits to a list of blocks.
So the order form in the blank template is a shell with a content slot. Every field is
its own component, and every one of them has hide.
What I mean by independent
When props.hide is true, the field renders null. The node stays in the slot, at the
same index. Turn it back on and it is where they left it. Delete is a different gesture.
We had themes where "hide" removed the node. The way back was to know a library id and drop a fresh copy, which wiped the props they had already set. I watched someone do that twice. Keeping the node is the feature.
The shell owns the behavior I don't want copied into every field. It saves a draft when
the phone hits the right length. It posts the order with POST /api/orders/create. It
keeps the honeypot, the submission-timing check, and the floating button that scrolls to
#feeef-order-buy. Fields read that state through useSlotContext("pdp"). The shell
publishes it with SlotContextBridge. If I see ff.orders.send in a field, I still grep
for it in review. That call belongs to an older client, and it skips the checks the shell
already did.
What the blank page shows
This is the product page, in slot order. I hid the fields people rarely want, and I left them in the list so turning them on is one checkbox.
| Block | Shown |
|---|---|
| Offers, extra quantity, email, delivery type, note, custom fields | Hidden. hide: true. Still sitting there. |
| Name, phone | Shown |
| State and city | Shown, one row, two child slots |
| Address | The node is there. The input waits until both state and city are set |
| Buy button with the total | Shown. The float scrolls to this |
| Quantity and add to cart | Shown, as their own row |
I split quantity off the buy button after watching someone try to park "add to cart"
under the gallery and leave "buy" under the form. One component with showQty and
showCart flags can only live in one place. Two nodes can live in two places.
The widget below is that list. It is a model of the editor, so you can poke it here. Toggle a field, move it, then mark state and city and watch the address row fill in.
Editor slots
Address stays in this slot. It appears after state and city are both set.
Folders, briefly
State and city are children of the geo row because they are one visual row and two
inspectors. That is not a reason to wrap the whole product page in three nested shells.
I use a folder when a component has slots/ or children/. Everything else is a flat
name.tsx with export const meta and function App(). Siblings under
pages/product/components/ compile to sections.main. The order field on meta is
the sort key.
slotsLayout is easy to trust too much. It tells the editor how to present the holes.
Your App() still has to render props.slots.stateSlot and props.slots.citySlot. I
have declared a layout, forgotten to paint the slot, and then watched the editor show a
region the shop never mounts. The merchant thinks the block is broken. It is empty.
What I left in, on purpose
Offers and the variant picker stay in the tree even though the blank theme hides them.
A product with offers, and a form that cannot select one, charges the catalog price
and surprises everyone at confirmation. Hidden is recoverable. Missing means the next
person rebuilds offers inside the buy button, badly. I have reviewed that version.
Shipping cards render nothing until a state is selected. A card that says "choose a wilaya" before a wilaya exists is how we trained people to tap a control that does nothing. The delivery-type block is hidden by default on top of that. Unhide it and the empty-state rule still holds.
If you are porting a single-file form, start from the blank shell and move the markup into the fields that are already there. The order form doc is the checklist I use. The create guide shows the files.