Skip to content

Shop & products

Routes

Route Purpose
/shop All products in popularity order, with a row of category chips
/shop/fruits Fruits category (24)
/shop/boxes Fruit Plug Boxes category (4)
/shop/subscription Subscription category (3)
/shop/merchandise Merchandise category (10)
/shop/rare-special Rare & Special category (5) — new, post Phase 0
/p/[slug] Product detail page (PDP)

Data flow

sequenceDiagram
    participant Browser
    participant Next as Next.js (Server Component)
    participant Woo as Woo Store API
    Browser->>Next: GET /shop
    Next->>Woo: GET /wc/store/v1/products?per_page=100&orderby=popularity
    Next->>Woo: GET /wc/store/v1/products/categories
    Woo-->>Next: JSON (parallel fetches)
    Next-->>Browser: SSR HTML · cached 5min (ISR tag `products`)

The Woo Store API is public, cookie-sessioned, and returns the same data the live WooCommerce admin sees. We parallelise the two fetches and server-render the grid.

How the PDP handles variable products

The Store API has a quirk: GET /products?slug=mangosteen can return a variation (with type: "variation") whose slug matches the parent's. We detect this and re-fetch by id to get the full parent record with attributes, description, price range. See packages/woo-client/src/index.ts:

export async function getProductBySlug(slug: string) {
  const list = await wooFetch("/products", { searchParams: { slug, per_page: 10 }});
  const parent = list.find(p => p.type !== "variation") ?? list[0];
  return parent ? wooFetch(`/products/${parent.id}`) : null;
}

On the PDP, related products are fetched by querying the same category as the current product and excluding the current one, limited to 4. Better than Woo's default "randomly related" — semantic matching.

Variation display

Variable products (e.g. Mangosteen has Each / 0.5KG Box / 1KG Box) render their attributes as a section of clickable chips. Selecting a chip updates the Add-to-Cart payload:

variation: [{ attribute: "Quantity", value: "1KG Box" }]

The selector lives in components/product/ProductActions.tsx — a thin client island embedded inside the otherwise-server-rendered PDP.

Image handling

Product images come through Jetpack Photon (i0.wp.com). next.config.ts allows the *.wp.com and fruitplug.co.uk hostnames. Cloudinary is prepared but not yet in use.

Performance

Each page pre-fetches once and renders server-side. TTFB is ~50–100ms after warm cache. Client-side JS is minimal — only the ProductActions component ships with interactivity, everything else is static HTML + CSS.