【アプリなし】card-product (product-card) が使える閲覧履歴セクションを自作したい!

未分類
未分類

アプリなしで、閲覧履歴セクションを自作し、card-product (product-card) を使いたい。

これを実現するのが、普通の方法では難しかった。

なぜなら、クッキーなり、ブラウザのlocalStorageに保存した情報をliquidコードで解釈するところが最大の障壁になるため、商品カード(snippet/card-product.liquid)に閲覧履歴情報内のproduct情報を渡してrenderで呼ぶという処理ができない。

だから多くの閲覧履歴セクションを自作の方法では、この問題を回避するために、card-productをrenderすることを諦め、商品カードをjsで擬似的にhtmlレンダリングする手法を取っている記事がほとんどだし、筆者もそうしていた。

ただ、この方法では、他の仕様変更で、商品カード自体の表示を拡張するときに、jsでレンダリングするhtmlコードにも改良をしないといけなくなる

かつ、この手法はShopifyのテーマに見られる便利な「商品カード(card-prodcut.liquid)」の概念に反して、ソースコードの見通しも悪くなる。

かつ、筆者はhtmlレンダリングだけでは実現できない機能にもぶつかった。

Judge.meのレビューレーティングの表示が、judge.meのアプリ経由でないと呼び出しできないためだ。

なので、なんとかして他の商品カードを表示しているセクション同様に、商品カードにproduct情報を渡して表示する方法でないと無理、となった。

これを解決するために、以下の記事を参考にし、ShopifyのSection Rendering APIというものを利用する形になった。

参考になった記事
https://qiita.com/Shopify_ojisan/items/f66b321dfdec4b43ad23

素晴らしい気づきを得られる本記事の著者さまには頭が上がりません。。!ありがとうございます。

上記の記事をさらに以下の点で改良させていただきました。

  • Cookieにでなくブラウザの「localStorage」に閲覧履歴情報を保存する仕様に変更
  • セクション上で閲覧履歴のデスクトップでの商品表示列数を変更できるようにスキーマをセット

1. section/recently-product-card.liquidを作成

<div class="js-recentlySectionId" data-recently-section-id="{{ section.id }}">
  {% if template contains 'product' %}
            {% render 'card-product',
              card_product: product,
              show_rating: true,
              show_quick_add: false
            %}
  {% endif %}
</div>
{% schema %}
{
  "name": "最近見た商品(埋め込み用セクション)",
  "class": "section"
}
{% endschema %}

2. section/recently-prouct-slider.liquidを作成

{% comment -%}
------------------------------------
最近チェックした商品
------------------------------------
{%- endcomment -%}

{%- comment -%} コントロールjs {%- endcomment -%}
<script>
  window.addEventListener('load', function(){
    const recentlyViewController = (function(){

      // ローカルストレージからデータを取得する
      let recentlyItems = localStorage.getItem('shopify_recently_viewed');
      recentlyItems = recentlyItems ? JSON.parse(recentlyItems) : [];

      // 閲覧履歴がある時 ローカルストレージにデータがある時
      if(recentlyItems.length > 0) {
        Shopify.Products.showRecentlyViewed({
          howManyToShow: 8, // 表示する商品数を変更できます。
          wrapperId: 'js-recentlySliderBody',
          recentlySectionId: Shopify.recentlySectionId,
          onComplete: function() {
            // データが読み込まれた後発火させたい時

            // 非表示にしているアイテムを表示させる
            let lists = Array.from(document.querySelectorAll('li.recently-slider__item'));
            lists.forEach((item) => {
              item.style.display = "block";
            });
          }
        });
      } else {
        // ローカルストレージにデータがない場合、セクションを非表示にする
        document.querySelector('.recently_product_slider_section').style.display = 'none';
      }

    })();
  }, false);
</script>

{{ 'component-card.css' | asset_url | stylesheet_tag }}
{{ 'component-price.css' | asset_url | stylesheet_tag }}
{{ 'component-slider.css' | asset_url | stylesheet_tag }}
{{ 'template-collection.css' | asset_url | stylesheet_tag }}

