# Son görüntülenen ürünler: ücretsiz Shopify bölümü

> Müşterilere son baktıkları ürünleri, en yenisi başta gösterin. Handle’lar müşterinin tarayıcısında kalır; fiyat ve stok mağazanızdan anlık gelir. Uygulama yok.

Son görüntülenen ürünler, her müşteriye son baktığı ürünleri en yeniden başlayarak gösteren ücretsiz bir Shopify bölümüdür. Ürün sayfasında, görüntülenen ürünün handle’ını müşterinin kendi tarayıcısına (localStorage) kaydeder; bölümü hangi sayfaya eklerseniz ekleyin bu handle’ları okur ve her ürünün güncel adını, görselini, fiyatını ve stok durumunu mağazanızdan çeker. Böylece stoğu tükenen ya da indirime giren bir ürün güncel haliyle görünür. Hiçbir veri bir uygulamaya ya da izleme hizmetine gönderilmez; silinen ürünler listeden kendiliğinden çıkar, "Geçmişi temizle" düğmesi listeyi boşaltır ve gösterecek ürün olana kadar bölüm görünmez. Online Store 2.0 temalarında uygulama olmadan çalışır.

- **Kategori:** Ürün ve koleksiyon keşfi
- **Kullanıldığı yerler:** Ürün, Ana sayfa, Koleksiyon, Sepet
- **Okuduğu veri:** Müşterinin tarayıcısındaki ürün handle’ları (localStorage), mağazadan güncel ürün bilgisi
- **Boyut:** 3 dosyada 34 KB, küçük bir betikle
- **Lisans:** MIT, © 2026 Lintel
- **Sayfa:** https://lintelagency.com/tr/kaynaklar/shopify-bolumleri/son-goruntulenen-urunler
- **Canlı demo:** https://lintelagency.com/resources/shopify-sections/demo/lib-recently-viewed.html
- **Eklenme tarihi:** 2026-11-02

## Ne işe yarar

- Ürün açıklamasının altında, müşteri karşılaştırdığı ürünlere tek tıkla dönebilsin diye
- Sepet sayfasında ya da ana sayfada, geri dönen ziyaretçiye baktığı ürünleri yeniden göstermek için
- 404 ya da arama sayfasında, müşterinin zaten bildiği ürünlere hızlıca dönebileceği bir kısayol olarak

## Tema düzenleyici ayarları

Bölüm tema düzenleyicide “Lib · Recently viewed” adıyla görünür. Kimlikler, türler, etiketler ve varsayılan değerler bölümün şemasından gelir. Etiketler tema düzenleyicide göründüğü gibi İngilizcedir.

### Content

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `heading` | `inline_richtext` | Heading | Recently viewed |  |
| `heading_tag` | `select` | Heading tag | h2 | `h1`, `h2`, `h3`, `p` |
| `heading_size` | `select` | Heading size | medium | `small`, `medium`, `large` |
| `text` | `richtext` | Intro text | – |  |
| `clear_label` | `text` | Clear history button | Clear history |  |
| `cleared_label` | `text` | Message after clearing | Your recently viewed list is now empty. |  |
| `show_on_product_pages` | `checkbox` | Show the list on product pages | `true` |  |

### Products

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `products_to_show` | `range` | Products to show | `4` | 2–12, step 1 |
| `image_ratio` | `select` | Image shape | portrait | `square`, `portrait`, `tall`, `landscape` |
| `card_style` | `select` | Card style | plain | `plain`, `card` |
| `card_radius` | `range` | Corner radius | `12` | 0–24 px, step 2 |
| `show_vendor` | `checkbox` | Show vendor | `false` |  |
| `sale_label` | `text` | Sale badge | Sale |  |
| `sold_out_label` | `text` | Sold out badge | Sold out |  |
| `sale_price_label` | `text` | Sale price label (screen readers) | Sale price |  |
| `regular_price_label` | `text` | Regular price label (screen readers) | Regular price |  |

### Layout

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `columns` | `range` | Columns on desktop | `4` | 2–6, step 1 |
| `columns_mobile` | `range` | Columns on mobile | `2` | 1–2, step 1 |
| `layout_mobile` | `select` | Layout on mobile | row | `grid`, `row` |
| `section_width` | `select` | Width | page | `page`, `full` |
| `text_alignment` | `select` | Heading alignment on desktop | left | `left`, `center` |
| `text_alignment_mobile` | `select` | Heading alignment on mobile | left | `left`, `center` |

### Colors

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `color_bg` | `color` | Background | #FFFFFF |  |
| `color_bg_gradient` | `color_background` | Background gradient | – |  |
| `color_text` | `color` | Text | #1F1B17 |  |
| `color_accent` | `color` | Accent (badges, focus) | #7A5A3A |  |
| `color_accent_text` | `color` | Text on accent | #FFFFFF |  |
| `color_card` | `color` | Card background | #FAF6F1 |  |

### Spacing and visibility

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `padding_top` | `range` | Top padding on desktop | `56` | 0–200 px, step 4 |
| `padding_bottom` | `range` | Bottom padding on desktop | `56` | 0–200 px, step 4 |
| `padding_top_mobile` | `range` | Top padding on mobile | `40` | 0–160 px, step 4 |
| `padding_bottom_mobile` | `range` | Bottom padding on mobile | `40` | 0–160 px, step 4 |
| `show_on` | `select` | Show on | all | `all`, `desktop`, `mobile` |
| `anchor_id` | `text` | Anchor ID | – |  |

### Motion

