LoginSignup
3
2

More than 1 year has passed since last update.

Shopifyのテーマ開発で、基本必須化してきている商品閲覧履歴のスライダー。
ほぼ100%使うけど…毎回前書いたコードをコピペしながら、「ここどうだったかな?」となって面倒なので、メモ的な意味で記事にしました!
基本コピペで使えるようにはなっていますが、スタイルシートの変更があるのでノーコードでの実装は難しいかと思われます。
なので、エンジニア向けの記事になります。

実装方法

元のネタはこちらです。
https://medium.com/gobeyond-ai/showing-recently-viewed-products-in-shopify-eff00309642c
https://github.com/carolineschnapp/recently-viewed
こちらを元に、少しずつ必要なものを継ぎ足し、継ぎ足し…秘伝のコードになっております。

assetsフォルダにjquery.products.jsを作成し、以下のコードを入力

/**
 * Cookie plugin
 *
 * Copyright (c) 2006 Klaus Hartl (stilbuero.de)
 * Dual licensed under the MIT and GPL licenses:
 * http://www.opensource.org/licenses/mit-license.php
 * http://www.gnu.org/licenses/gpl.html
 *
 */

jQuery.cookie=function(b,j,m){if(typeof j!="undefined"){m=m||{};if(j===null){j="";m.expires=-1}var e="";if(m.expires&&(typeof m.expires=="number"||m.expires.toUTCString)){var f;if(typeof m.expires=="number"){f=new Date();f.setTime(f.getTime()+(m.expires*24*60*60*1000))}else{f=m.expires}e="; expires="+f.toUTCString()}var l=m.path?"; path="+(m.path):"";var g=m.domain?"; domain="+(m.domain):"";var a=m.secure?"; secure":"";document.cookie=[b,"=",encodeURIComponent(j),e,l,g,a].join("")}else{var d=null;if(document.cookie&&document.cookie!=""){var k=document.cookie.split(";");for(var h=0;h<k.length;h++){var c=jQuery.trim(k[h]);if(c.substring(0,b.length+1)==(b+"=")){d=decodeURIComponent(c.substring(b.length+1));break}}}return d}};

