recently-viewed-products について
商品閲覧履歴を記録し、オンラインストアに最近見た商品ウィジェットを表示するためのスキルです。各プラットフォームの標準機能とカスタム実装を扱い、表示時には商品情報を再取得します。
主な機能
- 01Shopify、WooCommerce、BigCommerceの選択肢
- 02ブラウザストレージによる閲覧履歴
- 03localStorageを使った独自トラッキング
- 04サーバーからの商品情報更新
- 05プライバシー同意と有効期限の案内
活用シーン
- 01商品詳細ページへの最近見た商品欄の追加
- 02カートやホームページへの閲覧履歴表示
- 03ヘッドレスストア向け履歴ウィジェットの構築
- 04パーソナライズエンジンへの閲覧データ提供
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill recently-viewed-productsインストール後、Claude Code で/recently-viewed-productsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
tile.jsonevals/ssr-hydration-and-privacy-fallbacks/criteria.jsontask.mdevals/storage-utility-implementation/criteria.jsontask.mdevals/widget-component-and-batch-api/criteria.jsontask.md詳細
最近閲覧した商品
概要
購入者が閲覧した商品を追跡し、商品ページ、カート、ホームページに「最近閲覧した商品」ウィジェットを表示します。セッション中およびセッションをまたいだ閲覧履歴にはブラウザーのストレージを使用します。古い価格や在庫切れの商品が表示されないよう、商品データは常にサーバーから再取得します。
このスキルを使用する場面
- 再訪問者向けに「前回の続きから」体験を実装する場合
- 商品詳細ページやカートドロワーに「最近閲覧した商品」ウィジェットを追加する場合
- クライアント側の行動データを必要とするパーソナライズエンジンと連携する場合
- ヘッドレスストアフロントを構築しており、バックエンドに依存しない軽量な閲覧履歴が必要な場合
基本手順
ステップ 1:マーチャントのプラットフォームを特定し、適切な方法を選択する
| プラットフォーム | 推奨される方法 | 理由 |
|---|---|---|
| Shopify | OS2.0 テーマに組み込まれている Recently Viewed セクションを有効にするか、Also Bought • Related products / LimeSpot Personalizer をインストールします | Dawn とほとんどの OS2.0 テーマにはネイティブの Recently Viewed セクションが含まれています。LimeSpot(月額 15 ドル)は、セッションをまたいだ最近閲覧した商品を含む、AI を活用したパーソナライズ機能を追加します |
| WooCommerce | WooCommerce に組み込まれている Recently Viewed Products ウィジェットまたはショートコードを使用するか、YITH WooCommerce Frequently Bought Together をインストールします | WooCommerce には最近閲覧した商品ウィジェットが標準で含まれています。外観 → ウィジェット から、または [recent_products] ショートコードを使って配置できます |
| BigCommerce | BigCommerce Page Builder の Recently Viewed ウィジェットを使用するか、Cornerstone テーマの設定で有効にします | Cornerstone と BigCommerce Page Builder にはどちらも、ブラウザーの Cookie を自動的に使用するネイティブの Recently Viewed ウィジェットが含まれています |
| カスタム/ヘッドレス | localStorage ベースの閲覧追跡を実装し、新しい商品データを取得するサーバー側の一括エンドポイントを用意します | クライアント側ストレージを使うため、サーバー側に状態を保持する必要はありません。表示時にデータを再取得することで、価格と在庫を最新の状態に保てます |
ステップ 2:プラットフォームで最近閲覧した商品を有効にする
Shopify
組み込みセクションを使用する場合(OS2.0 テーマ — Dawn、Sense、Craft):
- オンラインストア → テーマ → カスタマイズ に移動します
- 商品ページのテンプレートを開きます
- セクションを追加 をクリックし、「Recently Viewed」を検索します。セクション一覧では 最近閲覧した商品 として表示されます
- 次の項目を設定します。
- 表示する商品の最大数 を設定します(4~8 件を推奨)
- セクションの見出しテキスト(「最近閲覧した商品」、「閲覧履歴」など)を設定します
- 商品カードのスタイルを設定します。価格や評価を表示するかどうかなどを選びます
- セクションでは JavaScript とブラウザーのストレージ(localStorage)が自動的に使用されるため、追加の設定は不要です
ホームページの場合:
- ホームページのテンプレートを開きます
- 同じ方法で 最近閲覧した商品 セクションを追加します
- このセクションのコンテンツは、以前に商品を閲覧したことがある再訪問者にのみ表示されます
LimeSpot Personalizer(デバイスをまたいだ AI によるパーソナライズ):
- Shopify App Store からインストールします
- LimeSpot は(ブラウザー内だけでなく)サーバー側で閲覧を追跡するため、最近閲覧した商品はデバイスやブラウザーをまたいで保持されます
- LimeSpot ダッシュボードで最近閲覧した商品ウィジェットの配置を設定します。アプリブロックを使って、どのページにも表示できます
WooCommerce
組み込みの「最近閲覧した商品」ウィジェット:
- 外観 → ウィジェットに移動します
- WooCommerce Recently Viewed Productsウィジェットを探します
- ショップサイドバーまたはフッターのウィジェットエリアにドラッグします
- 次の項目を設定します:
- 表示する商品の数:4~6がおすすめです
- タイトルを表示:はい/いいえ
- または、ショートコード
[woocommerce_recently_viewed_products per_page="4"]を商品ページのテンプレート、サイドバー、Gutenbergブロックに直接配置します
Classic Editor/Gutenbergの場合:
- Gutenbergでは、ショートコードブロックを追加し、
[woocommerce_recently_viewed_products per_page="4"]を貼り付けます - Elementorの場合は、ショートコードウィジェットを使って同じコードを埋め込みます
WooCommerceは、閲覧者のセッション(PHPセッション/Cookie)に最近閲覧した商品のIDを保存します。プラグインは不要です。ウィジェットはこのセッションから自動的に読み込みます。
BigCommerce
組み込みの「最近閲覧した商品」ウィジェット(Cornerstoneテーマ):
- ストアフロント → マイテーマ → カスタマイズに移動します
- 商品ページのテンプレートに移動します
- サイドバーで最近閲覧した商品セクションを探して有効にします(または、ページレイアウトで位置を調整します)
- 表示する商品の数を設定します
- BigCommerceはブラウザーのCookieを使って最近閲覧した商品を追跡します。アプリは不要です
ページビルダーを使う方法:
- ストアフロント → ページビルダーに移動します
- ウィジェットを表示したいページを開きます
- ウィジェットパネルから最近閲覧した商品ウィジェットをページにドラッグします
- 右側のウィジェットパネルで表示設定を行います
カスタム/ヘッドレス
localStorageを使った閲覧履歴の追跡:
// lib/recentlyViewed.js
const STORAGE_KEY = 'rv_products';
const MAX_ITEMS = 12;
const TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
export function recordView(productId) {
const items = getStoredItems();
const filtered = items.filter(i => i.id !== productId); // remove existing entry
const updated = [{ id: productId, viewedAt: Date.now() }, ...filtered].slice(0, MAX_ITEMS);
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); } catch {}
}
export function getRecentlyViewedIds(excludeId = null) {
const now = Date.now();
return getStoredItems()
.filter(i => now - i.viewedAt < TTL_MS && i.id !== excludeId)
.map(i => i.id);
}
function getStoredItems() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '[]'); }
catch { return []; }
}
商品詳細ページで閲覧履歴を記録する:
// In ProductDetailPage.jsx — read localStorage only in useEffect to avoid SSR mismatch
import { useEffect } from 'react';
import { recordView } from '../lib/recentlyViewed';
export function ProductDetailPage({ product }) {
useEffect(() => { recordView(product.id); }, [product.id]);
return (
<div>
{/* product content */}
<RecentlyViewedWidget excludeId={product.id} maxItems={4} />
</div>
);
}
ウィジェットコンポーネント — サーバーから最新の商品データを再取得します:
// RecentlyViewedWidget.jsx
export function RecentlyViewedWidget({ excludeId, maxItems = 4 }) {
const [products, setProducts] = useState([]);
useEffect(() => {
const ids = getRecentlyViewedIds(excludeId).slice(0, maxItems);
if (ids.length === 0) return;
fetch('/api/products/by-ids', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }),
})
.then(r => r.json())
.then(data => {
// Preserve the viewed order (API may return in any order)
const map = new Map(data.products.map(p => [p.id, p]));
setProducts(ids.map(id => map.get(id)).filter(Boolean));
})
.catch(() => {}); // Non-critical — fail silently
}, [excludeId, maxItems]);
if (products.length === 0) return null;
return (
<section aria-label="Recently viewed">
<h2>Recently Viewed</h2>
<div className="recently-viewed-grid">
{products.map(product => (
<article key={product.id}>
<a href={product.url}>
<img src={product.image} alt={product.name} loading="lazy" width="150" height="150" />
<p>{product.name}</p>
<p>${product.price}</p>
</a>
</article>
))}
</div>
</section>
);
}
一括商品エンドポイント(Node.js):
// api/products/by-ids.js
export async function getProductsByIds(req, res) {
const { ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0 || ids.length > 20)
return res.status(400).json({ error: 'Invalid ids' });
const products = await db.products.findMany({
where: { id: { in: ids }, published: true },
select: { id: true, name: true, price: true, image: true, url: true, inStock: true },
});
res.json({ products });
}
ベストプラクティス
- 商品オブジェクト全体ではなく、商品IDだけを保存する — 価格や在庫状況は変わるため、ウィジェットの表示時には必ずサーバーから再取得します
- 現在の商品を除外する —
excludeIdを渡して、購入者が現在閲覧している商品が表示されないようにします - エラーを表に出さない — 最近閲覧した商品ウィジェットは重要度が低いため、すべてのエラーを捕捉し、ページを壊すのではなく何も表示しないようにします
- マウント後にデータを読み込む — SSRフレームワークでは、サーバーとクライアントの不一致を避けるため、
useEffect内でlocalStorageから読み込みます - プライバシーへの同意を尊重する — Cookieポリシーで必要とされる場合は、ユーザーが分析用Cookieに同意した後にのみストレージへ書き込みます
- 有効期限を検討する — 販売終了商品が表示されないよう、読み込み時に30日より古い項目を削除します
よくある問題
| 問題 | 解決策 |
|---|---|
| Next.js/SSRでハイドレーションの不一致が起きる | localStorageはuseEffect内でのみ読み込み、レンダリング中には読み込みません。状態は空の配列で初期化します |
プライベートブラウジングでlocalStorageがエラーをスローする | すべての読み書きをtry/catchで囲み、現在のセッションではメモリ内配列にフォールバックします |
| 在庫切れまたは削除済みの商品がウィジェットに表示される | サーバーのレスポンスをフィルタリングし、published: trueの商品だけを含めます。保存されたIDが現在の商品カタログを反映しているとは限りません |
| 同じ商品が複数の位置に表示される | 先頭に追加する前に、その商品IDの既存の項目を除外します |
| 読み込み時にウィジェットによるレイアウトシフトが起きる | データ読み込み中は最小の高さを持つスケルトンでウィジェットの高さを確保するか、スクロールせずに見える範囲より下に配置します |
関連スキル
- @wishlist-save-for-later
- @product-page-design
- @product-comparison
- @storefront-theming