# Product lists from Shopify

> Best sellers, newest products, related and complementary products, a custom query, one collection or the products in a metafield, filled straight from Shopify with one attribute on smootify-product.

Status: Ready (Smootify 2.0)  
URL: https://www.smootify.io/docs/astro/build/product-lists  
Updated: 2026-10-21

Some lists cannot come from the content collection: best sellers need Shopify’s sales history, and related products are computed by Shopify. For these, a `smootify-product` fills itself from Shopify. You write one card, give it a `data-id` value, and Smootify repeats it once per product. There is no loop over entries.

> [!NOTE]
> Before you start, Smootify for Astro must be installed (see [Install](https://www.smootify.io/docs/astro/start/install)). These lists read Shopify directly, so the products do not need to be in the content collection. For a list that the build can answer, such as one collection, you can also map the entries yourself: the cards are then in the HTML before Smootify loads (see [Content](https://www.smootify.io/docs/astro/start/content)).

## How it works

1. Write an element for the grid, or a slider (see below).
2. Inside it, write a `smootify-product` and design it as a [product card](https://www.smootify.io/docs/astro/build/product-cards), inside an element with the class `sm-dyn-item`.
3. Set its `data-id` to one of the values below, and `limit` to the number of products.

```html
<div>
  <h2>Best sellers</h2>
  <div>
    <div class="sm-dyn-item">
      <smootify-product data-id="best-seller" limit="8">
        <a product="url" href="#">Product name</a>
        <smootify-price><span data-prop="price">$0.00</span></smootify-price>
      </smootify-product>
    </div>
  </div>
</div>
```

When the products arrive, the card takes the first one, and Smootify places a copy of the card after it for each of the others. In a slider, each product gets its own slide (see below).

**How many.** Without `limit`, a list shows 3 products. Related and complementary products are the exception: without `limit` they show all the products Shopify returns. A metafield list always shows all the products picked in it.

**When the list is empty.** If Shopify returns no product, the card is removed. The heading you put next to it stays, unless you hide it: see “Hide a section when its list is empty” below.

## The lists

Each `data-id` fills the card from a different place:

| `data-id` | What it shows | Also needs |
|---|---|---|
| `best-seller` | the best sellers, over the whole life of the store | nothing |
| `created-at` | the newest products, by their Shopify creation date | nothing |
| `purchasable` | products that can be bought now | nothing |
| `custom-query` | products that match a Shopify search | `data-query`, optionally `data-sort` |
| `specific-collection` | the products of one collection, in its Shopify order | `data-collection-handle` |
| `related` | products Shopify finds related to a product | `data-parent-id` |
| `complementary` | products that go with a product | `data-parent-id` |
| `metafields.custom.pairs_with` | the products picked in a metafield of a product | `data-parent-id` |
| `metafields.custom.my_products` | the products picked in a metafield of the customer | `data-parent-id="customer"` |

### Best sellers, newest, available

`best-seller`, `created-at` and `purchasable` need nothing else. `best-seller` uses the store’s sales history, which the loaders do not have, and `created-at` lists the newest products first.

### A custom query

`data-id="custom-query"` lists the products that match `data-query`, written in Shopify’s [product search syntax](https://shopify.dev/docs/api/storefront/latest/queries/products#arguments-query):

- `tag:gift` for a tag;
- `vendor:Acme` for a vendor;
- `product_type:Shoes` for a product type;
- `variants.price:<50` for a price range.

`data-sort` sets the order with one of Shopify’s [product sort keys](https://shopify.dev/docs/api/storefront/latest/enums/ProductSortKeys), such as `TITLE`, `PRICE` or `CREATED_AT`. The order runs one way only: `PRICE` is the lowest price first.

### One collection

`data-id="specific-collection"` with `data-collection-handle="summer"` lists the products of that collection, in the order set in Shopify. It is useful where the page is not the collection’s own page, such as the home page.

In a list of your collections, set `data-collection-handle` to the entry’s `id` (its handle): each collection then shows its own products. A collection entry loaded with `withProducts` also carries its products, which Astro can render itself: see [Content](https://www.smootify.io/docs/astro/start/content).

### Related and complementary products

On the product page, `data-id="related"` or `data-id="complementary"` lists products for the product in `data-parent-id`. Set `data-parent-id` to the entry’s `shopifyId`.

```astro
---
const p = entry.data;
---
<div condition="has-related">
  <h2>You may also like</h2>
  <div slider-track>
    <div class="sm-dyn-item">
      <smootify-product data-id="related" data-parent-id={p.shopifyId} limit="6"><a product="url" href="#">Product name</a></smootify-product>
    </div>
  </div>
</div>
```

- Shopify computes related products by itself.
- Complementary products are the ones you pick for each product in Shopify’s **Search & Discovery** app. If none are set there, the list stays empty.
- Without `data-parent-id`, the element is removed.

For the same lists in the cart, based on what the visitor added, see [Cart upsells](https://www.smootify.io/docs/astro/build/cart-upsells).

### Products picked in a metafield

`data-id="metafields.custom.pairs_with"` lists the products of the product metafield `custom.pairs_with` of the product in `data-parent-id`. Write the namespace and the key of your own metafield after `metafields.`.

- The metafield must be of type product reference, or a list of product references. Any other type gives no products.
- Its definition in Shopify needs **Storefront access**.
- `data-repeat="2"` repeats the list twice, which helps a slider that has only a few products.

The merchant then picks the products for each product in Shopify, such as “Complete the look”. To show the referenced products without prices or add to cart, the `metafield` attribute is lighter: see [Metafields on the page](https://www.smootify.io/docs/astro/build/metafields).

### Alternatives when a variant is sold out

Instead of a dead end, the product page can offer other products while the chosen variant cannot be bought. It takes only attributes on the page’s `smootify-product`: nothing goes into the content collection.

1. In Shopify, create a **variant** metafield of type **List of products**, such as `custom.alternatives`, with **Storefront access**. On each variant that needs them, pick its alternatives: a sold-out size can point to similar products.
2. Inside the page’s `smootify-product`, write an element with `condition="out-of-stock"`. It shows only while the chosen variant cannot be bought.
3. Inside it, write an element with `if-variant-metafield="alternatives"` and put your heading in it. It shows only when the chosen variant has alternatives.
4. Inside that element, write an `<a>` with `variant-metafield="alternatives"`, holding an `<img reference="image">` and an element with `reference="title"`. It repeats once per alternative.

```astro
---
const p = entry.data;
---
<smootify-product data-id={p.shopifyId}>
  <div condition="out-of-stock">
    <div if-variant-metafield="alternatives">
      <h2>Sold out. You may like these instead</h2>
      <a href="#" variant-metafield="alternatives">
        <img reference="image">
        <div reference="title">Product</div>
      </a>
    </div>
  </div>
</smootify-product>
```

Everything follows the chosen variant: the block shows for a sold-out size that has alternatives, changes with the size, and hides when the visitor picks a size in stock. More about references and `if-variant-metafield`: [Metafields on the page](https://www.smootify.io/docs/astro/build/metafields).

These links show the image and the title of each alternative. For cards with a price and an add-to-cart button, use a **product** metafield with a `smootify-product` list, as above, inside the same `condition="out-of-stock"` block: the alternatives are then the same for every variant, and `if-metafield="alternatives"` on the element around the heading hides it when the product has none.

### Products picked for each customer

With `data-parent-id="customer"`, the same `data-id="metafields.<namespace>.<key>"` lists the products of a metafield of the logged-in customer: a list the merchant curates for each customer, such as their usual order or the catalog of a wholesale buyer.

```html
<smootify-product data-id="metafields.custom.my_products" data-parent-id="customer">
  <div product="title">Product name</div>
  <smootify-price><span data-prop="price">$0.00</span></smootify-price>
</smootify-product>
```

- The metafield is a customer metafield, of type product reference or a list of product references. Create it in Shopify under **Settings › Custom data › Customers**, with its **Customer accounts** access set to **Read**, then pick the products on each customer.
- Customer accounts must be set up on the site: see [Customer accounts](https://www.smootify.io/docs/astro/build/account).
- For a visitor who is not logged in, or a customer with no products in the metafield, the element is removed.
- `data-repeat` works as for a product metafield. While the list has products, the page’s `body` gets the class `has-customer-` followed by the namespace and key, such as `has-customer-custom-my_products`.

## Lists in a slider

Write the card inside a `[slider-track]`, as in the related products example above. Each product after the first goes into its own slide: Smootify places a copy of the card’s `sm-dyn-item` for each of them. A card that hides, such as one with `hide-if-product-in-cart` whose product is in the cart, hides its whole slide, so the slider leaves no gap.

**No placeholder slides.** Astro needs none: see [placeholder cards](https://www.smootify.io/docs/astro/build/product-cards).

**The arrows and dots are yours.** The slider’s parts are written as in [Cards in a slider](https://www.smootify.io/docs/astro/build/product-cards#cards-in-a-slider), and a small `<script>` in the component keeps them in step with the track. The script disables the arrows at the ends and builds the dots again when the slides change.

## Hide a section when its list is empty

While a list has products, the page’s `body` gets the class `has-` followed by the list’s value: `has-best-seller`, `has-related`, and so on. For a metafield list, the class is `has-` with the namespace and key, such as `has-custom-pairs_with`.

For related and complementary products, two conditions do the work: an element with `condition="has-related"` or `condition="has-complementary"` shows only while that list has products. Put it on the section around the heading and the list, as in the example above. For the other lists, the `body` class can be used in CSS: see [CSS](https://www.smootify.io/docs/astro/developers/css).

## Other lists

- The visitor’s wishlist: see [Wishlist](https://www.smootify.io/docs/astro/features/wishlist).
- Recently viewed products: see [Recently viewed](https://www.smootify.io/docs/astro/features/last-viewed).
- Filtered lists the visitor can change: see [Filters, sorting and pagination](https://www.smootify.io/docs/astro/build/filters).

## Before you publish

> [!STUCK]
> - **Only 3 products show.** That is the default. Add `limit` with the number you want.
> - **Related or complementary products disappear.** `data-parent-id` is missing or empty. Set it to the entry's `shopifyId`.
> - **Complementary products never show.** No complementary products are set for that product in Shopify's Search & Discovery app.
> - **The metafield list is empty.** The metafield is not a product reference, or its definition has no Storefront access.
> - **The customer's list never shows.** The visitor is not logged in, the metafield is not a customer metafield, or its Customer accounts access is off.
> - **The heading stays when the list is empty.** Put `condition="has-related"` or `condition="has-complementary"` on the section, or use the `body` class for the other lists.
