こんにちは!前回のエピソードでは、Shopify Storefront APIをNext.jsに統合し、商品一覧ページ(PLP)を構築しました。今回は、商品詳細ページ(PDP: Product Detail Page)を作成し、SEO最適化や画像の遅延読み込みを追加します。Next.jsの動的ルーティングやメタタグの設定を通じて、検索エンジンでの可視性とパフォーマンスを向上させましょう!
このエピソードのゴール
- 動的ルーティングを使ってPDPを構築。
-
getStaticPaths
とgetStaticProps
で静的生成(SSG)を活用。 - Next.js Imageコンポーネントで画像を最適化。
- メタタグ(Open Graph、Twitter Cards)を追加してSEOを強化。
必要なもの
- 前回のプロジェクト(
next-ecommerce
)がセットアップ済み。 - Shopify Storefront APIのアクセストークンとエンドポイント。
- 基本的なTypeScript、React、GraphQLの知識。
ステップ1: 商品詳細データのクエリ作成
PDPでは、商品の詳細情報(説明、複数画像、バリエーションなど)を取得する必要があります。src/lib/queries.ts
に以下のGraphQLクエリを追加します:
export const GET_PRODUCT_BY_HANDLE_QUERY = `
query GetProductByHandle($handle: String!) {
productByHandle(handle: $handle) {
id
title
handle
descriptionHtml
priceRange {
minVariantPrice {
amount
currencyCode
}
}
images(first: 5) {
edges {
node {
url
altText
}
}
}
variants(first: 10) {
edges {
node {
id
title
price {
amount
currencyCode
}
}
}
}
}
}
`;
このクエリは、商品のハンドル(URLスラグ)に基づいて詳細データを取得します。商品の説明、複数画像、バリエーション(例: サイズやカラー)を含めます。
次に、src/lib/shopify.ts
に商品詳細を取得する関数を追加:
interface ProductDetail {
id: string;
title: string;
handle: string;
descriptionHtml: string;
priceRange: {
minVariantPrice: {
amount: string;
currencyCode: string;
};
};
images: {
edges: Array<{
node: {
url: string;
altText: string | null;
};
}>;
};
variants: {
edges: Array<{
node: {
id: string;
title: string;
price: {
amount: string;
currencyCode: string;
};
};
}>;
};
}
interface ProductDetailResponse {
productByHandle: ProductDetail | null;
}
export async function getProductByHandle(handle: string): Promise<ProductDetail | null> {
const data = await shopifyClient.request<ProductDetailResponse>(GET_PRODUCT_BY_HANDLE_QUERY, { handle });
return data.productByHandle;
}
この関数は、指定したハンドルの商品データを取得し、型安全に返します。
ステップ2: 動的ルーティングの設定
Next.jsの動的ルーティングを使って、PDPのURLを/products/[handle]
として構築します。src/app/products/[handle]/page.tsx
ファイルを作成し、以下のコードを追加:
import { getProductByHandle } from '@/lib/shopify';
import Image from 'next/image';
import { notFound } from 'next/navigation';
interface ProductDetail {
id: string;
title: string;
handle: string;
descriptionHtml: string;
priceRange: {
minVariantPrice: {
amount: string;
currencyCode: string;
};
};
images: {
edges: Array<{
node: {
url: string;
altText: string | null;
};
}>;
};
variants: {
edges: Array<{
node: {
id: string;
title: string;
price: {
amount: string;
currencyCode: string;
};
};
}>;
};
}
interface ProductPageProps {
params: { handle: string };
}
export async function getStaticPaths() {
// 実際のプロジェクトでは、すべての商品ハンドルを取得
// ここではサンプルとして空のリストを使用
return {
paths: [],
fallback: 'blocking', // ISRで動的生成
};
}
export async function getStaticProps({ params }: ProductPageProps) {
const product = await getProductByHandle(params.handle);
if (!product) {
return { notFound: true };
}
return {
props: {
product,
},
revalidate: 60, // ISR: 60秒ごとに再生成
};
}
export default function ProductPage({ product }: { product: ProductDetail }) {
if (!product) return notFound();
return (
<main className="container mx-auto p-4">
<div className="grid md:grid-cols-2 gap-8">
{/* 画像ギャラリー */}
<div className="space-y-4">
{product.images.edges.map((image, index) => (
<Image
key={index}
src={image.node.url}
alt={image.node.altText || product.title}
width={500}
height={500}
className="w-full h-auto object-cover rounded"
priority={index === 0} // 最初の画像は優先読み込み
/>
))}
</div>
{/* 商品情報 */}
<div>
<h1 className="text-3xl font-bold mb-4">{product.title}</h1>
<p className="text-2xl text-gray-700 mb-4">
{parseFloat(product.priceRange.minVariantPrice.amount).toFixed(2)}{' '}
{product.priceRange.minVariantPrice.currencyCode}
</p>
<div
className="prose mb-6"
dangerouslySetInnerHTML={{ __html: product.descriptionHtml }}
/>
<div className="mb-6">
<h2 className="text-lg font-semibold mb-2">バリエーション</h2>
<select className="border p-2 rounded w-full">
{product.variants.edges.map((variant) => (
<option key={variant.node.id} value={variant.node.id}>
{variant.node.title} - {parseFloat(variant.node.price.amount).toFixed(2)}{' '}
{variant.node.price.currencyCode}
</option>
))}
</select>
</div>
<button className="bg-primary text-white px-6 py-3 rounded hover:bg-opacity-90">
カートに追加
</button>
</div>
</div>
</main>
);
}
このコードは:
-
getStaticPaths
で動的ルートを定義(fallback: 'blocking'
でISRを有効化)。 -
getStaticProps
で商品データを取得し、商品が存在しない場合は404を返します。 - Tailwind CSSとNext.js Imageを使ってレスポンシブなPDPを構築。
- 商品の説明を
descriptionHtml
で安全にレンダリング。
注意: getStaticPaths
ではサンプルとして空のリストを使用しています。本番では、すべての商品ハンドルを事前に取得する必要があります。
ステップ3: SEO最適化
SEOを強化するため、メタタグ(Open Graph、Twitter Cards)を追加します。src/app/products/[handle]/page.tsx
の先頭に以下のコードを追加:
import Head from 'next/head';
// ... 既存のコード ...
export default function ProductPage({ product }: { product: ProductDetail }) {
if (!product) return notFound();
const ogImage = product.images.edges[0]?.node.url || '';
return (
<>
<Head>
<title>{product.title} | Next.js eCommerce</title>
<meta name="description" content={product.descriptionHtml.slice(0, 160)} />
<meta property="og:title" content={product.title} />
<meta property="og:description" content={product.descriptionHtml.slice(0, 160)} />
<meta property="og:image" content={ogImage} />
<meta property="og:type" content="product" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={product.title} />
<meta name="twitter:description" content={product.descriptionHtml.slice(0, 160)} />
<meta name="twitter:image" content={ogImage} />
</Head>
<main className="container mx-auto p-4">
{/* 既存のメインコンテンツ */}
</main>
</>
);
}
このコードは:
- ページのタイトルと説明を動的に設定。
- Open GraphとTwitter Cardsのメタタグを追加して、ソーシャルメディアでの共有を最適化。
- 商品の最初の画像をOG画像として使用。
ステップ4: 画像最適化
Next.js Imageコンポーネントは、自動リサイズや遅延読み込みを提供します。上記のコードでは:
-
width
とheight
を指定して画像のサイズを最適化。 -
priority
を最初の画像に設定してLCP(Largest Contentful Paint)を改善。 -
object-cover
で画像のアスペクト比を維持。
さらに、next.config.js
を更新してShopifyの画像ドメインを許可:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['cdn.shopify.com'],
},
};
module.exports = nextConfig;
これで、Shopifyの画像が正しく読み込まれます。
ステップ5: 動作確認
- 開発サーバーを起動(
npm run dev
)。 -
http://localhost:3000/products/商品のハンドル
にアクセス(例:/products/t-shirt
)。 - 以下の点を確認:
- 商品のタイトル、価格、説明、画像が正しく表示される。
- バリエーションの選択肢が表示される。
- ページのメタタグが正しく設定されている(デベロッパーツールで確認)。
- 画像が遅延読み込みされ、レスポンシブに表示される。
エラーがあれば、Shopify APIのレスポンスやハンドルを確認してください。
まとめと次のステップ
このエピソードでは、動的ルーティングとgetStaticProps
を使ってPDPを構築し、SEO最適化と画像最適化を実装しました。Next.js Imageやメタタグの設定により、パフォーマンスと検索エンジンの可視性が向上しました。
次回のエピソードでは、カート機能を構築し、React ContextまたはZustandを使って状態管理を行います。商品の追加や数量変更の処理も実装しますので、引き続きお楽しみに!
この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!