{%- style -%}
  .section-{{ section.id }}-padding {
    padding-top: {{ section.settings.padding_top | times: 0.75 | round: 0 }}px;
    padding-bottom: {{ section.settings.padding_bottom | times: 0.75 | round: 0 }}px;
  }

  @media screen and (min-width: 750px) {
    .section-{{ section.id }}-padding {
      padding-top: {{ section.settings.padding_top }}px;
      padding-bottom: {{ section.settings.padding_bottom }}px;
    }
  }
{%- endstyle -%}

    <div class="page-width section-{{ section.id }}-padding">

      <ul id="js-recentlySliderBody" class="collection-slider__list product-recently-viewed__list tw-gap-[2rem] tw-grid product-grid contains-card contains-card--product{% if settings.card_style == 'standard' %} contains-card--standard{% endif %} tw-grid-cols-{{ section.settings.columns_desktop }}{% if section.settings.collection == blank %} {% if show_mobile_slider == false %}max-md:tw-grid-cols-2{% endif %}{% else %} grid--{{ section.settings.columns_mobile }}-col-tablet-down{% endif %}"
        role="list"
        aria-label="{{ 'general.slider.name' | t }}"
      >
      </ul>
      {%- if show_mobile_slider or show_desktop_slider -%}
        <div class="slider-buttons no-js-hidden">
          <button
            type="button"
            class="slider-button slider-button--prev"
            name="previous"
            aria-label="{{ 'general.slider.previous_slide' | t }}"
            aria-controls="Slider-{{ section.id }}"
          >
            {% render 'icon-caret' %}
          </button>
          <div class="slider-counter caption">
            <span class="slider-counter--current">1</span>
            <span aria-hidden="true"> / </span>
            <span class="visually-hidden">{{ 'general.slider.of' | t }}</span>
            <span class="slider-counter--total">{{ products_to_display }}</span>
          </div>
          <button
            type="button"
            class="slider-button slider-button--next"
            name="next"
            aria-label="{{ 'general.slider.next_slide' | t }}"
            aria-controls="Slider-{{ section.id }}"
          >
            {% render 'icon-caret' %}
          </button>
        </div>
      {%- endif -%}
    </div>

