--- title: Combined Listings in Hydrogen description: Handle combined listings on product pages and in search results. source_url: html: >- https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings md: >- https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md --- ExpandOn this page * [Requirements](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#requirements) * [Ingredients](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#ingredients) * [Step 1: Set up the Combined Listings app](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-1-set-up-the-combined-listings-app) * [Step 2: Configure combined listings behavior](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-2-configure-combined-listings-behavior) * [Step 3: Add combined listings utilities](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-3-add-combined-listings-utilities) * [Step 4: Hide the cart button for combined listing parent products](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-4-hide-the-cart-button-for-combined-listing-parent-products) * [Step 5: Support product and variant images](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-5-support-product-and-variant-images) * [Step 6: Show a range of prices for combined listings in Product​Item](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-6-show-a-range-of-prices-for-combined-listings-in-productitem) * [Step 7: (Optional) Redirect to the first variant](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-7-optional-redirect-to-the-first-variant) * [Step 8: Filter combined listings from the all products page](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-8-filter-combined-listings-from-the-all-products-page) * [Step 9: Filter recommended products](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-9-filter-recommended-products) * [Step 10: (Optional) Filter out combined listings from collections pages](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-10-optional-filter-out-combined-listings-from-collections-pages) * [Step 11: Show price ranges on product pages](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-11-show-price-ranges-on-product-pages) * [Step 12: Style the price range display](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-12-style-the-price-range-display) * [Next steps](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#next-steps) # Combined Listings in Hydrogen This recipe lets you more precisely display and manage [combined listings](https://help.shopify.com/en/manual/products/combined-listings-app) on product pages and in search results for your Hydrogen storefront. A combined listing groups separate products together into a single product listing using a shared option like color or size. Each product appears as a variant but can have its own title, description, URL, and images. In this recipe, you'll make the following changes: 1. Set up the Combined Listings app in your Shopify admin and group relevant products together as combined listings. 2. Configure how combined listings will be handled on your storefront. 3. Update the `ProductForm` component to hide the **Add to cart** button for the parent products of combined listings. 4. Update the `ProductImage` component to support images from product variants and the product itself. 5. Show a range of prices for combined listings in `ProductItem`. Note This recipe is compatible with React Router 7.9.x and uses consolidated imports from 'react-router' instead of separate '@shopify/remix-oxygen' and '@remix-run/react' packages. *** ## Requirements * Your store must be on either a [Shopify Plus](https://www.shopify.com/plus) or enterprise plan. * Your store must have the [Combined Listings app](https://admin.shopify.com/apps/combined-listings) installed. *** ## Ingredients *New files added to the template by this recipe.* | File | Description | | - | - | | [app/lib/combined-listings.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/ingredients/templates/skeleton/app/lib/combined-listings.ts) | The `combined-listings.ts` file contains utilities and settings for handling combined listings. | *** ## Step 1: Set up the Combined Listings app 1. Install the [Combined Listings app](https://admin.shopify.com/apps/combined-listings). 2. [Create combined listing products in your store](https://help.shopify.com/en/manual/products/combined-listings-app#creating-a-combined-listing). 3. Add tags to the parent products of combined listings to indicate that they're part of a combined listing (for example `combined`). *** ## Step 2: Configure combined listings behavior You can customize how the parent products of combined listings are retrieved and displayed. To make this process easier, we included a configuration object in the `combined-listings.ts` file that you can edit to customize according to your preferences. ```ts // Edit these values to customize the combined listings behaviors export const combinedListingsSettings = { // If true, loading the product page will redirect to the first variant redirectToFirstVariant: false, // The tag that indicates a combined listing combinedListingTag: 'combined', // If true, combined listings will not be shown in the product list hideCombinedListingsFromProductList: true, }; ``` *** ## Step 3: Add combined listings utilities Create a new `combined-listings.ts` file that contains utilities and settings for handling combined listings. #### File: [combined-listings.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/ingredients/templates/skeleton/app/lib/combined-listings.ts) ## File ```ts // Edit these values to customize combined listings' behavior export const combinedListingsSettings = { // If true, loading the product page will redirect to the first variant redirectToFirstVariant: false, // The tag that indicates a combined listing combinedListingTag: 'combined', // If true, combined listings will not be shown in the product list hideCombinedListingsFromProductList: true, }; export const maybeFilterOutCombinedListingsQuery = combinedListingsSettings.hideCombinedListingsFromProductList ? `NOT tag:${combinedListingsSettings.combinedListingTag}` : ''; interface ProductWithTags { tags: string[]; } function isProductWithTags(u: unknown): u is ProductWithTags { const maybe = u as ProductWithTags; return ( u != null && typeof u === 'object' && 'tags' in maybe && Array.isArray(maybe.tags) ); } export function isCombinedListing(product: unknown) { return ( isProductWithTags(product) && product.tags.includes(combinedListingsSettings.combinedListingTag) ); } ``` *** ## Step 4: Hide the cart button for combined listing parent products 1. Update the `ProductForm` component to hide the **Add to cart** button for the parent products of combined listings and for variants' selected state. 2. Update the `Link` component to not replace the current URL when the product is a combined listing parent product. #### File: [ProductForm.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/components/ProductForm.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/patches/ProductForm.tsx.649d3b.patch)) ## File ```diff @@ -11,9 +11,11 @@ import type {ProductFragment} from 'storefrontapi.generated'; export function ProductForm({ productOptions, selectedVariant, + combinedListing, }: { productOptions: MappedProductOptions[]; selectedVariant: ProductFragment['selectedOrFirstAvailableVariant']; + combinedListing: boolean; }) { const navigate = useNavigate(); const {open} = useAside(); @@ -50,12 +52,13 @@ export function ProductForm({ key={option.name + name} prefetch="intent" preventScrollReset - replace + replace={!combinedListing} to={`/products/${handle}?${variantUriQuery}`} style={{ - border: selected - ? '1px solid black' - : '1px solid transparent', + border: + selected && !combinedListing + ? '1px solid black' + : '1px solid transparent', opacity: available ? 1 : 0.3, }} > @@ -76,9 +79,10 @@ export function ProductForm({ }`} key={option.name + name} style={{ - border: selected - ? '1px solid black' - : '1px solid transparent', + border: + selected && !combinedListing + ? '1px solid black' + : '1px solid transparent', opacity: available ? 1 : 0.3, }} disabled={!exists} @@ -101,25 +105,27 @@ export function ProductForm({ ); })} - { - open('cart'); - }} - lines={ - selectedVariant - ? [ - { - merchandiseId: selectedVariant.id, - quantity: 1, - selectedVariant, - }, - ] - : [] - } - > - {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'} - + {!combinedListing && ( + { + open('cart'); + }} + lines={ + selectedVariant + ? [ + { + merchandiseId: selectedVariant.id, + quantity: 1, + selectedVariant, + }, + ] + : [] + } + > + {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'} + + )} ); } ``` *** ## Step 5: Support product and variant images Update the `ProductImage` component to support images from both product variants and the product itself. #### File: [ProductImage.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/components/ProductImage.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/patches/ProductImage.tsx.42c3b6.patch)) ```diff @@ -1,10 +1,13 @@ -import type {ProductVariantFragment} from 'storefrontapi.generated'; +import type { + ProductVariantFragment, + ProductFragment, +} from 'storefrontapi.generated'; import {Image} from '@shopify/hydrogen'; export function ProductImage({ image, }: { - image: ProductVariantFragment['image']; + image: ProductVariantFragment['image'] | ProductFragment['featuredImage']; }) { if (!image) { return
; ``` *** ## Step 6: Show a range of prices for combined listings in Product​Item Update `ProductItem.tsx` to show a range of prices for the combined listing parent product instead of the variant price. #### File: [ProductItem.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/components/ProductItem.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/patches/ProductItem.tsx.d3223b.patch)) ```diff @@ -6,6 +6,7 @@ import type { RecommendedProductFragment, } from 'storefrontapi.generated'; import {useVariantUrl} from '~/lib/variants'; +import {isCombinedListing} from '../lib/combined-listings'; export function ProductItem({ product, @@ -36,9 +37,17 @@ export function ProductItem({ /> )}

{product.title}

- - - + {isCombinedListing(product) ? ( + + + + + + ) : ( + + + + )} ); } ``` *** ## Step 7: (Optional) Redirect to the first variant If you want to redirect automatically to the first variant of a combined listing when the parent handle is selected, add a redirect utility that's called whenever the parent handle is requested. #### File: [redirect.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/lib/redirect.ts) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/patches/redirect.ts.6d9851.patch)) ```diff @@ -1,4 +1,6 @@ import {redirect} from 'react-router'; +import type {ProductFragment} from 'storefrontapi.generated'; +import {isCombinedListing} from './combined-listings'; export function redirectIfHandleIsLocalized( request: Request, @@ -21,3 +23,23 @@ export function redirectIfHandleIsLocalized( throw redirect(url.toString()); } } + +export function redirectIfCombinedListing( + request: Request, + product: ProductFragment, +) { + const url = new URL(request.url); + let shouldRedirect = false; + + if (isCombinedListing(product)) { + url.pathname = url.pathname.replace( + product.handle, + product.selectedOrFirstAvailableVariant?.product.handle ?? '', + ); + shouldRedirect = true; + } + + if (shouldRedirect) { + throw redirect(url.toString()); + } +} ``` *** ## Step 8: Filter combined listings from the all products page Add filtering to the all products catalog page to exclude combined listing parent products. #### File: [collections.all.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/routes/collections.all.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/patches/collections.all.tsx.38a40d.patch)) ```diff @@ -1,11 +1,13 @@ import type {Route} from './+types/collections.all'; -import { - useLoaderData, -} from 'react-router'; +import {useLoaderData} from 'react-router'; import {getPaginationVariables, Image, Money} from '@shopify/hydrogen'; import {PaginatedResourceSection} from '~/components/PaginatedResourceSection'; import {ProductItem} from '~/components/ProductItem'; import type {CollectionItemFragment} from 'storefrontapi.generated'; +import { + combinedListingsSettings, + maybeFilterOutCombinedListingsQuery, +} from '../lib/combined-listings'; export const meta: Route.MetaFunction = () => { return [{title: `Hydrogen | Products`}]; @@ -33,7 +35,12 @@ async function loadCriticalData({context, request}: Route.LoaderArgs) { const [{products}] = await Promise.all([ storefront.query(CATALOG_QUERY, { - variables: {...paginationVariables}, + variables: { + ...paginationVariables, + query: combinedListingsSettings.hideCombinedListingsFromProductList + ? maybeFilterOutCombinedListingsQuery + : '', + }, }), // Add other queries here, so that they are loaded in parallel ]); @@ -80,6 +87,7 @@ const COLLECTION_ITEM_FRAGMENT = `#graphql id handle title + tags featuredImage { id altText @@ -107,8 +115,9 @@ const CATALOG_QUERY = `#graphql $last: Int $startCursor: String $endCursor: String + $query: String ) @inContext(country: $country, language: $language) { - products(first: $first, last: $last, before: $startCursor, after: $endCursor) { + products(first: $first, last: $last, before: $startCursor, after: $endCursor, query: $query) { nodes { ...CollectionItem } ``` *** ## Step 9: Filter recommended products 1. Add the `tags` property to the items returned by the product query. 2. (Optional) Add the filtering query to the product query to exclude combined listings. #### File: [\_index.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/routes/_index.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/patches/_index.tsx.243e26.patch)) ## File ```diff @@ -11,6 +11,7 @@ import type { RecommendedProductsQuery, } from 'storefrontapi.generated'; import {ProductItem} from '~/components/ProductItem'; +import {maybeFilterOutCombinedListingsQuery} from '~/lib/combined-listings'; export const meta: Route.MetaFunction = () => { return [{title: 'Hydrogen | Home'}]; @@ -48,7 +49,11 @@ async function loadCriticalData({context}: Route.LoaderArgs) { */ function loadDeferredData({context}: Route.LoaderArgs) { const recommendedProducts = context.storefront - .query(RECOMMENDED_PRODUCTS_QUERY) + .query(RECOMMENDED_PRODUCTS_QUERY, { + variables: { + query: maybeFilterOutCombinedListingsQuery, + }, + }) .catch((error: Error) => { // Log query errors, but don't throw them so the page can still render console.error(error); @@ -104,11 +109,9 @@ function RecommendedProducts({ {(response) => (
- {response - ? response.products.nodes.map((product) => ( - - )) - : null} + {response?.products.nodes.map((product) => ( + + ))}
)}
@@ -151,7 +154,12 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql amount currencyCode } + maxVariantPrice { + amount + currencyCode + } } + tags featuredImage { id url @@ -160,9 +168,9 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql height } } - query RecommendedProducts ($country: CountryCode, $language: LanguageCode) + query RecommendedProducts ($country: CountryCode, $language: LanguageCode, $query: String) @inContext(country: $country, language: $language) { - products(first: 4, sortKey: UPDATED_AT, reverse: true) { + products(first: 4, sortKey: UPDATED_AT, reverse: true, query: $query) { nodes { ...RecommendedProduct } ``` *** ## Step 10: (Optional) Filter out combined listings from collections pages Since it's not possible to directly apply query filters when retrieving collection products, you can manually filter out combined listings after they're retrieved based on their tags. #### File: [collections.$handle.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/routes/collections.$handle.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/patches/collections.$handle.tsx.f062a9.patch)) ## File ```diff @@ -5,6 +5,10 @@ import {PaginatedResourceSection} from '~/components/PaginatedResourceSection'; import {redirectIfHandleIsLocalized} from '~/lib/redirect'; import {ProductItem} from '~/components/ProductItem'; import type {ProductItemFragment} from 'storefrontapi.generated'; +import { + combinedListingsSettings, + isCombinedListing, +} from '~/lib/combined-listings'; export const meta: Route.MetaFunction = ({data}) => { return [{title: `Hydrogen | ${data?.collection.title ?? ''} Collection`}]; @@ -68,12 +72,25 @@ function loadDeferredData({context}: Route.LoaderArgs) { export default function Collection() { const {collection} = useLoaderData(); + // Manually filter out combined listings from the collection products, because filtering + // would not work here. + const filteredCollectionProducts = { + ...collection.products, + nodes: collection.products.nodes.filter( + (product) => + !( + combinedListingsSettings.hideCombinedListingsFromProductList && + isCombinedListing(product) + ), + ), + }; + return (

{collection.title}

{collection.description}

- connection={collection.products} + connection={filteredCollectionProducts} resourcesClassName="products-grid" > {({node: product, index}) => ( @@ -105,6 +122,7 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql id handle title + tags featuredImage { id altText @@ -144,7 +162,7 @@ const COLLECTION_QUERY = `#graphql first: $first, last: $last, before: $startCursor, - after: $endCursor + after: $endCursor, ) { nodes { ...ProductItem ``` *** ## Step 11: Show price ranges on product pages 1. Display a range of prices for combined listings instead of the variant price. 2. Show the featured image of the combined listing parent product instead of the variant image. 3. (Optional) Redirect to the first variant of a combined listing when the handle is requested. #### File: [products.$handle.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/routes/products.$handle.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/patches/products.$handle.tsx.6f2e82.patch)) ## File ```diff @@ -14,7 +14,14 @@ import { import {ProductPrice} from '~/components/ProductPrice'; import {ProductImage} from '~/components/ProductImage'; import {ProductForm} from '~/components/ProductForm'; -import {redirectIfHandleIsLocalized} from '~/lib/redirect'; +import { + redirectIfCombinedListing, + redirectIfHandleIsLocalized, +} from '~/lib/redirect'; +import { + isCombinedListing, + combinedListingsSettings, +} from '../lib/combined-listings'; export const meta: Route.MetaFunction = ({data}) => { return [ @@ -66,6 +73,10 @@ async function loadCriticalData({ // The API handle might be localized, so redirect to the localized handle redirectIfHandleIsLocalized(request, {handle, data: product}); + if (combinedListingsSettings.redirectToFirstVariant) { + redirectIfCombinedListing(request, product); + } + return { product, }; @@ -85,6 +96,7 @@ function loadDeferredData({context, params}: Route.LoaderArgs) { export default function Product() { const {product} = useLoaderData(); + const combinedListing = isCombinedListing(product); // Optimistically selects a variant with given available variant information const selectedVariant = useOptimisticVariant( @@ -94,7 +106,9 @@ export default function Product() { // Sets the search param to the selected variant without navigation // only when no search params are set in the url - useSelectedOptionInUrlParam(selectedVariant.selectedOptions); + useSelectedOptionInUrlParam( + combinedListing ? [] : selectedVariant.selectedOptions, + ); // Get the product options array const productOptions = getProductOptions({ @@ -102,21 +116,41 @@ export default function Product() { selectedOrFirstAvailableVariant: selectedVariant, }); - const {title, descriptionHtml} = product; + const {descriptionHtml, title} = product; + + const productImage = combinedListing + ? (product.featuredImage ?? selectedVariant?.image) + : selectedVariant?.image; return (
- +

{title}

- + {combinedListing ? ( +
+
+ + From + + + + To + + +
+
+ ) : ( + + )}


@@ -193,6 +227,22 @@ const PRODUCT_FRAGMENT = `#graphql description encodedVariantExistence encodedVariantAvailability + tags + featuredImage { + id + url + altText + } + priceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } options { name optionValues { ``` *** ## Step 12: Style the price range display Add a class to the product item to show a range of prices for combined listings. #### File: [app.css](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/styles/app.css) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/combined-listings/patches/app.css.881a99.patch)) ```diff @@ -418,6 +418,11 @@ button.reset:hover:not(:has(> *)) { width: 100%; } +.product-item .combined-listing-price { + display: flex; + grid-gap: 0.5rem; +} + /* * -------------------------------------------------- * routes/products.$handle.tsx ``` *** ## Next steps * Test your implementation by going to your store and searching for a combined listing. Make sure that the combined listing's details appear in the search results and on the product page. * (Optional) [Place a test order](https://help.shopify.com/en/manual/checkout-settings/test-orders) to see how orders for combined listings appear in your Shopify admin. *** * [Requirements](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#requirements) * [Ingredients](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#ingredients) * [Step 1: Set up the Combined Listings app](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-1-set-up-the-combined-listings-app) * [Step 2: Configure combined listings behavior](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-2-configure-combined-listings-behavior) * [Step 3: Add combined listings utilities](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-3-add-combined-listings-utilities) * [Step 4: Hide the cart button for combined listing parent products](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-4-hide-the-cart-button-for-combined-listing-parent-products) * [Step 5: Support product and variant images](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-5-support-product-and-variant-images) * [Step 6: Show a range of prices for combined listings in Product​Item](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-6-show-a-range-of-prices-for-combined-listings-in-productitem) * [Step 7: (Optional) Redirect to the first variant](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-7-optional-redirect-to-the-first-variant) * [Step 8: Filter combined listings from the all products page](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-8-filter-combined-listings-from-the-all-products-page) * [Step 9: Filter recommended products](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-9-filter-recommended-products) * [Step 10: (Optional) Filter out combined listings from collections pages](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-10-optional-filter-out-combined-listings-from-collections-pages) * [Step 11: Show price ranges on product pages](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-11-show-price-ranges-on-product-pages) * [Step 12: Style the price range display](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#step-12-style-the-price-range-display) * [Next steps](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/combined-listings.md#next-steps)