View Categories

Custom Product Card Rendering with Releva

5 min read

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 linereplace 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 #

LiquidDescription
{{ 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 #

SymptomFix
Block is empty on the storefrontCheck that the cart attribute key in your section matches the filename. For releva-recs.liquid, use _rlv_h_releva-recs.
Empty wrapper, no cardsThe {% assign %} line is missing or the attribute key is wrong.
Some cards missingThose products are unpublished or hidden from the current market — this is expected.
No checkbox or section ID field in settingsThe Releva app needs to be updated.
Cards show but clicks aren’t trackedMake sure your card uses <a href="{{ product.url }}">.

Section Examples #

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 %}