{% schema %}
{
  "name": "最近閲覧した商品",
  "tag": "section",
  "class": "recently_product_slider_section",
  "disabled_on": {
    "groups": ["header", "footer"]
  },
  "settings": [
    {
      "type": "range",
      "id": "products_to_show",
      "min": 2,
      "max": 25,
      "step": 1,
      "default": 4,
      "label": "t:sections.featured-collection.settings.products_to_show.label"
    },
    {
      "type": "range",
      "id": "columns_desktop",
      "min": 1,
      "max": 5,
      "step": 1,
      "default": 4,
      "label": "t:sections.featured-collection.settings.columns_desktop.label"
    },
    {
      "type": "range",
      "id": "padding_top",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "label": "t:sections.all.padding.padding_top",
      "default": 36
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "label": "t:sections.all.padding.padding_bottom",
      "default": 36
    },
    {
      "type": "color_scheme",
      "id": "color_scheme",
      "label": "t:sections.all.colors.label",
      "info": "t:sections.all.colors.has_cards_info",
      "default": "scheme-1"
    
  ],
  "presets": [
    {
      "name": "最近閲覧した商品"
    }
  ]
}
{% endschema %}

3. assets/jquery.products.jsを作成

/**
 * Module to show Recently Viewed Products
 *
 * Copyright (c) 2014 Caroline Schnapp (11heavens.com)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 */
Shopify.Products = (function() {
    var config = {
      howManyToShow: 3,
      howManyToStoreInMemory: 10,
      wrapperId: 'recently-viewed-products',
      templateId: 'recently-viewed-product-template',
      onComplete: null,
      recentlySectionId: null
    };
  
    var productHandleQueue = [];
    var wrapper = null;
    var template = null;
    var shown = 0;
  
    var localStorageHandler = {
      name: 'shopify_recently_viewed',
      write: function(recentlyViewed) {
        localStorage.setItem(this.name, JSON.stringify(recentlyViewed));
      },
      read: function() {
        var recentlyViewed = localStorage.getItem(this.name);
        return recentlyViewed ? JSON.parse(recentlyViewed) : [];
      },
      destroy: function() {
        localStorage.removeItem(this.name);
      },
      remove: function(productHandle) {
        var recentlyViewed = this.read();
        var position = jQuery.inArray(productHandle, recentlyViewed);
        if (position !== -1) {
          recentlyViewed.splice(position, 1);
          this.write(recentlyViewed);
        }
      }
    };
  
    var finalize = function() {
      wrapper.show();
      // If we have a callback.
      if (config.onComplete) {
        try { config.onComplete(); } catch (error) { console.log(error); }
      }
    };
  
    var moveAlong = function() {
      if (productHandleQueue.length && shown < config.howManyToShow) {
        fetch(`/products/${productHandleQueue[0]}?sections=${config.recentlySectionId}`)
          .then((r) => r.json())
          .then(
            (d) => {
              wrapper.append(d[`${config.recentlySectionId}`]);
              productHandleQueue.shift();
              shown++;
              moveAlong();
            },
            (e) => {
              console.log(e);
              localStorageHandler.remove(productHandleQueue[0]);
              productHandleQueue.shift();
              moveAlong();
            }
        );
      } else {
        finalize();
      }
    };
  
    return {
  
      resizeImage: function(src, size) {
        if (size == null) {
          return src;
        }
  
        if (size == 'master') {
          return src.replace(/http(s)?:/, "");
        }
  
        var match  = src.match(/\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?/i);
  
        if (match != null) {
          var prefix = src.split(match[0]);
          var suffix = match[0];
  
          return (prefix[0] + "_" + size + suffix).replace(/http(s)?:/, "");
        } else {
          return null;
        }
      },
  
      showRecentlyViewed: function(params) {
        var params = params || {};
  
        // Update defaults.
        jQuery.extend(config, params);
  
        // Read from localStorage.
        productHandleQueue = localStorageHandler.read();
  
        // Template and element where to insert.
        wrapper = jQuery('#' + config.wrapperId);
  
        // How many products to show.
        config.howManyToShow = Math.min(productHandleQueue.length, config.howManyToShow);
  
        // If we have any to show.
        if (config.howManyToShow && wrapper.length) {
          // Getting each product with an Ajax call and rendering it on the page.
          moveAlong();
        }
      },
  
      getConfig: function() {
        return config;
      },
  
      clearList: function() {
        localStorageHandler.destroy();
      },
  
      recordRecentlyViewed: function(params) {
        //window.alert('recordRecentlyViewed function has been called.');
  
        var params = params || {};
  
        // Update defaults.
        jQuery.extend(config, params);
  
        // Read from localStorage.
        var recentlyViewed = localStorageHandler.read();
  
        // If we are on a product page.
        if (window.location.pathname.indexOf('/products/') !== -1) {
  
          // What is the product handle on this page.
          var productHandle = window.location.pathname.match(/\/products\/([a-z0-9\-]+)/)[1];
          console.log('Product handle:', productHandle);
  
          // In what position is that product in memory.
          var position = jQuery.inArray(productHandle, recentlyViewed);
          console.log('Product position in list:', position);
  
          // If not in memory.
          if (position === -1) {
            // Add product at the start of the list.
            recentlyViewed.unshift(productHandle);
            console.log('Product added to recently viewed list:', recentlyViewed);
  
            // Only keep what we need.
            recentlyViewed = recentlyViewed.splice(0, config.howManyToStoreInMemory);
          } else {
            // Remove the product and place it at start of list.
            recentlyViewed.splice(position, 1);
            recentlyViewed.unshift(productHandle);
            console.log('Product reordered in recently viewed list:', recentlyViewed);
          }
  
          // Update localStorage.
          localStorageHandler.write(recentlyViewed);
          console.log('LocalStorage updated with:', recentlyViewed);
        }
      }
    };
  })();

4. theme.liquid内の</head>の手前に以下を設置

{%- comment -%} 最近チェックした商品 セッティング{%- endcomment -%}
<!-- jqueryを他の箇所で読み込みしていないものと仮定しています -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js" defer="defer"></script>
<script src="{{ "jquery.products.js" | asset_url }}" defer="defer"></script>
<script>
window.addEventListener('load', function() {
  Shopify.recentlySectionId = document.querySelector('.js-recentlySectionId').dataset.recentlySectionId;
});
</script>
{%- comment -%} 最近チェックした商品 セッティング{%- endcomment -%}

5. theme.liquid内の</body>の手前に以下を設置

{%- comment -%} 最近チェックした商品 読み込み用インスタンス {%- endcomment -%}
<div>
{% section 'recently-product-card' %}
</div>
{%- comment -%} 最近チェックした商品 読み込み用インスタンス {%- endcomment -%}

6. main-product.liquidの冒頭に以下を設置

{%- comment -%} 最近チェックした商品 書き込み {%- endcomment -%}
<script>
window.addEventListener('load', function() {
  Shopify.Products.recordRecentlyViewed();
});
</script>
{%- comment -%} 最近チェックした商品 書き込み {%- endcomment -%}

めちゃくちゃニッチすぎる備忘録でした!

Webサイト制作を依頼したい方へ

PRYTHM WORKS(プリズムワークス)は、東京都墨田区、東京スカイツリーのふもとにあるWebコンテンツ制作事務所です。

華々しいおしゃれなECサイトをはじめ、アンダーグラウンドな案件まで幅広くご依頼をいただき、どんな案件でも真心・丁寧・楽しくを理念に掲げて制作しております!

作りたいサービスはあるけど、まずは費用感が知りたい!という方も、まずはお問い合わせください!

mail@prytymworks.tokyo

PRYTHM WORKSが手掛ける仕事の一部をご紹介します。

制作のご依頼者様用 費用かんたんお見積もりフォーム

おそらく本記事を読まれるのは、制作の現場の、法人またはフリーの、プロデューサーの方、ディレクターの方、エンジニアの方がほとんどかと存じます。
いつもおつかれさまです!そして本記事をお読みいただきありがとうございます。
紹介した記事の内容について、またはその他制作のご依頼について、以下のフォームより簡易お見積もりができます!
試算だけならフォーム送信しなくてもできますので、ぜひ試しにいかがでしょう?

お仕事をご希望の制作者様用 お問合せフォーム

また、まずはライトなご相談から…ということであれば、こちらのコンタクトフォームからお気軽にどうぞ!ご縁を大切にしてご返信いたします!

    ShopifyでのECサイト制作を行うならPRYTHMWORKS(プリズムワークス)へ

    ShopifyでのECサイト制作を外注しようとお考えの方は、PRYTHMWORKS(プリズムワークス)にご依頼ください。高品質なWebサイト制作で、お客様のビジネス成長を支援する会社です。Shopifyを使ったECサイト制作に力を入れており、売上向上に貢献するECサイト構築を代行いたします。豊富な経験と実績にもとづき、お客様のニーズに最適なECサイトを構築しますので、費用相場や制作事例など、気になることがございましたらお気軽にお問い合わせください。移行のご相談も承っております。

    社名合同会社PRYTHMWORKS
    事業内容EC/Webサイトの構築、管理保守
    CEO吉川直人
    法人事務所所在地〒150-0001
    東京都渋谷区神宮前六丁目23番4号桑野ビル2階
    設立日2023年(令和5年)7月3日
    沿革2020年(令和2年)10月
    個人事業主PRYTHMWORKS事業開始
    2023年(令和5年)7月
    合同会社PRYTHMWORKS設立

    会社に関するご連絡は以下よりお願いいたします。

    mail@prytymworks.tokyo


    お問い合わせ用LINEはこちらからどうぞ!

    お問い合わせ用LINEはこちらからどうぞ!

    LINE

    GoogleMeetの相談予約が可能になりました!

    以下の空いている日からサクッとGoogleMeet(ビデオ通話)の予約をどうぞ!