---
title: Custom Product Card Rendering with Releva
description: "# Use Your Theme's Product Card with Releva ## What This Does Releva picks **which products** to recommend. Your Shopify theme controls **how they look**. You write standard Shopify Liquid — tags, prices, metafields, third-party app integrations all work out…"
url: "https://releva.ai/documentation/shopify/custom-product-card-rendering-with-releva/"
updated: "2026-04-20"
category: User Guide / Getting Started / Shopify
---

# Custom Product Card Rendering with Releva

# Use Your Theme's Product Card with Releva

## What This Does

Releva picks **which products** to recommend. Your Shopify theme controls **how they look**.

You write standard Shopify Liquid — tags, prices, metafields, third-party app integrations all work out of the box because Shopify renders the HTML server-side.

## Setup

### 1. Create a Section File

1. Go to **Shopify Admin** > **Online Store** > **Themes**.
2. Click **"..."** > **Edit code**.
3. In the **Sections** folder, click **"Add a new file"**.
4. Name it (e.g.) `releva-recs.liquid`.
5. Paste one of the examples from the end of this guide.
6. Click **Save**.

### 2. Configure the Releva Block

1. Go to **Online Store** > **Themes** > **Customize**.
2. Add or find the **Releva Product Block**.
3. Enter the **Product Block Token** from your Releva dashboard.
4. Check **"Use my theme's product card"**.
5. In **Product Card Section ID**, enter the section filename without `.liquid` (e.g. `releva-recs`).
6. Save.

That's it. Releva handles everything else — writing the product list, fetching the section, and tracking clicks.

## What Every Section Must Contain

**Three things to remember**:

1. **First line** — **replace** `YOUR-SECTION-ID-HERE` **with your section filename.** For `releva-recs.liquid`, use `_rlv_h_releva-recs`.
2. `{% if product.title != blank %}` **guard — skips products that are unpublished or hidden from the current market.**
3. `{% schema %}` **tag — required by Shopify on every section file**.

```
{% assign rlv_handles = cart.attributes['_rlv_h_YOUR-SECTION-ID-HERE'] | default: '' | split: ',' %}

{% for handle in rlv_handles %}
  {% assign product = all_products[handle] %}
  {% if product.title != blank %}
    <!-- Your product card markup goes here -->
    <a href="{{ product.url }}">{{ product.title }} — {{ product.price | money }}</a>
  {% endif %}
{% endfor %}

{% schema %}
{ "name": "Releva Recommendations", "settings": [] }
{% endschema %}
```

Inside the loop, `product` is the full Shopify product object — render it however you want.

## Available Product Fields

| Liquid | Description |
| --- | --- |
| `{{ product.title }}` | Product title |
| `{{ product.price \| money }}` | Current price, formatted |
| `{{ product.compare_at_price \| money }}` | Compare-at price |
| `{{ product.featured_image \| image_url: width: 300 }}` | Product image |
| `{{ product.url }}` | Product URL |
| `{{ product.tags }}` | Product tags (for badges, promos) |
| `{{ product.vendor }}` | Product vendor |
| `{{ product.type }}` | Product type |
| `{{ product.variants }}` | All variants |
| `{{ product.metafields }}` | Custom metafields |
| `{{ product.available }}` | Availability |
| `{{ product.description }}` | Product description |

## Multiple Blocks on One Page

Each block needs its **own** section file. Two blocks pointing to the same file will collide.

If two blocks should look identical, create two section files and have both render a shared snippet:

```
{% assign rlv_handles = cart.attributes['_rlv_h_YOUR-SECTION-ID-HERE'] | default: '' | split: ',' %}
{% for handle in rlv_handles %}
  {% assign product = all_products[handle] %}
  {% if product.title != blank %}
    {% render 'my-shared-card', product: product %}
  {% endif %}
{% endfor %}
{% schema %}{ "name": "Releva Trending", "settings": [] }{% endschema %}
```

## Good to Know

- **20-product cap** — Shopify limits `all_products[handle]` to 20 lookups per page. Releva respects this automatically.
- **Click tracking is automatic** — any `<a href="{{ product.url }}">` link in your section gets tracking applied. No extra markup needed.
- **Theme editor preview** — the block will appear empty in the theme editor. Test on the live storefront.
- **Markets / multi-language** — prices and translations match the market the visitor is browsing.

## Troubleshooting

| Symptom | Fix |
| --- | --- |
| Block is empty on the storefront | Check that the cart attribute key in your section matches the filename. For `releva-recs.liquid`, use `_rlv_h_releva-recs`. |
| Empty wrapper, no cards | The `{% assign %}` line is missing or the attribute key is wrong. |
| Some cards missing | Those products are unpublished or hidden from the current market — this is expected. |
| No checkbox or section ID field in settings | The Releva app needs to be updated. |
| Cards show but clicks aren't tracked | Make sure your card uses `<a href="{{ product.url }}">`. |

---

# Section Examples

## Carousel / slider

A Swiper.js carousel with arrow buttons and responsive breakpoints.