| Kimlik | Tür | Etiket | Varsayılan | Seçenekler ya da aralık |
| --- | --- | --- | --- | --- |
| `animate` | `checkbox` | Fade cards in as they load | `true` |  |

## Kurulum

### Shopify yönetim panelinde

1. Canlı temanızda değil, bir kopyasında çalışın: Shopify yönetim panelinde **Online Mağaza > Temalar** sayfasına gidin, mevcut temanızın **…** menüsünü açın ve **Kopya oluştur**’u seçin.
2. Kopyada **…** menüsünü açın ve **Kodu düzenle**’yi seçin.
3. `sections` klasörüne `lib-recently-viewed.liquid` adında yeni bir dosya ekleyin, aşağıdaki kaynak koddan `sections/lib-recently-viewed.liquid` içeriğini yapıştırıp kaydedin.
4. `lib-product-card.liquid` ve `lib-image.liquid` dosyalarını da aynı şekilde `snippets` klasörüne ekleyin. Başka bir Lib bölümünden gelen aynı adlı bir snippet zaten varsa aynı dosyadır; tek kopya yeterli.
5. Kopyada **Özelleştir**’i açın, **Bölüm ekle**’yi seçin ve **Lib · Recently viewed** bölümünü ekleyin. Örnek içeriği kendi içeriğinizle değiştirin, renkleri ayarlayın.
6. Kopyayı önizleyin, her şey doğru göründüğünde yayınlayın.

### Shopify CLI ile

Yerel tema klasörünüzde dosyaları indirin ve yalnızca bunları yayında olmayan bir temaya gönderin (temanın kimliğini `shopify theme list` gösterir):

```sh
curl -fsSL -o sections/lib-recently-viewed.liquid https://lintelagency.com/resources/shopify-sections/files/lib-recently-viewed.liquid
curl -fsSL -o snippets/lib-product-card.liquid https://lintelagency.com/resources/shopify-sections/files/lib-product-card.liquid
curl -fsSL -o snippets/lib-image.liquid https://lintelagency.com/resources/shopify-sections/files/lib-image.liquid
shopify theme push --theme <unpublished-theme-id> --only sections/lib-recently-viewed.liquid --only snippets/lib-product-card.liquid --only snippets/lib-image.liquid
```

Canlı temaya asla göndermeyin: her zaman yayında olmayan bir temanın kimliğini verin, `--live` ya da `--allow-live` kullanmayın. Ardından bölümü yukarıdaki gibi tema düzenleyicide ekleyin.

## Dosyalar

