Crystallize logo

The request path of a storefront: catalogue page, product page, cart and checkout

The calls behind the four kinds of page every storefront has, in the order they happen, with the API for each step. Catalogue and product pages are cached Discovery reads with per-shopper parts streamed in. The cart and checkout run on the Shop API from your server.

Any catalogue page

Home, category, brand and landing pages are the same for every visitor except for a few personal parts. Build them that way:

  1. Resolve language and market from the path, not from a cookie. A crawler never sends your cookie, so a cookie market means search engines only ever see the default market's prices. See Languages, markets and routing.
  2. Read Discovery in a cached server function, keyed on the query and its variables. See Caching and revalidation.
  3. Stream the personal parts behind a loading boundary: taste-ranked rails, a "shopping as" badge, contract prices. Use real content as the fallback (the same list in its default order), not a skeleton.
  4. Trim the payload. Ask for WebP image variants in the widths a card uses, and ask for facets only where filters are shown. Facets are a large part of a listing's response.
  5. Refresh on publish. A Core webhook calls a route in your storefront that clears the cached reads.

A category listing is one browse on the product shape, filtered on the folder path at any depth, with the facets the sidebar shows:

query Category($path: String!) {
  browse {
    product(
      language: en
      filters: { parentPaths: { equals: $path } }
      facets: { brand_items_name: { limit: 10 }, attributes_value: { limit: 10 } }
      pagination: { limit: 24 }
    ) {
      summary {
        totalHits
        hasMoreHits
        endToken
        facets
        priceRange(priceIdentifier: "default") { min max }
      }
      hits {
        itemId
        name
        path
        defaultVariant {
          sku
          defaultPrice
          firstImage {
            url
            altText
            variants(types: "webp", maxWidth: 768) { url width height }
          }
        }
      }
    }
  }
}

summary.facets comes back as a hash keyed by facet field, with a count and a label per value. Each value carries the filter field to send back, so a facet link can be a plain URL that an agent or crawler can follow. Page through results with endToken as pagination.after. The Discovery API pages cover filters, sorting and pagination in full.

Product page

  1. One Discovery read by path returns the product, its variants, images, relations, breadcrumbs and typed specs. Use search with a path filter and a fragment for the shape. Paths are unique only within their folder, so never look a product up by its last path segment.
  2. One aliased Discovery read by `itemId` returns the product's path in every language, for hreflang and the language switcher.
  3. The buy box resolves the chosen options with variant(selected:) and shows stock from the variant's stock location.
  4. The price: the list price from Discovery for everyone. For a signed-in buyer, the contract price from a cached server-side read. Booking availability comes from the Shop API, loaded after the product.
  5. JSON-LD and the Markdown version of the page come from the same typed data. See SEO and GEO.
query Product($path: String!) {
  search(language: en, filters: { path: { equals: $path } }, pagination: { limit: 1 }) {
    hits {
      ... on product {
        itemId
        name
        path
        updatedAt
        breadcrumbs { name path }
        variants {
          sku
          name
          attributes
          defaultPrice
          defaultStock
          images { url altText variants(types: "webp") { url width height } }
        }
        selected: variant(selected: [{ attribute: "Color", value: "Olive" }]) {
          sku
          defaultPrice
          defaultStock
        }
      }
    }
  }
}

breadcrumbs is a list of trails, one per path to the item, and each trail ends with the item itself. Attribute names and values in variant(selected:) are case-sensitive. A name that only matches in another case returns null and adds a warning under extensions.warnings.

Paths in every language

Paths are translated, so the same product has a different path in each language. Read them all in one request, one alias per language, joined on the item id:

query Paths($id: String!) {
  en: search(language: en, filters: { itemId: { equals: $id } }, pagination: { limit: 1 }) { hits { path } }
  no: search(language: no, filters: { itemId: { equals: $id } }, pagination: { limit: 1 }) { hits { path } }
  sv: search(language: sv, filters: { itemId: { equals: $id } }, pagination: { limit: 1 }) { hits { path } }
}

A missing alias result means the item is not published in that language. Leave that language out of hreflang and the language switcher.

Cart

  1. A server action calls the Shop API `hydrate` with the whole basket, the price context and the customer. Hydrate replaces the cart's items, so send every line, not only the one that changed.
  2. The answer is the source of truth for the cart drawer: names, prices, taxes, discounts and totals are calculated by the Shop API.
  3. Show the change at once. Update the UI optimistically, then replace it with the hydrate answer, which takes a few hundred milliseconds.

The browser holds only the cart id, in an httpOnly cookie. The full flow, with the price context, is on Cart, checkout and orders.

Checkout

  1. Set the customer on the cart (setCustomer), then any cart metadata (setMeta), then lock the cart with place. After place the cart can't change. If the shopper goes back, start a new cart.
  2. Create the payment session on your server from the total that place returned.
  3. When the payment provider confirms (usually in its webhook), create the order with createFromCart, passing pipelines, payment status and payments in the same call.
  4. For bookings, confirm the reservations once the cart has its order. See Booking APIs.
  5. The confirmation page shows the order that createFromCart returned. Reading it back by id can briefly answer "not found", so retry rather than fail.
  6. Order history lists orders(customerIdentifier). A new order can take from seconds to minutes to appear there, so the confirmation page should say so.
tip

Where the details are

The Checkout Flow Tutorial walks through hydrate, place and createFromCart with full queries. Orders is the reference for order mutations, payments and pipelines.