DeenruvDeenruv
Building a Storefront

Listing Products

Learn how to list products in collections, implement product search, and use faceted search for filtering in your storefront.

Products are listed when:

  • Displaying the contents of a collection
  • Displaying search results

In Deenruv, we usually use the search query for both of these. The reason is that the search query is optimized for high performance, because it is backed by a dedicated search index. Other queries such as products or Collection.productVariants can also be used to fetch a list of products, but they need to perform much more complex database queries, and are therefore slower.

Listing products in a collection

Following on from the navigation example, let's assume that a customer has clicked on a collection item from the menu, and we want to display the products in that collection.

Typically, we will know the slug of the selected collection, so we can use the collection query to fetch the details of this collection:

query GetCollection($slug: String!) {
    collection(slug: $slug) {
        id
        name
        slug
        description
        featuredAsset {
            id
            preview
        }
    }
}
{
    "slug": "electronics"
}
{
    "data": {
        "collection": {
            "id": "2",
            "name": "Electronics",
            "slug": "electronics",
            "description": "",
            "featuredAsset": {
                "id": "16",
                "preview": "https://demo.deenruv.com/assets/preview/5b/jakob-owens-274337-unsplash__preview.jpg"
            }
        }
    }
}

The collection data can be used to render the page header.

Next, we can use the search query to fetch the products in the collection:

query GetCollectionProducts($slug: String!, $skip: Int, $take: Int) {
  search(
    input: {
      collectionSlug: $slug,
      groupByProduct: true,
      skip: $skip,
      take: $take }
  ) {
    totalItems
    items {
      productName
      slug
      productAsset {
        id
        preview
      }
      priceWithTax {
        ... on SinglePrice {
          value
        }
        ... on PriceRange {
          min
          max
        }
      }
      currencyCode
    }
  }
}
{
    "slug": "electronics",
    "skip": 0,
    "take": 10
}

(the following data has been truncated for brevity)

{
    "data": {
        "search": {
            "totalItems": 20,
            "items": [
                {
                    "productName": "Laptop",
                    "slug": "laptop",
                    "productAsset": {
                        "id": "1",
                        "preview": "https://demo.deenruv.com/assets/preview/71/derick-david-409858-unsplash__preview.jpg"
                    },
                    "priceWithTax": {
                        "min": 155880,
                        "max": 275880
                    },
                    "currencyCode": "USD"
                },
                {
                    "productName": "Tablet",
                    "slug": "tablet",
                    "productAsset": {
                        "id": "2",
                        "preview": "https://demo.deenruv.com/assets/preview/b8/kelly-sikkema-685291-unsplash__preview.jpg"
                    },
                    "priceWithTax": {
                        "min": 39480,
                        "max": 53400
                    },
                    "currencyCode": "USD"
                }
            ]
        }
    }
}

The key thing to note here is that we are using the collectionSlug input to the search query. This ensures that the results all belong to the selected collection.

The search query can also be used to perform a full-text search of the products in the catalog by passing the term input:

query SearchProducts($term: String!, $skip: Int, $take: Int) {
  search(
    input: {
      term: $term,
      groupByProduct: true,
      skip: $skip,
      take: $take }
  ) {
    totalItems
    items {
      productName
      slug
      productAsset {
        id
        preview
      }
      priceWithTax {
        ... on SinglePrice {
          value
        }
        ... on PriceRange {
          min
          max
        }
      }
      currencyCode
    }
  }
}
{
    "term": "camera",
    "skip": 0,
    "take": 10
}

(the following data has been truncated for brevity)

{
    "data": {
        "search": {
            "totalItems": 8,
            "items": [
                {
                    "productName": "Instant Camera",
                    "slug": "instant-camera",
                    "productAsset": {
                        "id": "12",
                        "preview": "https://demo.deenruv.com/assets/preview/b5/eniko-kis-663725-unsplash__preview.jpg"
                    },
                    "priceWithTax": {
                        "min": 20999,
                        "max": 20999
                    },
                    "currencyCode": "USD"
                },
                {
                    "productName": "Camera Lens",
                    "slug": "camera-lens",
                    "productAsset": {
                        "id": "13",
                        "preview": "https://demo.deenruv.com/assets/preview/9b/brandi-redd-104140-unsplash__preview.jpg"
                    },
                    "priceWithTax": {
                        "min": 12480,
                        "max": 12480
                    },
                    "currencyCode": "USD"
                }
            ]
        }
    }
}

You can also limit the full-text search to a specific collection by passing the collectionSlug or collectionId input.

The search query can also be used to perform faceted search. This is a powerful feature which allows customers to filter the results according to the facet values assigned to the products & variants.

By using the facetValues field, the search query will return a list of all the facet values which are present in the result set. This can be used to render a list of checkboxes or other UI elements which allow the customer to filter the results.

query SearchProducts($term: String!, $skip: Int, $take: Int) {
  search(
    input: {
      term: $term,
      groupByProduct: true,
      skip: $skip,
      take: $take }
  ) {
    totalItems
    facetValues {
      count
      facetValue {
        id
        name
        facet {
          id
          name
        }
      }
    }
    items {
      productName
      slug
      productAsset {
        id
        preview
      }
      priceWithTax {
        ... on SinglePrice {
          value
        }
        ... on PriceRange {
          min
          max
        }
      }
      currencyCode
    }
  }
}
{
    "term": "camera",
    "skip": 0,
    "take": 10
}

(the following data has been truncated for brevity)

