# Build product listing and detail pages Build a product list and detail page that show the same price and availability the cart will use. Keep the page's presentation separate from the rules that determine whether a variant is sellable. ## Before you begin Use the [Storefront quickstart](/getting-started/storefront-quickstart) to verify one prepared product. Resolve the store, price channel, currency, country, and locale through your application's [request context](/concepts/storefront/request-context). ## 1. Load the product grid Storefront API ```graphql query ProductGrid($storeId: ID!, $priceChannelId: ID!, $currency: String!, $country: String!, $locale: String, $after: String) { products(first: 20, after: $after, storeId: $storeId, priceChannelId: $priceChannelId, priceCurrency: $currency, priceCountry: $country, locale: $locale, sortKey: NAME, sortDirection: ASC) { nodes { id name slug heroVariant { id image { src } price { value { centAmount currencyCode fractionDigits } discountedPrice { value { centAmount currencyCode fractionDigits } } } availability { availableForPurchase } } } pageInfo { hasNextPage endCursor } } } ``` Variables ```json { "storeId": "", "priceChannelId": "", "currency": "EUR", "country": "DK", "locale": "en-GB", "after": null } ``` Use the product ID as a stable rendering key and the slug for the product link. Display `price.discountedPrice?.value ?? price.value` as the current price. `price.value` is the original price; requesting only that field would miss a product discount. Load another page only when `hasNextPage` is true, with the returned cursor and unchanged context. Render an empty state without treating an empty list as a network failure. ## 2. Load the selected product Storefront API ```graphql query ProductDetail($slug: String!, $storeId: ID!, $priceChannelId: ID!, $currency: String!, $country: String!) { product(slug: $slug, storeId: $storeId, priceChannelId: $priceChannelId, priceCurrency: $currency, priceCountry: $country) { id name description variants(first: 100) { nodes { id name sku image { src } price { value { centAmount currencyCode fractionDigits } discountedPrice { value { centAmount currencyCode fractionDigits } } } availability { availableForPurchase } } pageInfo { hasNextPage endCursor } } } } ``` Variables ```json { "slug": "copenhagen-lamp", "storeId": "", "priceChannelId": "", "currency": "EUR", "country": "DK" } ``` Handle a null product as an unavailable or missing page. If a product has more variants than this page contains, paginate before claiming to offer every option. For structured selectors, add the attribute assignments and selections your UI needs from the reference. ## 3. Select an actual variant Keep the selected variant ID in state. Change its image, price, SKU, and purchase control together. Disable purchase when there is no usable price or `availableForPurchase` is false. Never create a variant ID or SKU by concatenating option labels. ## Verify Test a missing product, missing image, missing price, unavailable variant, and a product with multiple choices. Add the selected variant to a cart with matching context and confirm the returned item and amount. The [Next.js example](/guides/storefront/nextjs) places these operations under `src/lib/thorcommerce/storefront/queries/products.graphql` and the UI under `src/features/products`.