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/