/**
 * 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
   };

   var productHandleQueue = [];
   var wrapper = null;
   var template = null;
   var shown = 0;

   var cookie = {
     configuration: {
       expires: 90,
       path: '/',
       domain: window.location.hostname
     },
     name: 'shopify_recently_viewed',
     write: function(recentlyViewed) {
       jQuery.cookie(this.name, recentlyViewed.join(' '), this.configuration);
     },
     read: function() {
       var recentlyViewed = [];
       var cookieValue = jQuery.cookie(this.name);
       if (cookieValue !== null) {
         recentlyViewed = cookieValue.split(' ');
       }
       return recentlyViewed;
     },
     destroy: function() {
       jQuery.cookie(this.name, null, this.configuration);
     },
     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) { }
     }
   };

   var moveAlong = function() {
     if (productHandleQueue.length && shown < config.howManyToShow) {
       jQuery.ajax({
         dataType: 'json',
         url: '/products/' + productHandleQueue[0] + '.js',
         cache: false,
         success: function(product) {
           template.tmpl(product).appendTo(wrapper);
           productHandleQueue.shift();
           shown++;
           moveAlong();
         },
         error: function() {
           cookie.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 cookie.
       productHandleQueue = cookie.read();

       // Template and element where to insert.
       template = jQuery('#' + config.templateId);
       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 && template.length && wrapper.length) {
         // Getting each product with an Ajax call and rendering it on the page.
         moveAlong();
       }

     },

     getConfig: function() {
       return config;
     },

     clearList: function() {
       cookie.destroy();
     },

     recordRecentlyViewed: function(params) {

       var params = params || {};

       // Update defaults.
       jQuery.extend(config, params);

       // Read cookie.
       var recentlyViewed = cookie.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];
         // In what position is that product in memory.
         var position = jQuery.inArray(productHandle, recentlyViewed);
         // If not in memory.
         if (position === -1) {
           // Add product at the start of the list.
           recentlyViewed.unshift(productHandle);
           // 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);
         }

         // Update cookie.
         cookie.write(recentlyViewed);

       }

     }

   };

 })();

theme.liquidに以下のコードを追加し、ファイルを読み込む

jqueryとjqueryのtmplを使用するために読み込みが必要になってくるコードです。
最後の1行 {{ "jquery.products.js" | asset_url | script_tag }} を {{ content_for_header }} より上で読み込むとうまくいかない時があるのでご注意!

    {%- comment -%} 最近チェックした商品 読み込み {%- endcomment -%}
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    {{ '//ajax.aspnetcdn.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js' | script_tag }}
    {{ "jquery.products.js" | asset_url | script_tag }}

main-product.liquidに以下のコードを書き込む

{%- comment -%} 最近チェックした商品 書き込み {%- endcomment -%}
<script type="text/javascript">
Shopify.Products.recordRecentlyViewed();
</script>

商品ページが読み込まれた時に、商品データをクッキーに入力するコードになります。
GitHubのコードにはtheme.liquidに下記のコードを書き込むように書かれていますが、個人的にはtheme.liquidをごちゃつかせたくないので商品詳細ページに書き込むようにしています。

{% if template contains 'product' %}

<script>
Shopify.Products.recordRecentlyViewed();
</script>

{% endif %}

sectionsフォルダにrecently-prouct-slider.liquidを作成するし以下のコードを書き込む

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

<!-- product-recently-viewed -->
<div class="product-recently-viewed collection-slider">
  <div class="container">
    <h2 class="product-recently-viewed__title section__title">最近チェックした商品</h2>

    <ul id="js-recentlySliderBody" class="js-recentlySlider collection-slider__list product-recently-viewed__list">

      {% unless customer %}
        {%- comment -%} 未ログイン時 {%- endcomment -%}
        <p id="wishlist-empty-text"
          style="padding: 60px 0; text-align: center;min-height: 320px; display: flex; align-items: center;">
          まだ最近チェックした商品がありません
        </p>
      {% endunless %}

    </ul>
  </div>
</div>

{%- comment -%} ログイン時のみ発動 {%- endcomment -%}
{% if customer %}
  {%- comment -%} 商品カード {%- endcomment -%}
  {% raw %}
    <script id="recently-viewed-product-template"  type="text/x-jquery-tmpl">
      <li class="featured-collection-slider__item collection-slider__item">
        <div class="product-card">
          <a href="${url}" class="product-card__link">
            <div class="product-card__head">
              <img src="${Shopify.Products.resizeImage(featured_image, "master")}" alt="${title}">
            </div>
            <div class="product-card__body">
              <h3 class="product-card__title">${title}</h3>
              <p class="product-card__price">
                ${(price / 100).toLocaleString()}
                <span class="tax">(税込)</span>
              </p>
            </div>
          </a>
        </div>
      </li>
    </script>
  {% endraw %}


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

        // クッキーからデータを取得する
        let recentlyItems = jQuery.cookie('shopify_recently_viewed');

        // 閲覧履歴がある時 クッキーにデータがある時
        if(recentlyItems != null ) {
          Shopify.Products.showRecentlyViewed({
            howManyToShow: 8,// 表示する商品数を変更できます。
            wrapperId: 'js-recentlySliderBody',
            onComplete: function() {
              // データが読み込まれた後発火させたい時
              $('#js-recentlySliderBody').slick({
                autoplay: false,
                dots: false,
                arrows: true,
                slidesToShow: 4,
                slidesToScroll: 1,
                infinite: false,
                prevArrow: '<button class="slick-prev"><span class="slick-arrow__inner"></span></button>',
                nextArrow: '<button class="slick-next"><span class="slick-arrow__inner"></span></button>',
                responsive: [
                  {
                    breakpoint: 480,
                    settings: {
                      slidesToShow: 2,
                    },
                  },
                ],
              });

              // 非表示にしているアイテムを表示させる
              let lists = Array.from(document.querySelectorAll('li.recently-slider__item'));
              lists.forEach((item) => {
                item.style.display = "block";
              });
            }
          });
        } else {
          // 閲覧履歴がない時の処理 クッキーにデータがない時
          document.getElementById('js-recentlySliderBody').insertAdjacentHTML("afterbegin",`<p id="wishlist-empty-text" style="padding: 60px 0; text-align: center;min-height: 320px; display: flex; align-items: center;">まだ最近チェックした商品がありません</p>`);
        }

      })();
    }, false);
  </script>
{% endif %}

{% schema %}
{
  "name": "Product Recently Viewed",
  "settings": [
  ],
  "presets": [
    {
      "name": "Product Recently Viewed"
    }
  ]
}
{% endschema %}

上記のファイルを作成するとOS.2.0であれば、任意の場所に読み込まれるようになりますが、2.0以前のテーマでTOPページに呼び出したい時は任意の場所に{% section 'recently-prouct-slider.liquid' %}で呼び出してください。
 

まとめ

コードばかりで解説が少なくなりましたが、それはまた別の機会に記事で紹介したいと思います。
一つ注意点があり、商品のハンドル名に日本語が含まれる場合は、クッキーへの書き込みがうまくいかないので注意が必要になります!
(ハンドルとは…SHOP_URL/products/ハンドル です!)

ちょっとだけ告知を
ARCHETYPでは、Shopifyのテーマ開発やアプリ開発をやりたいエンジニアを募集しております!
気になった方はお気軽にエントリーをお待ちしております!
https://www.archetyp.jp/recruit/

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2