```
{% assign rlv_handles = cart.attributes['_rlv_h_YOUR-SECTION-ID-HERE'] | default: '' | split: ',' %}

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">

<style>
  .releva-slider-wrap {
    position: relative;
    max-width: 1240px;
    margin: 0 auto;
    padding: 20px 50px;
  }
  .releva-slider-wrap .swiper { overflow: hidden; }
  .releva-slider-wrap .swiper-slide { height: auto; display: flex; }
  .releva-slider-wrap .releva-card { width: 100%; text-align: center; }
  .releva-slider-wrap .releva-card a { display: block; text-decoration: none; color: inherit; }
  .releva-slider-wrap .releva-card .img-wrap { position: relative; overflow: hidden; }
  .releva-slider-wrap .releva-card img { width: 100%; height: auto; display: block; }
  .releva-slider-wrap .releva-card h3 { font-size: 14px; margin: 10px 0 4px; font-weight: 600; }
  .releva-slider-wrap .releva-card .price { font-weight: bold; }
  .releva-slider-wrap .releva-card .price s { color: #999; font-weight: normal; margin-right: 6px; }

  .releva-slider-wrap .sale-badge {
    position: absolute;
    top: 8px;
    left: 8px;
    background: #e53935;
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    padding: 4px 8px;
    border-radius: 4px;
    z-index: 2;
    letter-spacing: 0.5px;
  }

  .releva-slider-wrap .swiper-button-prev,
  .releva-slider-wrap .swiper-button-next {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 40px;
    background: rgba(255, 255, 255, 0.9);
    border-radius: 50%;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    cursor: pointer;
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0;
  }
  .releva-slider-wrap .swiper-button-prev { left: 5px; }
  .releva-slider-wrap .swiper-button-next { right: 5px; }

  .releva-slider-wrap .swiper-button-prev::after,
  .releva-slider-wrap .swiper-button-next::after {
    content: '';
    display: block;
    width: 10px;
    height: 10px;
    border-top: 2px solid #222;
    border-right: 2px solid #222;
  }
  .releva-slider-wrap .swiper-button-prev::after {
    transform: rotate(-135deg);
    margin-left: 4px;
  }
  .releva-slider-wrap .swiper-button-next::after {
    transform: rotate(45deg);
    margin-right: 4px;
  }

  .releva-slider-wrap .swiper-button-disabled {
    opacity: 0.35;
    cursor: default;
  }
</style>

<div class="releva-slider-wrap">
  <div class="swiper releva-swiper">
    <div class="swiper-wrapper">
      {% for handle in rlv_handles %}
        {% assign product = all_products[handle] %}
        {% if product.title != blank %}
          <div class="swiper-slide">
            <div class="releva-card">
              <a href="{{ product.url }}">
                <div class="img-wrap">
                  {% if product.compare_at_price > product.price %}
                    {% assign discount = product.compare_at_price | minus: product.price | times: 100 | divided_by: product.compare_at_price %}
                    <span class="sale-badge">-{{ discount }}%</span>
                  {% endif %}
                  {% if product.featured_image %}
                    {% assign img = product.featured_image %}
                    <img
                      src="{{ img | image_url: width: 400 }}"
                      alt="{{ product.title | escape }}"
                      loading="lazy"
                      width="400"
                      height="{{ 400 | divided_by: img.aspect_ratio | round }}"
                    >
                  {% endif %}
                </div>
                <h3>{{ product.title }}</h3>
                <div class="price">
                  {% if product.compare_at_price > product.price %}
                    <s>{{ product.compare_at_price | money }}</s>
                  {% endif %}
                  {{ product.price | money }}
                </div>
              </a>
            </div>
          </div>
        {% endif %}
      {% endfor %}
    </div>
  </div>
  <div class="swiper-button-prev"></div>
  <div class="swiper-button-next"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>
<script>
  (function () {
    function initRelevaSwipers() {
      if (typeof Swiper === 'undefined') { setTimeout(initRelevaSwipers, 100); return; }
      document.querySelectorAll('.releva-slider-wrap').forEach(function (wrap) {
        var el = wrap.querySelector('.releva-swiper');
        if (!el || el.dataset.rlvSwiperInit) return;
        el.dataset.rlvSwiperInit = '1';
        new Swiper(el, {
          slidesPerView: 2,
          spaceBetween: 15,
          navigation: {
            prevEl: wrap.querySelector('.swiper-button-prev'),
            nextEl: wrap.querySelector('.swiper-button-next')
          },
          breakpoints: {
            640:  { slidesPerView: 3 },
            1024: { slidesPerView: 4 },
            1200: { slidesPerView: 5 }
          }
        });
      });
    }
    window.addEventListener('releva:recommendations', initRelevaSwipers);
    if (document.readyState !== 'loading') initRelevaSwipers();
    else document.addEventListener('DOMContentLoaded', initRelevaSwipers);
  })();
</script>

{% schema %}
{ "name": "Releva Recommendations", "settings": [] }
{% endschema %}
```

To avoid theme-check warnings, you can host Swiper locally: download `swiper-bundle.min.css` and `swiper-bundle.min.js` from the CDN, upload to your theme's **Assets** folder, then replace the CDN URLs with `{{ 'swiper-bundle.min.css' | asset_url | stylesheet_tag }}` and `<script src="{{ 'swiper-bundle.min.js' | asset_url }}" defer></script>`.

## Reusing your existing theme card

If your theme already has a product card snippet (e.g. `snippets/product-card.liquid`), just wrap it in the loop:

```
{% assign rlv_handles = cart.attributes['_rlv_h_YOUR-SECTION-ID-HERE'] | default: '' | split: ',' %}

<div class="my-grid">
  {% for handle in rlv_handles %}
    {% assign product = all_products[handle] %}
    {% if product.title != blank %}
      {% render 'product-card', product: product %}
    {% endif %}
  {% endfor %}
</div>

{% schema %}
{ "name": "Releva Recommendations", "settings": [] }
{% endschema %}
```