{
    "data": {
        "search": {
            "totalItems": 8,
            "facetValues": [
                {
                    "facetValue": {
                        "id": "1",
                        "name": "Electronics",
                        "facet": {
                            "id": "1",
                            "name": "category"
                        }
                    },
                    "count": 8
                },
                {
                    "facetValue": {
                        "id": "9",
                        "name": "Photo",
                        "facet": {
                            "id": "1",
                            "name": "category"
                        }
                    },
                    "count": 8
                },
                {
                    "facetValue": {
                        "id": "10",
                        "name": "Polaroid",
                        "facet": {
                            "id": "2",
                            "name": "brand"
                        }
                    },
                    "count": 1
                },
                {
                    "facetValue": {
                        "id": "11",
                        "name": "Nikkon",
                        "facet": {
                            "id": "2",
                            "name": "brand"
                        }
                    },
                    "count": 2
                }
            ],
            "items": [
                {
                    "productName": "Instant Camera",
                    "slug": "instant-camera",
                    "productAsset": {
                        "id": "12",
                        "preview": "https://demo.deenruv.com/assets/preview/b5/eniko-kis-663725-unsplash__preview.jpg"
                    },
                    "priceWithTax": {
                        "min": 20999,
                        "max": 20999
                    },
                    "currencyCode": "USD"
                },
                {
                    "productName": "Camera Lens",
                    "slug": "camera-lens",
                    "productAsset": {
                        "id": "13",
                        "preview": "https://demo.deenruv.com/assets/preview/9b/brandi-redd-104140-unsplash__preview.jpg"
                    },
                    "priceWithTax": {
                        "min": 12480,
                        "max": 12480
                    },
                    "currencyCode": "USD"
                },
                {
                    "productName": "Vintage Folding Camera",
                    "slug": "vintage-folding-camera",
                    "productAsset": {
                        "id": "14",
                        "preview": "https://demo.deenruv.com/assets/preview/3c/jonathan-talbert-697262-unsplash__preview.jpg"
                    },
                    "priceWithTax": {
                        "min": 642000,
                        "max": 642000
                    },
                    "currencyCode": "USD"
                }
            ]
        }
    }
}

These facet values can then be used to filter the results by passing them to the facetValueFilters input.

For example, let's filter the results to only include products which have the "Nikkon" brand. Based on our last request we know that there should be 2 such products, and that the facetValue.id for the "Nikkon" brand is 11.

{
    "facetValue": {
        "id": "11",
        "name": "Nikkon",
        "facet": {
            "id": "2",
            "name": "brand"
        }
    },
    "count": 2
}

Here's how we can use this information to filter the results:

In the next example, rather than passing each individual variable (skip, take, term) as a separate argument, we are passing the entire SearchInput object as a variable. This allows us more flexibility in how we use the query, as we can easily add or remove properties from the input object without having to change the query itself.

query SearchProducts($input: SearchInput!) {
  search(input: $input) {
    totalItems
    facetValues {
      count
      facetValue {
        id
        name
        facet {
          id
          name
        }
      }
    }
    items {
      productName
      slug
      productAsset {
        id
        preview
      }
      priceWithTax {
        ... on SinglePrice {
          value
        }
        ... on PriceRange {
          min
          max
        }
      }
      currencyCode
    }
  }
}
{
    "input": {
        "term": "camera",
        "skip": 0,
        "take": 10,
        "groupByProduct": true,
        "facetValueFilters": [{ "and": "11" }]
    }
}
{
    "data": {
        "search": {
            "totalItems": 2,
            "facetValues": [
                {
                    "facetValue": {
                        "id": "1",
                        "name": "Electronics",
                        "facet": {
                            "id": "1",
                            "name": "category"
                        }
                    },
                    "count": 2
                },
                {
                    "facetValue": {
                        "id": "9",
                        "name": "Photo",
                        "facet": {
                            "id": "1",
                            "name": "category"
                        }
                    },
                    "count": 2
                },
                {
                    "facetValue": {
                        "id": "11",
                        "name": "Nikkon",
                        "facet": {
                            "id": "2",
                            "name": "brand"
                        }
                    },
                    "count": 2
                }
            ],
            "items": [
                {
                    "productName": "Camera Lens",
                    "slug": "camera-lens",
                    "productAsset": {
                        "id": "13",
                        "preview": "https://demo.deenruv.com/assets/preview/9b/brandi-redd-104140-unsplash__preview.jpg"
                    },
                    "priceWithTax": {
                        "value": 12480
                    },
                    "currencyCode": "USD"
                },
                {
                    "productName": "Nikkormat SLR Camera",
                    "slug": "nikkormat-slr-camera",
                    "productAsset": {
                        "id": "18",
                        "preview": "https://demo.deenruv.com/assets/preview/95/chuttersnap-324234-unsplash__preview.jpg"
                    },
                    "priceWithTax": {
                        "value": 73800
                    },
                    "currencyCode": "USD"
                }
            ]
        }
    }
}

The facetValueFilters input can be used to specify multiple filters, combining each with either and or or.

For example, to filter by both the "Camera" and "Nikkon" facet values, we would use:

{
    "facetValueFilters": [{ "and": "9" }, { "and": "11" }]
}

To filter by "Nikkon" or "Sony", we would use:

{
    "facetValueFilters": [{ "or": ["11", "15"] }]
}

Listing custom product data

If you have defined custom fields on the Product or ProductVariant entity, you might want to include these in the search results. With the DefaultSearchPlugin this is not possible, as this plugin is designed to be a minimal and simple search implementation.

Instead, you can use the ElasticsearchPlugin which provides advanced features which allow you to index custom data. The Elasticsearch plugin is designed as a drop-in replacement for the DefaultSearchPlugin, so you can simply swap out the plugins in your deenruv-config.ts file.

On this page