Wayfinder: chart the Shopify headless commerce map
The route from "mozimo.in displays Shopify products" to "mozimo.in sells them", as a plan with a phase spine and gates, plus the open decision tickets that still shape it and the evidence they rest on. - .scratch/shopify-headless-commerce/: plan.md (phases and gates), map.md (the wayfinder map), 10 open decision tickets - CONTEXT.md: Storefront / Theme store / Cart / Parity vocabulary - docs/adr/0001-0003: Checkout stays Shopify-hosted, one Variant per product, the checkout domain stays shop.mozimo.in - docs/research/ and docs/shopify-catalog-inventory.md: verified store facts, the Cart API probe, and the catalogue inventory Raw Shopify doc scrapes under .scratch/shopify-research/ are ignored: they are working material, not record.
This commit is contained in:
1 parent
bb0c78fec5
commit
de0553d0d4
21 files changed
+3483
No files matched your search
@@ -0,0 +1,44 @@
|
||||
# Seed the dev store from the live catalogue
|
||||
|
||||
`Type:` task (HITL)
|
||||
`Status:` open
|
||||
|
||||
Part of [the map](../map.md). Empty the frontier from the top.
|
||||
|
||||
## Question
|
||||
|
||||
The dev store `staging-iw0gviik.myshopify.com` is empty. How does it come to hold the live catalogue faithfully, so that Phase 1 Cart work is built against realistic data?
|
||||
|
||||
The manual CSV route was chosen for this. Verification has now shown exactly where it holds and where it **cannot**:
|
||||
|
||||
### ✅ Products, and product metafields with preparation
|
||||
|
||||
- Export: **Products → Export** → scope → *Plain CSV* or *CSV for Excel/Numbers* → **Export products**. One file. Note that an **All products export is emailed**, not downloaded.
|
||||
- Import: **Products → Import** → add file → *Overwrite products with matching handles* → **Import products**.
|
||||
- Product metafields **do** ride in the CSV as their own columns, named `<Name> (product.metafields.<namespace>.<key>)`. `custom.ingredients` as multi-line text and `custom.energy_kcal` as a number are both documented-supported types.
|
||||
- **Precondition:** the metafield **definitions must already exist in the destination store** (Settings → Custom data → Products). Shopify's docs only promise columns for *defined* metafields. What happens to an undefined metafield's value is undocumented — so create the definitions before importing, not after.
|
||||
- Hazards: with *Overwrite* on, a **blank metafield cell overwrites the destination value as blank**. Images are URLs only and Shopify re-hosts them at import, so production's products and images must stay publicly reachable until the import finishes. There is **no dry run**, imports cannot be cancelled, there is no import history, and the file cap is 15 MB.
|
||||
|
||||
### ❌ Collection membership — impossible by CSV
|
||||
|
||||
The `Collection` column is **never exported**, and on import it is the only column you may add — and it admits **one collection per product**. Most of this catalogue belongs to three or four collections each: `mozimo-carnival-celebration-m` is in `best-sellers`, `gift-collection`, `diwali` and `hampers-boxes` at once. Sixteen collections and their multi-membership therefore need a **second mechanism**.
|
||||
|
||||
This part re-opens the seeding decision, because the chosen route cannot finish the job alone.
|
||||
|
||||
### ⚠️ Variant option shape — read the real export
|
||||
|
||||
Shopify's docs say a one-option product should carry `Default Title` in `Option1 value`, while the **official sample CSV represents its single-option product as `Option1 name = Title`, `Option1 value = Premium`** — which is exactly this catalogue's stored shape. The docs and their own sample disagree, and the documented hazards around *changing* option values (variants are deleted and recreated, so IDs change) make this worth confirming rather than assuming. Exporting from production settles it, and that export is needed anyway.
|
||||
|
||||
## To resolve
|
||||
|
||||
1. Export the full catalogue from production and record what `Option1 name` / `Option1 value` actually contain.
|
||||
2. Create the metafield definitions in the dev store before importing.
|
||||
3. Run a **small-batch import with *Overwrite* off** to surface per-line errors cheaply, then the full set.
|
||||
4. Decide the second mechanism for collection membership — admin bulk-editor passes, or a small Admin API script using the credentials the repo already mints. Record which was chosen and why.
|
||||
5. Record what actually landed: product count, collection count and membership, metafield coverage, anything rebuilt by hand.
|
||||
|
||||
Facts later tickets depend on: which products exist in the dev store, whether their Variant GIDs differ from production's (they will), and whether the metafield values survived.
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
@@ -0,0 +1,19 @@
|
||||
# Cart persistence and SSR hydration
|
||||
|
||||
`Type:` grilling (HITL)
|
||||
`Status:` open
|
||||
`Blocked by:` 01
|
||||
|
||||
Part of [the map](../map.md).
|
||||
|
||||
## Question
|
||||
|
||||
How is a Cart identified and carried across requests? Shopify addresses a Cart by ID, and this Storefront renders on the server, so the mechanism has to survive both.
|
||||
|
||||
Decide: where the Cart ID lives (an httpOnly cookie the server sets, client storage, or both), what its attributes are, and how the client hydrates without a flash of an empty Cart.
|
||||
|
||||
Decide the failure paths too, because both are reachable: a Cart expires after 30 days, and Shopify deletes it on checkout completion — so a returning buyer can arrive holding a cookie that points at nothing. What happens then — silent re-create, cleared Cart, or an error?
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
@@ -0,0 +1,22 @@
|
||||
# Degraded-mode experience
|
||||
|
||||
`Type:` grilling (HITL)
|
||||
`Status:` open
|
||||
|
||||
Part of [the map](../map.md).
|
||||
|
||||
## Question
|
||||
|
||||
When Shopify is unreachable, what does a buyer actually get?
|
||||
|
||||
The Storefront currently falls back to the Editorial catalog, which is display-only and whose prices can drift from commerce truth. We decided that stale **real** data beats fabricated data, with buying disabled. This ticket settles what that means concretely:
|
||||
|
||||
- When is degraded mode entered, and how is it detected — a failed Cart call, a failed product fetch, or both?
|
||||
- What does a buyer see? Does a notice appear, what does it say, and is it brand-appropriate for a luxury storefront?
|
||||
- Is stale data served transparently, or is the page visibly marked as out of date?
|
||||
- What exactly is disabled — the add-to-cart control, the Cart itself, or the route to Checkout?
|
||||
- What happens to an existing Cart mid-session when the API drops?
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
@@ -0,0 +1,25 @@
|
||||
# Prototype the Cart surfaces
|
||||
|
||||
`Type:` prototype (HITL)
|
||||
`Status:` open
|
||||
`Blocked by:` 02
|
||||
|
||||
Part of [the map](../map.md).
|
||||
|
||||
## Question
|
||||
|
||||
What do the mini-cart drawer and the `/cart` page actually look like, and what do they do?
|
||||
|
||||
Raise the fidelity of the discussion cheaply: a rough, concrete prototype of both surfaces is worth more than further prose. React to it and settle:
|
||||
|
||||
- What the drawer shows on add-to-cart — the added line only, or the whole Cart?
|
||||
- Line-item controls: quantity, remove, and how the gifting note attaches.
|
||||
- Where subtotal, discount entry and any shipping-threshold messaging sit.
|
||||
- How the drawer and the full page relate — does one open the other?
|
||||
- The transition to Checkout: what the button says, and how the handoff to `shop.mozimo.in` is framed so it doesn't read as leaving the brand.
|
||||
|
||||
The design language is editorial and restrained; the Cart is the first transactional surface in an otherwise editorial site, which is the tension worth prototyping.
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
@@ -0,0 +1,22 @@
|
||||
# The gifting model
|
||||
|
||||
`Type:` grilling (HITL)
|
||||
`Status:` open
|
||||
|
||||
Part of [the map](../map.md).
|
||||
|
||||
## Question
|
||||
|
||||
What is the gifting model on the Storefront?
|
||||
|
||||
A gift note as a Cart attribute is in Phase 1, and wrap and delivery options are Phase 3. Settle:
|
||||
|
||||
- Which of note, wrap and scheduled delivery actually apply to how this brand sells.
|
||||
- Where each is entered — product page, drawer, or `/cart`.
|
||||
- Whether it is per-Cart or per-line. A hamper and a bar in one order may need different messages.
|
||||
- Whether wrap or scheduled delivery carry a charge, and how that appears in the Cart.
|
||||
- What the Cart records versus what Checkout renders. Cart attributes flow through to the order; scheduled delivery in particular tends to need Shopify-side configuration, so establish what is even possible before promising it.
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
@@ -0,0 +1,20 @@
|
||||
# Discount configuration check
|
||||
|
||||
`Type:` task
|
||||
`Status:` open
|
||||
|
||||
Part of [the map](../map.md).
|
||||
|
||||
## Question
|
||||
|
||||
Does this store run any discounts?
|
||||
|
||||
The Storefront API cannot answer this: `discountCodes` and `discountNodes` are not fields on `QueryRoot` at 2026-10, and discount data is only reachable per live Cart. Whether a discount exists is therefore unverifiable from code, and the parity checklist cannot be written without knowing.
|
||||
|
||||
Resolve by checking Shopify admin directly: are there active discount codes, automatic discounts, or abandoned-checkout style offers? Do any apply by default at checkout?
|
||||
|
||||
Record what exists — codes, types, conditions, expiry — and note which of them a headless Cart would need to reproduce (`cartDiscountCodesUpdate` applies a code, but automatic discounts are Shopify-side).
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
@@ -0,0 +1,28 @@
|
||||
# The Parity checklist
|
||||
|
||||
`Type:` grilling (HITL)
|
||||
`Status:` open
|
||||
`Blocked by:` 05, 06
|
||||
|
||||
Part of [the map](../map.md).
|
||||
|
||||
## Question
|
||||
|
||||
What exactly must the Storefront be able to do before the Theme store's pages are retired?
|
||||
|
||||
Phase 5's gate is "the parity checklist is met", which is circular until the checklist exists. Enumerate it and settle what is genuinely required versus merely nice:
|
||||
|
||||
- Guest checkout, and confirmation that no account is ever needed to buy.
|
||||
- Discount codes, per whatever the configuration check found.
|
||||
- Gifting, per the gifting model.
|
||||
- Shipping and delivery options as a buyer experiences them. India-only is settled; what is shown about it is not.
|
||||
- Order confirmation and transactional email, which Shopify sends but which currently carries the Theme store's branding.
|
||||
- Order history, which depends on Phase 4 — so decide whether Phase 4 gates this or merely improves it.
|
||||
- Search, and whether the existing predictive search is sufficient.
|
||||
- Anything else a buyer can currently do on `shop.mozimo.in` that mozimo.in cannot.
|
||||
|
||||
For each item, mark whether it gates retirement or is deferred. Anything that gates retirement has to be finished before Phase 5, so this checklist sets the real scope of Phase 3.
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
@@ -0,0 +1,23 @@
|
||||
# Audit what points at the Theme store
|
||||
|
||||
`Type:` task (HITL)
|
||||
`Status:` open
|
||||
|
||||
Part of [the map](../map.md).
|
||||
|
||||
## Question
|
||||
|
||||
What currently sends traffic to `shop.mozimo.in`?
|
||||
|
||||
This cannot be answered from the codebase alone, and finding a live campaign pointed at a Theme store URL two days before cutover is the classic way this migration goes wrong. Inventory:
|
||||
|
||||
- **In the repo** — every hardcoded `shop.mozimo.in` reference, including the header's account and Bag icons, and any product URL built from the Shopify domain. (The audit should cover this, but the repo half is discoverable now.)
|
||||
- **Outside the repo** — paid ad campaigns and their landing URLs, Google and Meta shopping feeds, Instagram and Facebook shop, marketplace listings, email templates, and printed packaging or QR codes.
|
||||
|
||||
For each: what it points at, whether a mozimo.in equivalent exists, and whether it needs updating before or after cutover.
|
||||
|
||||
This gates Phase 5 and sets how much redirect coverage is actually required.
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
@@ -0,0 +1,22 @@
|
||||
# Catalogue content gaps
|
||||
|
||||
`Type:` grilling (HITL)
|
||||
`Status:` open
|
||||
|
||||
Part of [the map](../map.md).
|
||||
|
||||
## Question
|
||||
|
||||
Once the full 61-product catalogue is surfaced, most product pages will render without the content the design assumes: 41 of 61 products have no ingredients and no nutrition metafields, and `custom.tagline` is populated on exactly one product.
|
||||
|
||||
Settle how that is handled:
|
||||
|
||||
- Does the Storefront absorb it with conditional rendering, and what fills the space left behind? A product page with a missing panel can look broken rather than sparse.
|
||||
- Or is the data populated in Shopify first, which is content work rather than engineering work?
|
||||
- Or do the products without content stay unsurfaced until content exists, which contradicts surfacing the full catalogue but keeps every page complete?
|
||||
|
||||
Also settle the dead compatibility path this exposes: the entire `facts.*` metafield namespace is empty, so the `facts.nutrition` / `facts.ingredients` / `facts.storage` branch in `shapeProduct` can never fire and should be removed or justified.
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
@@ -0,0 +1,23 @@
|
||||
# Confirm the customer accounts version
|
||||
|
||||
`Type:` task
|
||||
`Status:` open
|
||||
|
||||
Part of [the map](../map.md).
|
||||
|
||||
## Question
|
||||
|
||||
Is this store on new customer accounts or legacy (classic) customer accounts?
|
||||
|
||||
Everything points to **new**: the admin panel exposes **Self-serve returns** and **Store credit** (✔ for new accounts, ✘ for legacy in Shopify's own comparison table), and the account URL is the `shopify.com/<id>/account` form that only the new system uses.
|
||||
|
||||
But that URL is **not conclusive**. Shopify's upgrade guide populates the new URL *before* cutover, so a store mid-migration can show it while still serving legacy account pages. Legacy accounts were deprecated on 2026-02-26 and the **sunset date is still unannounced** as of October 2026 — so if this store is genuinely still on legacy, Phase 4 stops being a comfortable late phase and has to move ahead of the sunset.
|
||||
|
||||
There are two clean discriminators, either of which settles it:
|
||||
|
||||
- The **Upgrade banner** on Settings → Customer accounts.
|
||||
- Admin GraphQL `Shop.customerAccountsV2.customerAccountsVersion`, which returns `CLASSIC` or `NEW_CUSTOMER_ACCOUNTS`.
|
||||
|
||||
## Answer
|
||||
|
||||
<!-- filled on resolution -->
|
||||
Reference in new issue
Block a user