- [sections/lib-recently-viewed.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-recently-viewed.liquid): `sections/` klasörüne
- [snippets/lib-product-card.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-product-card.liquid): `snippets/` klasörüne
- [snippets/lib-image.liquid](https://lintelagency.com/resources/shopify-sections/files/lib-image.liquid): `snippets/` klasörüne

## Sık sorulan sorular

### Görüntüleme geçmişi nerede saklanıyor?

Yalnızca müşterinin tarayıcısında, localStorage’da tek bir anahtar altında, en çok 24 ürün handle’ından oluşan bir liste olarak. Ad, e-posta, çerez kimliği ya da başka hiçbir şey tutulmaz; Lintel’e ya da bir uygulamaya hiçbir şey gönderilmez. Müşteri site verilerini sildiğinde ya da bölümdeki "Geçmişi temizle" düğmesine bastığında liste silinir.

### Bölümü ürün şablonuna eklemem gerekiyor mu?

Evet. Bölüm ürünleri yalnızca ürün sayfalarında kaydeder; bu yüzden önce ürün şablonuna, sonra listeyi göstermek istediğiniz diğer şablonlara ekleyin. Listenin ürün sayfalarında görünmesini istemiyorsanız "Listeyi ürün sayfalarında göster" seçeneğini kapatın; ürün yine de kaydedilir.

### Fiyatlar ve stok güncel mi?

Evet. Bölüm her handle için ürünün güncel verisini mağazanızdan ister (/products/<handle>.js); fiyat, karşılaştırma fiyatı ve tükendi durumu mağazanızın bugün gösterdiğiyle aynıdır. Sildiğiniz ürünler listeden düşer.

### Çerez bildirimi ya da onay gerekir mi?

Bölüm ürün handle’larını müşteriyi izlemek ya da profillemek için değil, müşterinin kendi kullanımı için cihazda tutar; gizlilik düzenlemelerinde bu genellikle işlevsel depolama sayılır. Yükümlülükleriniz satış yaptığınız pazarlara ve gizlilik politikanıza bağlıdır; emin değilseniz kendi danışmanınızın görüşünü alın.

### Müşteri henüz hiçbir ürüne bakmadıysa ne görür?

Hiçbir şey: gösterilecek en az bir ürün olana kadar bölüm gizli kalır; JavaScript kapalıysa hiç görünmez. Tema düzenleyicide ise tasarımı ayarlayabilmeniz için yer tutucu kartlar gösterilir.

## Kaynak kod

### sections/lib-recently-viewed.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{%- comment -%}
  Recently viewed products: cards for the products this shopper looked at, newest first. On a
  product page the section notes the product's handle in the browser's localStorage (nothing else,
  and nothing leaves the device); wherever the section is placed it reads those handles, asks the
  store for each product's current details (/products/<handle>.js) and builds the cards. Products
  that were deleted drop out of the list. The section stays hidden until there is something to show,
  and a "clear history" button empties the list. Without JavaScript it renders nothing.
  Depends on: snippets/lib-product-card.liquid, snippets/lib-image.liquid
{%- endcomment -%}

{%- liquid
  assign s = section.settings
  assign heading_tag = s.heading_tag | default: 'h2'
  assign card_tag = 'h3'
  if heading_tag == 'h3' or heading_tag == 'p'
    assign card_tag = 'h4'
  endif
  assign cols = s.columns | plus: 0
  assign cols_m = s.columns_mobile | plus: 0
  assign img_vw = 100 | divided_by: cols
  assign img_sizes = '(min-width: 990px) ' | append: img_vw | append: 'vw, (min-width: 750px) 33vw, 50vw'
  assign current_handle = ''
  if product != blank
    assign current_handle = product.handle
  endif
  assign list_hidden = false
  if product != blank and s.show_on_product_pages == false
    assign list_hidden = true
  endif
-%}

<lib-recently-viewed
  class="lib-recent lib-recent--{{ s.section_width }}"
  {% if s.anchor_id != blank %}
    id="{{ s.anchor_id | handleize }}"
  {% endif %}
  data-show="{{ s.show_on }}"
  data-card="{{ s.card_style }}"
  data-layout-m="{{ s.layout_mobile }}"
  data-current="{{ current_handle | escape }}"
  data-limit="{{ s.products_to_show }}"
  data-root="{{ routes.root_url }}"
  data-currency="{{ cart.currency.iso_code }}"
  data-locale="{{ request.locale.iso_code }}"
  {% if list_hidden %}
    data-record-only
  {% endif %}
  {% if s.animate %}
    data-animate
  {% endif %}
  {% unless request.design_mode %}
    hidden
  {% endunless %}
  style="
    --lib-bg: {{ s.color_bg }};
    {% if s.color_bg_gradient != blank %}--lib-bg-image: {{ s.color_bg_gradient }};{% endif %}
    --lib-text: {{ s.color_text }};
    --lib-accent: {{ s.color_accent }};
    --lib-accent-text: {{ s.color_accent_text }};
    --lib-card: {{ s.color_card }};
    --lib-cols: {{ cols }};
    --lib-cols-m: {{ cols_m }};
    --lib-radius-card: {{ s.card_radius }}px;
    --lib-pt: {{ s.padding_top }}px;
    --lib-pb: {{ s.padding_bottom }}px;
    --lib-pt-m: {{ s.padding_top_mobile }}px;
    --lib-pb-m: {{ s.padding_bottom_mobile }}px;
  "
>
  <div class="lib-recent__inner">
    <header class="lib-recent__header" data-align="{{ s.text_alignment }}" data-align-m="{{ s.text_alignment_mobile }}">
      <div class="lib-recent__copy">
        {%- if s.heading != blank -%}
          <{{ heading_tag }} class="lib-recent__heading lib-recent__heading--{{ s.heading_size }}">
            {{- s.heading -}}
          </{{ heading_tag }}>
        {%- endif -%}
        {%- if s.text != blank -%}
          <div class="lib-recent__text">{{ s.text }}</div>
        {%- endif -%}
      </div>
      {%- if s.clear_label != blank -%}
        <button type="button" class="lib-recent__clear" data-clear>{{ s.clear_label | escape }}</button>
      {%- endif -%}
    </header>

    <ul class="lib-recent__list" role="list" data-list>
      {%- if request.design_mode -%}
        {%- comment -%} Editor only: shoppers see their own products, never these placeholders. {%- endcomment -%}
        {%- for i in (1..cols) -%}
          <li class="lib-recent__item" data-placeholder>
            {%- render 'lib-product-card', title_tag: card_tag, ratio: s.image_ratio -%}
          </li>
        {%- endfor -%}
      {%- endif -%}
    </ul>
    <p class="lib-recent__status" role="status" data-status></p>

    <template data-card-template>
      <li class="lib-recent__item">
        {%- render 'lib-product-card',
          template: true,
          title_tag: card_tag,
          ratio: s.image_ratio,
          show_vendor: s.show_vendor,
          sold_out_label: s.sold_out_label,
          sale_label: s.sale_label,
          sale_price_label: s.sale_price_label,
          regular_price_label: s.regular_price_label
        -%}
      </li>
    </template>
    <span hidden data-sizes="{{ img_sizes }}" data-cleared="{{ s.cleared_label | escape }}"></span>
  </div>
</lib-recently-viewed>

{% stylesheet %}
  .lib-recent {
    display: block;
    background-color: var(--lib-bg);
    background-image: var(--lib-bg-image, none);
    color: var(--lib-text);
    padding: var(--lib-pt) 1rem var(--lib-pb);
  }
  .lib-recent[hidden] { display: none; }
  .lib-recent__inner { max-width: 1200px; margin: 0 auto; }
  .lib-recent--full .lib-recent__inner { max-width: none; }
  @media (max-width: 749px) {
    .lib-recent { padding-top: var(--lib-pt-m); padding-bottom: var(--lib-pb-m); }
    .lib-recent[data-show='desktop'] { display: none; }
  }
  @media (min-width: 750px) {
    .lib-recent[data-show='mobile'] { display: none; }
  }

  .lib-recent__header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: 0.75rem 1.5rem;
    margin-bottom: clamp(1.25rem, 3vw, 2rem);
  }
  .lib-recent__copy { min-width: 0; max-width: 40rem; }
  .lib-recent__heading {
    margin: 0;
    font-size: clamp(1.5rem, 2.6vw, 2.125rem);
    line-height: 1.15;
    letter-spacing: -0.02em;
    text-wrap: balance;
  }
  .lib-recent__heading--small { font-size: clamp(1.125rem, 1.6vw, 1.375rem); }
  .lib-recent__heading--large { font-size: clamp(2rem, 4vw, 3rem); }
  .lib-recent__text { margin-top: 0.5rem; font-size: 1rem; line-height: 1.6; opacity: 0.85; }
  .lib-recent__text > :first-child { margin-top: 0; }
  .lib-recent__text > :last-child { margin-bottom: 0; }
  .lib-recent__header[data-align='center'] { flex-direction: column; align-items: center; text-align: center; }
  @media (max-width: 749px) {
    .lib-recent__header[data-align-m='left'] { flex-direction: row; align-items: flex-end; text-align: left; }
    .lib-recent__header[data-align-m='center'] { flex-direction: column; align-items: center; text-align: center; }
  }

  .lib-recent__clear {
    min-height: 2.75rem;
    padding: 0.5rem 0.25rem;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    font-size: 0.875rem;
    text-decoration: underline;
    text-underline-offset: 0.2em;
    cursor: pointer;
  }
  .lib-recent__clear:focus-visible { outline: 2px solid var(--lib-accent); outline-offset: 2px; border-radius: 4px; }

  .lib-recent__list {
    display: grid;
    grid-template-columns: repeat(var(--lib-cols), minmax(0, 1fr));
    gap: clamp(1rem, 2vw, 1.5rem);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .lib-recent__item { display: flex; min-width: 0; }
  @media (max-width: 749px) {
    .lib-recent__list { grid-template-columns: repeat(var(--lib-cols-m), minmax(0, 1fr)); }
    /* Row: one swipeable line with a peek of the next card. */
    .lib-recent[data-layout-m='row'] .lib-recent__list {
      grid-auto-flow: column;
      grid-template-columns: none;
      grid-auto-columns: calc((100% - 1rem) / var(--lib-cols-m) - 2.5rem);
      overflow-x: auto;
      scroll-snap-type: x mandatory;
      scrollbar-width: none;
      padding-bottom: 0.25rem;
    }
    .lib-recent[data-layout-m='row'] .lib-recent__item { scroll-snap-align: start; }
  }
  .lib-recent__status:empty { display: none; }
  .lib-recent__status { margin: 0; font-size: 0.9375rem; opacity: 0.85; }

  @media (prefers-reduced-motion: no-preference) {
    .lib-recent[data-animate] .lib-recent__item[data-new] { animation: lib-recent-in 0.5s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
  }
  @keyframes lib-recent-in {
    from { opacity: 0; transform: translateY(12px); }
  }
{% endstylesheet %}

{% javascript %}
  (() => {
    if (customElements.get('lib-recently-viewed')) return;
    // One list per store, newest first. Only product handles are kept, never personal data.
    const STORAGE_KEY = 'lib-recently-viewed';
    const MAX_STORED = 24;
    const IMAGE_WIDTHS = [300, 600, 900];

    const readHandles = () => {
      try {
        const value = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
        return Array.isArray(value) ? value.filter((h) => typeof h === 'string') : [];
      } catch (error) {
        // Storage blocked (private mode, policy) or a damaged value: start from nothing.
        return [];
      }
    };
    const writeHandles = (handles) => {
      try {
        if (handles.length) localStorage.setItem(STORAGE_KEY, JSON.stringify(handles.slice(0, MAX_STORED)));
        else localStorage.removeItem(STORAGE_KEY);
      } catch (error) {
        // Storage full or blocked: the list simply isn't kept on this device.
      }
    };
    const sized = (url, width) => `${url}${url.includes('?') ? '&' : '?'}width=${width}`;

    class LibRecentlyViewed extends HTMLElement {
      connectedCallback() {
        this.list = this.querySelector('[data-list]');
        this.status = this.querySelector('[data-status]');
        this.template = this.querySelector('[data-card-template]');
        const meta = this.querySelector('[data-sizes]');
        this.sizes = meta?.dataset.sizes || '50vw';
        this.clearedText = meta?.dataset.cleared || '';
        this.querySelector('[data-clear]')?.addEventListener('click', () => this.clear());

        const current = this.dataset.current;
        const handles = readHandles();
        if (current) writeHandles([current, ...handles.filter((h) => h !== current)]);
        if (this.hasAttribute('data-record-only')) return;

        const wanted = handles.filter((h) => h !== current).slice(0, Number(this.dataset.limit) || 4);
        if (wanted.length) this.load(wanted);
      }

      async load(handles) {
        const root = this.dataset.root || '/';
        const results = await Promise.allSettled(
          handles.map(async (handle) => {
            const response = await fetch(`${root}products/${encodeURIComponent(handle)}.js`, {headers: {Accept: 'application/json'}});
            if (response.status === 404) return {handle, gone: true};
            if (!response.ok) throw new Error(`product ${handle}: ${response.status}`);
            return {handle, product: await response.json()};
          }),
        );
        const gone = results.filter((r) => r.status === 'fulfilled' && r.value.gone).map((r) => r.value.handle);
        if (gone.length) writeHandles(readHandles().filter((h) => !gone.includes(h)));
        results
          .filter((r) => r.status === 'rejected')
          .forEach((r) => console.warn('Recently viewed: a product could not be loaded.', r.reason));

        const products = results.filter((r) => r.status === 'fulfilled' && r.value.product).map((r) => r.value.product);
        if (!products.length) return;
        this.list.querySelectorAll('[data-placeholder]').forEach((el) => el.remove());
        this.list.append(...products.map((product) => this.card(product)));
        this.hidden = false;
      }

      price(cents) {
        try {
          return new Intl.NumberFormat(this.dataset.locale || undefined, {style: 'currency', currency: this.dataset.currency || 'USD'}).format(cents / 100);
        } catch (error) {
          // Unknown locale or currency code: a plain number still reads correctly.
          return (cents / 100).toFixed(2);
        }
      }

      card(product) {
        const item = this.template.content.firstElementChild.cloneNode(true);
        const slot = (name) => item.querySelector(`[data-slot="${name}"]`);
        const root = this.dataset.root || '/';
        const link = slot('link');
        link.href = `${root}products/${product.handle}`;
        link.textContent = product.title;
        const vendor = slot('vendor');
        if (vendor) vendor.textContent = product.vendor || '';

        const image = slot('image');
        const src = product.featured_image;
        if (src) {
          image.src = sized(src, IMAGE_WIDTHS[1]);
          image.srcset = IMAGE_WIDTHS.map((w) => `${sized(src, w)} ${w}w`).join(', ');
          image.sizes = this.sizes;
          image.alt = product.media?.[0]?.alt || product.title;
          image.hidden = false;
        }

        const onSale = product.compare_at_price > product.price;
        slot('price').textContent = this.price(product.price);
        if (onSale) {
          slot('price').classList.add('lib-card__price-now--sale');
          slot('compare').textContent = this.price(product.compare_at_price);
          ['compare', 'sale-label', 'regular-label'].forEach((name) => (slot(name).hidden = false));
        }
        const card = item.querySelector('.lib-card');
        if (!product.available) {
          card.classList.add('is-sold-out');
          if (slot('sold-out').textContent.trim()) slot('sold-out').hidden = false;
        } else if (onSale && slot('sale').textContent.trim()) {
          slot('sale').hidden = false;
        }
        item.toggleAttribute('data-new', true);
        return item;
      }

      clear() {
        writeHandles([]);
        this.list.replaceChildren();
        this.querySelector('[data-clear]')?.remove();
        this.status.textContent = this.clearedText;
        if (!this.clearedText) this.hidden = true;
      }
    }
    customElements.define('lib-recently-viewed', LibRecentlyViewed);
  })();
{% endjavascript %}

{% schema %}
{
  "name": "Lib · Recently viewed",
  "tag": "section",
  "class": "lib-section",
  "settings": [
    { "type": "paragraph", "content": "Add this section to the product template so viewed products are noted, then anywhere else you want the list. Only product handles are kept, in the shopper's browser." },
    { "type": "header", "content": "Content" },
    { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Recently viewed" },
    {
      "type": "select",
      "id": "heading_tag",
      "label": "Heading tag",
      "info": "Use H1 only when this is the page's main heading.",
      "options": [
        { "value": "h1", "label": "H1" },
        { "value": "h2", "label": "H2" },
        { "value": "h3", "label": "H3" },
        { "value": "p", "label": "Paragraph" }
      ],
      "default": "h2"
    },
    {
      "type": "select",
      "id": "heading_size",
      "label": "Heading size",
      "options": [
        { "value": "small", "label": "Small" },
        { "value": "medium", "label": "Medium" },
        { "value": "large", "label": "Large" }
      ],
      "default": "medium"
    },
    { "type": "richtext", "id": "text", "label": "Intro text" },
    { "type": "text", "id": "clear_label", "label": "Clear history button", "info": "Leave empty to hide the button.", "default": "Clear history" },
    { "type": "text", "id": "cleared_label", "label": "Message after clearing", "default": "Your recently viewed list is now empty." },
    { "type": "checkbox", "id": "show_on_product_pages", "label": "Show the list on product pages", "info": "When off, product pages only note the product; the list shows on other pages.", "default": true },

    { "type": "header", "content": "Products" },
    { "type": "range", "id": "products_to_show", "label": "Products to show", "min": 2, "max": 12, "step": 1, "default": 4 },
    {
      "type": "select",
      "id": "image_ratio",
      "label": "Image shape",
      "options": [
        { "value": "square", "label": "Square" },
        { "value": "portrait", "label": "Portrait" },
        { "value": "tall", "label": "Tall" },
        { "value": "landscape", "label": "Landscape" }
      ],
      "default": "portrait"
    },
    {
      "type": "select",
      "id": "card_style",
      "label": "Card style",
      "options": [
        { "value": "plain", "label": "Plain" },
        { "value": "card", "label": "Card" }
      ],
      "default": "plain"
    },
    { "type": "range", "id": "card_radius", "label": "Corner radius", "min": 0, "max": 24, "step": 2, "unit": "px", "default": 12 },
    { "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": false },
    { "type": "text", "id": "sale_label", "label": "Sale badge", "default": "Sale" },
    { "type": "text", "id": "sold_out_label", "label": "Sold out badge", "default": "Sold out" },
    { "type": "text", "id": "sale_price_label", "label": "Sale price label (screen readers)", "default": "Sale price" },
    { "type": "text", "id": "regular_price_label", "label": "Regular price label (screen readers)", "default": "Regular price" },

    { "type": "header", "content": "Layout" },
    { "type": "range", "id": "columns", "label": "Columns on desktop", "min": 2, "max": 6, "step": 1, "default": 4 },
    { "type": "range", "id": "columns_mobile", "label": "Columns on mobile", "min": 1, "max": 2, "step": 1, "default": 2 },
    {
      "type": "select",
      "id": "layout_mobile",
      "label": "Layout on mobile",
      "options": [
        { "value": "grid", "label": "Grid" },
        { "value": "row", "label": "Swipeable row" }
      ],
      "default": "row"
    },
    {
      "type": "select",
      "id": "section_width",
      "label": "Width",
      "options": [
        { "value": "page", "label": "Page width" },
        { "value": "full", "label": "Full width" }
      ],
      "default": "page"
    },
    {
      "type": "select",
      "id": "text_alignment",
      "label": "Heading alignment on desktop",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "default": "left"
    },
    {
      "type": "select",
      "id": "text_alignment_mobile",
      "label": "Heading alignment on mobile",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "default": "left"
    },

    { "type": "header", "content": "Colors" },
    { "type": "color", "id": "color_bg", "label": "Background", "default": "#FFFFFF" },
    { "type": "color_background", "id": "color_bg_gradient", "label": "Background gradient", "info": "Optional. Covers the background color." },
    { "type": "color", "id": "color_text", "label": "Text", "default": "#1F1B17" },
    { "type": "color", "id": "color_accent", "label": "Accent (badges, focus)", "default": "#7A5A3A" },
    { "type": "color", "id": "color_accent_text", "label": "Text on accent", "default": "#FFFFFF" },
    { "type": "color", "id": "color_card", "label": "Card background", "default": "#FAF6F1" },

    { "type": "header", "content": "Spacing and visibility" },
    { "type": "range", "id": "padding_top", "label": "Top padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 56 },
    { "type": "range", "id": "padding_bottom", "label": "Bottom padding on desktop", "min": 0, "max": 200, "step": 4, "unit": "px", "default": 56 },
    { "type": "range", "id": "padding_top_mobile", "label": "Top padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 40 },
    { "type": "range", "id": "padding_bottom_mobile", "label": "Bottom padding on mobile", "min": 0, "max": 160, "step": 4, "unit": "px", "default": 40 },
    {
      "type": "select",
      "id": "show_on",
      "label": "Show on",
      "options": [
        { "value": "all", "label": "All devices" },
        { "value": "desktop", "label": "Desktop only" },
        { "value": "mobile", "label": "Mobile only" }
      ],
      "default": "all"
    },
    { "type": "text", "id": "anchor_id", "label": "Anchor ID", "info": "Optional. Link to this section with #your-id." },

    { "type": "header", "content": "Motion" },
    { "type": "checkbox", "id": "animate", "label": "Fade cards in as they load", "info": "Never when reduced motion is on.", "default": true }
  ],
  "presets": [{ "name": "Lib · Recently viewed" }]
}
{% endschema %}
```

### snippets/lib-product-card.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{% doc %}
  Product card shared by the library's product sections: image (second image on hover), optional
  vendor, price with compare-at price, sale and sold-out badges, and an optional quick add. Quick add
  is a plain Shopify product form that posts the only variant to /cart/add; products with options
  link to the product page. Colors come from the parent section's --lib-* custom properties.
  With `template: true` it prints empty slots (data-slot) for a script to fill from product JSON,
  so cards built in the browser look exactly like cards built in Liquid.
  @param {product} [product] - The product; blank prints a placeholder card (theme editor only)
  @param {boolean} [template] - Print empty slots for a script instead of a product
  @param {string} [title_tag] - Tag for the product title (default 'h3')
  @param {string} [ratio] - Image shape: 'square' (default), 'portrait', 'tall' or 'landscape'
  @param {string} [sizes] - sizes attribute for the image
  @param {boolean} [eager] - Load the image immediately
  @param {boolean} [show_vendor] - Show the vendor above the title
  @param {boolean} [show_second_image] - Show the second image on hover
  @param {boolean} [show_quick_add] - Show the quick add button or the choose options link
  @param {string} [quick_add_label] - Quick add button text
  @param {string} [choose_label] - Link text for products with options
  @param {string} [sold_out_label] - Badge and button text for sold-out products
  @param {string} [sale_label] - Badge text for products on sale
  @param {string} [sale_price_label] - Screen-reader label before the sale price
  @param {string} [regular_price_label] - Screen-reader label before the compare-at price
  @param {string} [id_prefix] - Unique prefix for the title id (the quick add button points at it)
  @example
  {% render 'lib-product-card', product: product, ratio: 'portrait', show_quick_add: true, quick_add_label: 'Add to cart', id_prefix: section.id %}
{% enddoc %}
{%- liquid
  assign tag = title_tag | default: 'h3'
  assign shape = ratio | default: 'square'
  assign title_id = id_prefix | append: '-' | append: product.id
  assign on_sale = false
  if product.compare_at_price > product.price
    assign on_sale = true
  endif
  assign has_img2 = false
  if show_second_image and product.media.size > 1 and product.media[1].preview_image != blank
    assign has_img2 = true
  endif
-%}

{%- if template -%}
  <article class="lib-card">
    <div class="lib-card__media" data-ratio="{{ shape }}">
      <img class="lib-card__img" data-slot="image" alt="" loading="lazy" width="600" height="600" hidden>
      <span class="lib-card__badge lib-card__badge--sold-out" data-slot="sold-out" hidden>{{ sold_out_label | escape }}</span>
      <span class="lib-card__badge" data-slot="sale" hidden>{{ sale_label | escape }}</span>
    </div>
    <div class="lib-card__body">
      {%- if show_vendor -%}
        <p class="lib-card__vendor" data-slot="vendor"></p>
      {%- endif -%}
      <{{ tag }} class="lib-card__title"><a class="lib-card__link" data-slot="link" href=""></a></{{ tag }}>
      <p class="lib-card__price">
        <span class="lib-card__sr" data-slot="sale-label" hidden>{{ sale_price_label | escape }}</span>
        <span class="lib-card__price-now" data-slot="price"></span>
        <span class="lib-card__sr" data-slot="regular-label" hidden>{{ regular_price_label | escape }}</span>
        <s class="lib-card__price-was" data-slot="compare" hidden></s>
      </p>
    </div>
  </article>
{%- elsif product != blank -%}
  <article class="lib-card{% if product.available == false %} is-sold-out{% endif %}">
    <div class="lib-card__media" data-ratio="{{ shape }}">
      {%- render 'lib-image', image: product.featured_image, class: 'lib-card__img', sizes: sizes, eager: eager, placeholder: 'product-1' -%}
      {%- if has_img2 -%}
        <div class="lib-card__img2" aria-hidden="true">
          {%- render 'lib-image', image: product.media[1].preview_image, class: 'lib-card__img', sizes: sizes -%}
        </div>
      {%- endif -%}
      {%- if product.available == false and sold_out_label != blank -%}
        <span class="lib-card__badge lib-card__badge--sold-out">{{ sold_out_label | escape }}</span>
      {%- elsif on_sale and sale_label != blank -%}
        <span class="lib-card__badge">{{ sale_label | escape }}</span>
      {%- endif -%}
    </div>

    <div class="lib-card__body">
      {%- if show_vendor and product.vendor != blank -%}
        <p class="lib-card__vendor">{{ product.vendor | escape }}</p>
      {%- endif -%}
      <{{ tag }} class="lib-card__title" id="{{ title_id }}">
        <a class="lib-card__link" href="{{ product.url }}">{{ product.title | escape }}</a>
      </{{ tag }}>
      <p class="lib-card__price">
        {%- if on_sale -%}
          <span class="lib-card__sr">{{ sale_price_label | escape }}</span>
          <span class="lib-card__price-now lib-card__price-now--sale">{{ product.price | money }}</span>
          <span class="lib-card__sr">{{ regular_price_label | escape }}</span>
          <s class="lib-card__price-was">{{ product.compare_at_price | money }}</s>
        {%- else -%}
          <span class="lib-card__price-now">{{ product.price | money }}</span>
        {%- endif -%}
      </p>

      {%- if show_quick_add -%}
        <div class="lib-card__cta">
          {%- if product.has_only_default_variant -%}
            {%- form 'product', product, class: 'lib-card__form' -%}
              <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
              <input type="hidden" name="quantity" value="1">
              <button
                type="submit"
                name="add"
                class="lib-card__add"
                aria-describedby="{{ title_id }}"
                {% if product.available == false %}
                  disabled
                {% endif %}
              >
                {%- if product.available -%}
                  {{- quick_add_label | escape -}}
                {%- else -%}
                  {{- sold_out_label | default: quick_add_label | escape -}}
                {%- endif -%}
              </button>
            {%- endform -%}
          {%- else -%}
            <a class="lib-card__add" href="{{ product.url }}" aria-describedby="{{ title_id }}">
              {{- choose_label | escape -}}
            </a>
          {%- endif -%}
        </div>
      {%- endif -%}
    </div>
  </article>
{%- else -%}
  <article class="lib-card">
    <div class="lib-card__media" data-ratio="{{ shape }}">
      {{ 'product-1' | placeholder_svg_tag: 'lib-card__img' }}
    </div>
    <div class="lib-card__body">
      <{{ tag }} class="lib-card__title">Example product title</{{ tag }}>
      <p class="lib-card__price"><span class="lib-card__price-now">{{ 4800 | money }}</span></p>
    </div>
  </article>
{%- endif -%}

{% stylesheet %}
  .lib-card {
    position: relative;
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
    border-radius: var(--lib-radius-card, 12px);
    color: var(--lib-text, #1f1b17);
  }
  [data-card='card'] .lib-card { background: var(--lib-card, #faf6f1); }
  .lib-card__media {
    position: relative;
    overflow: hidden;
    aspect-ratio: 1;
    border-radius: var(--lib-radius-card, 12px);
    background: color-mix(in srgb, var(--lib-text, #1f1b17) 6%, transparent);
  }
  .lib-card__media[data-ratio='portrait'] { aspect-ratio: 4 / 5; }
  .lib-card__media[data-ratio='tall'] { aspect-ratio: 2 / 3; }
  .lib-card__media[data-ratio='landscape'] { aspect-ratio: 4 / 3; }
  [data-card='card'] .lib-card__media { border-end-start-radius: 0; border-end-end-radius: 0; }
  .lib-card__img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
  }
  svg.lib-card__img { fill: color-mix(in srgb, var(--lib-text, #1f1b17) 22%, transparent); }
  .lib-card__img2 { position: absolute; inset: 0; opacity: 0; transition: opacity 0.4s ease; }
  @media (hover: hover) {
    .lib-card:hover .lib-card__img2 { opacity: 1; }
    .lib-card:hover .lib-card__img { transform: scale(1.03); }
  }
  .lib-card:focus-within .lib-card__img2 { opacity: 1; }
  .lib-card.is-sold-out .lib-card__media img { opacity: 0.6; }

  .lib-card__badge {
    position: absolute;
    z-index: 2;
    top: 0.75rem;
    left: 0.75rem;
    padding: 0.3rem 0.7rem;
    border-radius: 999px;
    background: var(--lib-accent, #1f1b17);
    color: var(--lib-accent-text, #fff);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    line-height: 1.2;
    pointer-events: none;
  }
  .lib-card__badge[hidden] { display: none; }
  .lib-card__badge--sold-out {
    background: var(--lib-bg, #fff);
    color: var(--lib-text, #1f1b17);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--lib-text, #1f1b17) 20%, transparent);
  }

  .lib-card__body { display: flex; flex: 1; flex-direction: column; gap: 0.3rem; padding-top: 0.9rem; }
  [data-card='card'] .lib-card__body { padding: 0.9rem 1rem 1rem; }
  .lib-card__vendor { margin: 0; font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.7; }
  .lib-card__vendor:empty { display: none; }
  .lib-card__title { margin: 0; font-size: 1rem; font-weight: 600; line-height: 1.3; }
  .lib-card__link { color: inherit; text-decoration: none; }
  /* The title link covers the whole card; the quick add button sits above it. */
  .lib-card__link::after { content: ''; position: absolute; inset: 0; z-index: 1; border-radius: var(--lib-radius-card, 12px); }
  .lib-card__link:hover { text-decoration: underline; text-underline-offset: 0.2em; }
  .lib-card__link:focus-visible { outline: none; }
  .lib-card__link:focus-visible::after { outline: 2px solid var(--lib-accent, #1f1b17); outline-offset: 3px; }
  .lib-card__price { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin: 0; font-size: 0.9375rem; }
  .lib-card__price-now--sale { font-weight: 600; }
  .lib-card__price-was { opacity: 0.7; font-size: 0.875rem; }
  .lib-card__price-was[hidden] { display: none; }
  .lib-card__sr {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .lib-card__cta { position: relative; z-index: 2; margin-top: auto; padding-top: 0.75rem; }
  .lib-card__form { margin: 0; }
  .lib-card__add {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 2.75rem;
    padding: 0.6rem 1rem;
    border: 1.5px solid var(--lib-text, #1f1b17);
    border-radius: min(var(--lib-radius-card, 12px), 999px);
    background: transparent;
    color: var(--lib-text, #1f1b17);
    font: inherit;
    font-size: 0.875rem;
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
  }
  .lib-card__add:hover:not(:disabled) {
    background: var(--lib-accent, #1f1b17);
    border-color: var(--lib-accent, #1f1b17);
    color: var(--lib-accent-text, #fff);
  }
  .lib-card__add:focus-visible { outline: 2px solid var(--lib-accent, #1f1b17); outline-offset: 2px; }
  .lib-card__add:disabled { opacity: 0.45; cursor: default; }

  @media (prefers-reduced-motion: reduce) {
    .lib-card__img,
    .lib-card__img2,
    .lib-card__add { transition: none; }
    .lib-card:hover .lib-card__img { transform: none; }
  }
{% endstylesheet %}
```

### snippets/lib-image.liquid

```liquid
{%- comment -%}
  MIT License · (c) 2026 Lintel · https://lintelagency.com/resources/shopify-sections
{%- endcomment -%}
{% doc %}
  Responsive image with optional art direction: a separate mobile image (below 750px),
  each honouring its own focal point from the theme editor. Falls back to a placeholder.

  @param {image} [image] - Desktop image (also used on mobile when no mobile image is set)
  @param {image} [image_mobile] - Mobile image, below 750px
  @param {string} [class] - Class for the img element
  @param {string} [sizes] - sizes attribute for the desktop image (default 100vw)
  @param {boolean} [eager] - Load immediately with high fetch priority (use for the first sections on a page)
  @param {string} [placeholder] - placeholder_svg_tag name when no image is set (default 'lifestyle-1')

  @example
  {% render 'lib-image', image: section.settings.image, image_mobile: section.settings.image_mobile, class: 'lib-hero__img', sizes: '(min-width: 990px) 50vw, 100vw', eager: true %}
{% enddoc %}

{%- liquid
  assign desktop = image
  if desktop == blank
    assign desktop = image_mobile
  endif
  assign img_sizes = sizes | default: '100vw'
  assign img_loading = 'lazy'
  assign img_priority = 'auto'
  if eager
    assign img_loading = 'eager'
    assign img_priority = 'high'
  endif
  assign art = false
  if image != blank and image_mobile != blank
    assign art = true
  endif
-%}

{%- if desktop != blank -%}
  <picture
    class="lib-picture{% if art %} lib-picture--art{% endif %}"
    {% if art %}
      style="--lib-pos-m: {{ image_mobile.presentation.focal_point | default: '50% 50%' }};"
    {% endif %}
  >
    {%- if art -%}
      <source
        media="(max-width: 749px)"
        srcset="
          {{ image_mobile | image_url: width: 375 }} 375w,
          {{ image_mobile | image_url: width: 750 }} 750w,
          {{ image_mobile | image_url: width: 1100 }} 1100w,
          {{ image_mobile | image_url: width: 1500 }} 1500w
        "
        sizes="100vw"
        width="{{ image_mobile.width }}"
        height="{{ image_mobile.height }}"
      >
    {%- endif -%}
    {{
      desktop
      | image_url: width: 2400
      | image_tag:
        class: class,
        loading: img_loading,
        fetchpriority: img_priority,
        sizes: img_sizes,
        widths: '600, 900, 1200, 1600, 2000, 2400'
    }}
  </picture>
{%- else -%}
  {{ placeholder | default: 'lifestyle-1' | placeholder_svg_tag: class }}
{%- endif -%}

{% stylesheet %}
  .lib-picture { display: contents; }
  @media (max-width: 749px) {
    /* image_tag sets the desktop focal point inline; the mobile image needs its own. */
    .lib-picture--art img { object-position: var(--lib-pos-m) !important; }
  }
{% endstylesheet %}
```

## Lisans

MIT Lisansı, © 2026 Lintel. Kendi mağazanızda ya da müşteri işlerinde ücretsiz kullanabilir, değiştirebilir ve yayınlayabilirsiniz; her dosyanın başındaki lisans satırını koruyun.
