こんにちは!前回のエピソードでは、Core Web Vitalsを最適化し、Lighthouseスコアを90以上に引き上げました。今回は、ユーザー体験をさらに向上させるために、商品検索とフィルタリング機能を追加します。Algoliaを活用して高速な検索機能を実装し、価格やカテゴリに基づくフィルタリングも構築します。Next.jsとTailwind CSSを使って、直感的な検索UIをデザインしましょう!
このエピソードのゴール
- AlgoliaをNext.jsに統合して検索機能を実装。
- 価格とカテゴリに基づくフィルタリング機能を追加。
- 検索とフィルタリングの状態を管理(デバウンスを活用)。
- Tailwind CSSでレスポンシブな検索UIを構築。
必要なもの
- 前回のプロジェクト(
next-ecommerce
)がセットアップ済み。 - Algoliaアカウント(無料プランで十分)。
-
@algolia/client-search
パッケージ。 - 基本的なTypeScript、React、Next.jsの知識。
ステップ1: Algoliaのセットアップ
Algoliaは、高速で柔軟な検索エンジンです。まず、Algoliaアカウントを作成し、プロジェクトを準備します。
-
Algoliaアカウントの作成:
- Algolia公式サイトでアカウントを作成。
- 新しいアプリケーションを作成し、インデックス(例:
products
)を追加。 - APIキー(Application ID、Search-Only API Key、Admin API Key)を取得。
-
環境変数の設定:
.env.local
にAlgoliaのAPIキーを追加:
NEXT_PUBLIC_ALGOLIA_APP_ID=あなたのアプリケーションID
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY=あなたの検索専用APIキー
ALGOLIA_ADMIN_API_KEY=あなたの管理者APIキー
ステップ2: Algoliaクライアントの設定
Algoliaと通信するために、クライアントをセットアップします。必要なパッケージをインストール:
npm install algoliasearch @algolia/client-search
src/lib/algolia.ts
ファイルを作成し、以下のコードを追加:
import algoliasearch from 'algoliasearch/lite';
const appId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!;
const searchKey = process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!;
export const algoliaClient = algoliasearch(appId, searchKey);
export const searchIndex = algoliaClient.initIndex('products');
このコードは、Algoliaの検索クライアントを初期化し、products
インデックスにアクセスできるようにします。
ステップ3: 商品データをAlgoliaに同期
Shopifyの商品データをAlgoliaに同期するスクリプトを作成します。src/lib/syncToAlgolia.ts
ファイルを作成:
import { getProducts } from './shopify';
import algoliasearch from 'algoliasearch';
const appId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!;
const adminApiKey = process.env.ALGOLIA_ADMIN_API_KEY!;
const algoliaClient = algoliasearch(appId, adminApiKey);
const index = algoliaClient.initIndex('products');
export async function syncProductsToAlgolia() {
try {
const products = await getProducts(100); // 最大100商品を取得
const objects = products.map((product) => ({
objectID: product.id,
title: product.title,
handle: product.handle,
price: parseFloat(product.priceRange.minVariantPrice.amount),
currency: product.priceRange.minVariantPrice.currencyCode,
image: product.images.edges[0]?.node.url || '',
categories: product.tags || ['clothing', 'accessories'], // Shopifyのタグまたは仮データ
}));
await index.saveObjects(objects);
console.log('商品がAlgoliaに同期されました');
} catch (error) {
console.error('Algolia同期エラー:', error);
}
}
このスクリプトは、Shopifyの商品データを取得し、Algoliaのインデックスに保存します。プロジェクトの初期化時や商品更新時に実行します。ターミナルで以下を実行:
npx ts-node src/lib/syncToAlgolia.ts
注意: 本番環境では、Vercel FunctionsやGitHub Actionsを使って定期的な同期をスケジュールしてください。
ステップ4: 検索とフィルタリングUIの構築
商品一覧ページを更新し、検索バーとフィルタリング機能を追加します。src/app/page.tsx
を以下のように更新:
import { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import Image from 'next/image';
import { searchIndex } from '@/lib/algolia';
import { debounce } from '@/lib/utils';
interface Product {
objectID: string;
title: string;
handle: string;
price: number;
currency: string;
image: string;
categories: string[];
}
export default function Home() {
const [query, setQuery] = useState('');
const [products, setProducts] = useState<Product[]>([]);
const [priceFilter, setPriceFilter] = useState<number | null>(null);
const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const fetchProducts = useCallback(
debounce(async (searchQuery: string, price: number | null, category: string | null) => {
setLoading(true);
try {
const filters = [];
if (price !== null) filters.push(`price <= ${price}`);
if (category) filters.push(`categories:${category}`);
const { hits } = await searchIndex.search<Product>(searchQuery, {
filters: filters.join(' AND '),
});
setProducts(hits);
} catch (error) {
console.error('検索エラー:', error);
} finally {
setLoading(false);
}
}, 300),
[]
);
useEffect(() => {
fetchProducts(query, priceFilter, categoryFilter);
}, [query, priceFilter, categoryFilter, fetchProducts]);
return (
<main className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-6">商品一覧</h1>
{/* 検索バー */}
<div className="mb-6">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="商品を検索..."
className="w-full border p-2 rounded focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
{/* フィルタリング */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div>
<label className="block text-sm font-medium mb-1">価格上限</label>
<select
onChange={(e) => setPriceFilter(e.target.value ? Number(e.target.value) : null)}
className="border p-2 rounded w-full"
>
<option value="">すべて</option>
<option value="5000">5,000 JPY以下</option>
<option value="10000">10,000 JPY以下</option>
<option value="20000">20,000 JPY以下</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">カテゴリ</label>
<select
onChange={(e) => setCategoryFilter(e.target.value || null)}
className="border p-2 rounded w-full"
>
<option value="">すべて</option>
<option value="clothing">服</option>
<option value="accessories">アクセサリー</option>
</select>
</div>
</div>
{/* 商品リスト */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
{loading ? (
Array(6)
.fill(0)
.map((_, index) => (
<div key={index} className="border rounded-lg p-4 animate-pulse">
<div className="w-full h-48 bg-gray-200 rounded"></div>
<div className="h-6 bg-gray-200 mt-2 rounded"></div>
<div className="h-4 bg-gray-200 mt-2 rounded w-1/2"></div>
</div>
))
) : products.length === 0 ? (
<p className="text-gray-600">商品が見つかりませんでした。</p>
) : (
products.map((product) => (
<Link href={`/products/${product.handle}`} key={product.objectID}>
<div className="border rounded-lg p-4 hover:shadow-lg transition">
<Image
src={product.image}
alt={product.title}
width={200}
height={200}
className="w-full h-48 object-cover rounded"
loading="lazy"
style={{ aspectRatio: '1 / 1' }}
/>
<h2 className="text-xl font-semibold mt-2">{product.title}</h2>
<p className="text-gray-600">
{product.price.toFixed(2)} {product.currency}
</p>
</div>
</Link>
))
)}
</div>
</main>
);
}
このコードは:
- 検索バーとフィルタリングオプション(価格、カテゴリ)を追加。
- Algoliaの検索結果をリアルタイムで表示。
- スケルトンUIをローディング中に表示。
- Tailwind CSSでレスポンシブなUIを構築。
ステップ5: デバウンス関数の実装
検索リクエストを制限するために、デバウンス関数を追加します。src/lib/utils.ts
ファイルを作成:
export function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
この関数は、ユーザーの入力から300ms待機してから検索リクエストを送信し、過剰なAPI呼び出しを防ぎます。
ステップ6: 動作確認
- Algoliaに商品データを同期(
syncProductsToAlgolia
を実行)。 - 開発サーバーを起動(
npm run dev
)。 -
http://localhost:3000
にアクセスし、以下の点を確認:- 検索バーにキーワードを入力すると、関連商品がリアルタイムで表示される。
- 価格やカテゴリフィルタを選択すると、商品リストが適切に更新される。
- ローディング中にスケルトンUIが表示される。
- モバイルでもUIが適切に表示される。
- デベロッパーツールのネットワークタブで、検索リクエストがデバウンスされていることを確認。
エラーがあれば、AlgoliaダッシュボードでインデックスデータやAPIキーを確認してください。
まとめと次のステップ
このエピソードでは、Algoliaを使って高速な検索とフィルタリング機能を実装しました。デバウンスやスケルトンUIを活用することで、快適で効率的な検索体験を提供できました。
次回のエピソードでは、多言語対応(i18n)を追加し、グローバル市場向けにアプリを拡張します。next-intl
を使ったルーティングやSEO最適化も紹介しますので、引き続きお楽しみに!
この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!