TEXTBOOK SECTION / AI LEARNING

商品詳細ページを作る

Square API × Next.js 実践EC開発|自社ECを低コストで作る、決済・在庫・注文管理の設計と実装の「最小構成のECを作る」より、商品詳細ページを作るを解説。生成AI、AI活用、DX、業務改善を実践しながら学べるオンライン教材です。

3最小構成のECを作るEC

OVERVIEW

この節で学べること

概要を表示する
項目内容
教材名Square API × Next.js 実践EC開発|自社ECを低コストで作る、決済・在庫・注文管理の設計と実装
最小構成のECを作る
商品詳細ページを作る
カテゴリEC
学習内容生成AI、AI活用、DX、業務改善を実践しながら理解するための教材です。

TABLE OF CONTENTS

目次

CONTENT

ここから

この章でわかること

この章では、商品ごとの詳細ページを作ります。

第5章では、商品一覧ページを作りました。

商品一覧ページは、商品を見つけてもらう場所です。

一方で、商品詳細ページは、

この商品を買うかどうかを判断してもらう場所です。

そのため、商品詳細ページには、一覧ページよりも多くの情報を載せます。

商品画像
商品名
価格
商品説明
在庫状態
配送情報
購入ボタン
関連商品
SEO情報
構造化データ

攻略本でいうと、商品一覧ページは「ショップの棚」です。

商品詳細ページは「商品を手に取って説明を聞く場所」です。

ここで不安が減れば、購入ボタンへ進んでもらいやすくなります。


6.1 Dynamic Routeで商品詳細ページを作る

商品詳細ページは、商品ごとにURLが変わります。

たとえば、次のようなURLです。

/products/classic-dashi
/products/gift-dashi-set
/products/limited-box

このように、URLの一部が商品ごとに変わるページには、Dynamic Routeを使います。

Next.js公式ドキュメントでは、事前に正確なルート名がわからない場合や、動的データからルートを生成したい場合にDynamic Segmentsを使うと説明されています。[slug]のようなフォルダ名を使うことで、動的なURLを作れます。(nextjs.org)

商品詳細ページのファイル構成は次のようになります。

src
└─ app
   └─ products
      ├─ page.tsx
      └─ [slug]
         └─ page.tsx

[slug]が、商品ごとに変わる部分です。

/products/classic-dashi
↓
slug = classic-dashi

/products/gift-dashi-set
↓
slug = gift-dashi-set

商品を取得する関数を作る

まず、商品データからslugに一致する商品を探す関数を作ります。

src/data/products.tsに追加します。

import type { Product } from '@/types/product';

export const products: Product[] = [
  {
    id: 'product_001',
    slug: 'classic-dashi',
    name: 'クラシックだし',
    description: '毎日の味噌汁や煮物に使いやすい、香り高いだしパックです。',
    price: 1200,
    currency: 'JPY',
    imageUrl: '/images/products/classic-dashi.jpg',
    isAvailable: true,
  },
  {
    id: 'product_002',
    slug: 'gift-dashi-set',
    name: 'ギフトだしセット',
    description: '大切な方への贈り物に使いやすい、上品なギフトセットです。',
    price: 3500,
    currency: 'JPY',
    imageUrl: '/images/products/gift-dashi-set.jpg',
    isAvailable: false,
  },
];

/**
 * 役割: URLのslugから該当する商品を取得する
 * 入力: 商品URLに使うslug
 * 出力: 一致するProduct。存在しない場合はundefined
 */
export function getProductBySlug(slug: string): Product | undefined {
  return products.find((product) => product.slug === slug);
}

商品詳細ページを作る

src/app/products/[slug]/page.tsxを作ります。

import { notFound } from 'next/navigation';
import { getProductBySlug } from '@/data/products';
import { formatPrice } from '@/utils/formatPrice';

type ProductDetailPageProps = {
  params: Promise<{
    slug: string;
  }>;
};

/**
 * 役割: 商品slugに応じた商品詳細ページを表示する
 * 入力: URLパラメータとして渡される商品slug
 * 出力: 商品名、説明、価格、販売状態を含む商品詳細ページ
 */
export default async function ProductDetailPage({
  params,
}: ProductDetailPageProps) {
  const { slug } = await params;
  const product = getProductBySlug(slug);

  if (!product) {
    notFound();
  }

  return (
    <main className="mx-auto max-w-6xl px-4 py-12">
      <div className="grid gap-10 lg:grid-cols-2">
        <div className="rounded-2xl bg-gray-100 p-8">
          <p className="text-sm text-gray-500">商品画像エリア</p>
        </div>

        <section className="space-y-6">
          <div className="space-y-3">
            <p className="text-sm font-semibold text-gray-500">Product</p>
            <h1 className="text-3xl font-bold tracking-tight text-gray-900">
              {product.name}
            </h1>
            <p className="text-xl font-semibold text-gray-900">
              {formatPrice(product.price, product.currency)}
            </p>
          </div>

          <p className="leading-8 text-gray-700">{product.description}</p>

          <p className="text-sm font-semibold">
            {product.isAvailable ? '販売中' : '現在売り切れです'}
          </p>
        </section>
      </div>
    </main>
  );
}

攻略ポイント

Dynamic Routeは、商品ページを自動で増やす仕組みです。

商品データが増える
↓
slugが増える
↓
商品詳細ページも増える

1商品ごとに手作業でページを作る必要はありません。


6.2 商品スラッグの設計

スラッグとは、URLに使う文字列です。

たとえば、次のURLではclassic-dashiがスラッグです。

/products/classic-dashi

スラッグは、商品ページの住所のようなものです。

一度公開したURLは、できるだけ変えないほうがよいです。

理由は、検索エンジンやSNS、外部リンクに影響するからです。

よいスラッグ

classic-dashi
gift-dashi-set
limited-summer-box
organic-green-tea

避けたいスラッグ

product1
item-abc
新商品
テスト商品
20260706item

日本語スラッグも技術的には使えますが、URL共有時にエンコードされて読みにくくなる場合があります。

そのため、本書では英小文字とハイフンを使います。

使う文字
↓
英小文字
数字
ハイフン

避ける文字
↓
スペース
記号
絵文字
長すぎる文字列

スラッグ設計のルール

短くする
商品内容がわかる
英小文字にする
単語はハイフンでつなぐ
公開後はなるべく変えない

攻略ポイント

スラッグは、商品ページの名前ではなく、住所です。

住所を頻繁に変えると、お客様も検索エンジンも迷います。

商品名
↓
変更してもよい

slug
↓
なるべく変更しない

商品名の表現を後から変える可能性がある場合でも、スラッグは安定した名前にしておきます。


6.3 商品説明、価格、在庫、配送情報の表示

商品詳細ページでは、購入判断に必要な情報を表示します。

一覧ページでは、情報を絞りました。

詳細ページでは、もう少し丁寧に伝えます。

何の商品か
いくらか
今買えるか
いつ届くか
どう使うか
誰に向いているか

Product型を拡張する

配送情報や詳しい説明を表示するために、商品型を少し拡張します。

src/types/product.tsを更新します。

export type Product = {
  id: string;
  slug: string;
  name: string;
  shortDescription: string;
  description: string;
  price: number;
  currency: 'JPY';
  imageUrl: string;
  isAvailable: boolean;
  stockLabel: string;
  shippingLabel: string;
};

商品データの例です。

export const products: Product[] = [
  {
    id: 'product_001',
    slug: 'classic-dashi',
    name: 'クラシックだし',
    shortDescription: '毎日の味噌汁や煮物に使いやすい、香り高いだしパックです。',
    description:
      'クラシックだしは、日々の料理に使いやすいだしパックです。味噌汁、煮物、うどん、炊き込みご飯など、家庭料理に自然になじみます。',
    price: 1200,
    currency: 'JPY',
    imageUrl: '/images/products/classic-dashi.jpg',
    isAvailable: true,
    stockLabel: '在庫あり',
    shippingLabel: '通常3営業日以内に発送',
  },
];

詳細ページに表示する

<div className="space-y-4 rounded-xl border border-gray-200 p-4">
  <div className="flex items-center justify-between gap-4">
    <span className="text-sm text-gray-500">在庫状況</span>
    <span className="text-sm font-semibold text-gray-900">
      {product.stockLabel}
    </span>
  </div>

  <div className="flex items-center justify-between gap-4">
    <span className="text-sm text-gray-500">配送目安</span>
    <span className="text-sm font-semibold text-gray-900">
      {product.shippingLabel}
    </span>
  </div>
</div>

表示する情報の優先順位

商品詳細ページでは、何でも載せればよいわけではありません。

購入前に必要な情報を、見やすい順番で並べます。

1. 商品画像
2. 商品名
3. 価格
4. 短い説明
5. 在庫状態
6. 配送情報
7. 購入ボタン
8. 詳しい説明
9. FAQ

攻略ポイント

商品詳細ページの役割は、購入前の不安を減らすことです。

何の商品かわからない
↓
説明する

いくらかわからない
↓
価格をはっきり出す

買えるかわからない
↓
在庫状態を出す

いつ届くかわからない
↓
配送目安を出す

お客様が迷う理由を、ページの中で先に解消します。


6.4 購入ボタンの設計

商品詳細ページで最も重要なパーツが、購入ボタンです。

購入ボタンは、ただのボタンではありません。

商品ページから決済へ進むための入口です。

最初は、購入ボタンを押したらSquareの決済ページへ進む構成にします。

商品詳細ページ
↓
購入ボタン
↓
Next.jsのAPI
↓
Square決済ページ

購入ボタンの基本設計

購入ボタンでは、次の状態を考えます。

販売中
↓
購入ボタンを表示する

売り切れ
↓
購入ボタンを無効化する

処理中
↓
ボタンを押せないようにする

src/components/product/BuyButton.tsxを作ります。

'use client';

import { useState } from 'react';

type BuyButtonProps = {
  productId: string;
  isAvailable: boolean;
};

/**
 * 役割: 商品購入処理を開始するボタンを表示する
 * 入力: 商品ID、販売可能状態
 * 出力: 販売中なら購入ボタン、売り切れなら無効化されたボタン
 */
export function BuyButton({ productId, isAvailable }: BuyButtonProps) {
  const [isLoading, setIsLoading] = useState(false);

  async function handleClick(): Promise<void> {
    if (!isAvailable || isLoading) {
      return;
    }

    setIsLoading(true);

    try {
      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ productId }),
      });

      if (!response.ok) {
        throw new Error('決済ページの作成に失敗しました。');
      }

      const data: unknown = await response.json();

      if (!isCheckoutResponse(data)) {
        throw new Error('決済ページURLの形式が正しくありません。');
      }

      window.location.href = data.checkoutUrl;
    } catch {
      setIsLoading(false);
      alert('購入処理を開始できませんでした。時間をおいて再度お試しください。');
    }
  }

  return (
    <button
      type="button"
      onClick={handleClick}
      disabled={!isAvailable || isLoading}
      className="w-full rounded-md bg-black px-6 py-3 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:bg-gray-300"
    >
      {isLoading ? '決済ページを準備中...' : isAvailable ? '購入する' : '売り切れ'}
    </button>
  );
}

type CheckoutResponse = {
  checkoutUrl: string;
};

/**
 * 役割: APIレスポンスが決済URLを含む形式か確認する
 * 入力: unknown型のAPIレスポンス
 * 出力: checkoutUrlを持つCheckoutResponseかどうか
 */
function isCheckoutResponse(value: unknown): value is CheckoutResponse {
  if (typeof value !== 'object' || value === null) {
    return false;
  }

  return (
    'checkoutUrl' in value &&
    typeof (value as { checkoutUrl: unknown }).checkoutUrl === 'string'
  );
}

ここでは、まだ/api/checkoutの中身は作りません。

第7章でSquare Checkout APIと接続します。

商品詳細ページで使う

import { BuyButton } from '@/components/product/BuyButton';

<BuyButton productId={product.id} isAvailable={product.isAvailable} />;

攻略ポイント

購入ボタンでは、ブラウザ側の情報を信用しすぎてはいけません。

たとえば、ブラウザ側でproductIdや価格を書き換えられる可能性があります。

そのため、決済金額や在庫状態は、必ずサーバー側で再確認します。

ブラウザ側
↓
購入したい商品IDを送る

サーバー側
↓
商品IDから正しい価格を取得する
在庫状態を確認する
Square決済リンクを作る

購入ボタンは入口です。

正しい判断はサーバー側で行います。


6.5 メタデータを商品ごとに生成する

商品詳細ページでは、商品ごとにSEO情報を変える必要があります。

たとえば、商品名が違えば、ページタイトルも変わります。

/products/classic-dashi
↓
クラシックだし|商品詳細

/products/gift-dashi-set
↓
ギフトだしセット|商品詳細

Next.jsでは、動的なメタデータをgenerateMetadataで生成できます。

Next.js公式ドキュメントでは、静的なmetadataオブジェクトまたは動的なgenerateMetadata関数を使って、SEOやWeb共有向けのメタデータを定義できると説明されています。generateMetadataは動的ルートのparamsなどを受け取り、ページごとのメタデータ生成に利用できます。(nextjs.org, nextjs.org)

src/app/products/[slug]/page.tsxに追加します。

import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getProductBySlug } from '@/data/products';
import { formatPrice } from '@/utils/formatPrice';

type ProductDetailPageProps = {
  params: Promise<{
    slug: string;
  }>;
};

export async function generateMetadata({
  params,
}: ProductDetailPageProps): Promise<Metadata> {
  const { slug } = await params;
  const product = getProductBySlug(slug);

  if (!product) {
    return {
      title: '商品が見つかりません',
    };
  }

  return {
    title: `${product.name}|商品詳細`,
    description: product.shortDescription,
    openGraph: {
      title: `${product.name}|商品詳細`,
      description: product.shortDescription,
      images: [
        {
          url: product.imageUrl,
          alt: product.name,
        },
      ],
    },
  };
}

メタデータで設定するもの

商品ページでは、最低限次の情報を設定します。

title
description
openGraph.title
openGraph.description
openGraph.images

SNSやLINEで商品ページが共有されたとき、OGP画像やタイトルが表示されます。

そのため、商品ごとに適切なメタデータを出すことが大切です。

攻略ポイント

商品詳細ページは、検索とSNSの入口にもなります。

検索結果
↓
titleとdescriptionが大切

SNS共有
↓
OGP画像とタイトルが大切

商品ページ内
↓
本文と購入ボタンが大切

ページの中身だけでなく、外から見えたときの印象も整えます。


6.6 構造化データを追加する

構造化データとは、検索エンジンやAIにページの意味を伝えるためのデータです。

Next.js公式ドキュメントでは、JSON-LDは検索エンジンやAIがページ構造を理解するために使える構造化データ形式だと説明されています。また、Next.jsではscriptタグでapplication/ld+jsonを出力する実装例が紹介されています。(nextjs.org)

商品ページでは、商品名、説明、画像、価格、在庫状態などを構造化データとして出せます。

JSON-LDを作る

商品詳細ページ内で、JSON-LDを作ります。

const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'Product',
  name: product.name,
  description: product.shortDescription,
  image: product.imageUrl,
  offers: {
    '@type': 'Offer',
    price: product.price,
    priceCurrency: product.currency,
    availability: product.isAvailable
      ? 'https://schema.org/InStock'
      : 'https://schema.org/OutOfStock',
  },
};

ページ内に出力します。

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c'),
  }}
/>

どこに入れるか

商品詳細ページのreturnの中に入れます。

return (
  <>
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c'),
      }}
    />

    <main className="mx-auto max-w-6xl px-4 py-12">
      {/* 商品詳細UI */}
    </main>
  </>
);

注意点

構造化データは、検索順位を必ず上げる魔法ではありません。

あくまで、検索エンジンにページ内容を正しく伝えるための補助です。

また、ページに表示していない内容を構造化データだけに入れるのは避けます。

よい例
↓
ページに表示している商品名、価格、在庫状態をJSON-LDにも入れる

悪い例
↓
ページに存在しないレビュー評価や架空の在庫情報を入れる

攻略ポイント

構造化データは、検索エンジン向けの説明書です。

ユーザー向け
↓
ページ本文

検索エンジン向け
↓
JSON-LD

両方の内容が矛盾しないようにします。


6.7 関連商品を表示する

商品詳細ページでは、関連商品を表示すると回遊しやすくなります。

お客様がその商品を買わなかったとしても、別の商品に興味を持ってもらえる可能性があります。

商品詳細を見る
↓
少し違うと感じる
↓
関連商品を見る
↓
別の商品に進む

最初は、同じ商品以外を数件表示するだけでも十分です。

関連商品を取得する関数

src/data/products.tsに追加します。

/**
 * 役割: 現在の商品を除外して関連商品を取得する
 * 入力: 現在表示している商品ID、取得件数
 * 出力: 関連商品として表示するProduct配列
 */
export function getRelatedProducts(
  currentProductId: string,
  limit: number,
): Product[] {
  return products
    .filter((product) => product.id !== currentProductId)
    .slice(0, limit);
}

詳細ページで表示する

import { ProductCard } from '@/components/product/ProductCard';
import { getProductBySlug, getRelatedProducts } from '@/data/products';

const relatedProducts = getRelatedProducts(product.id, 3);

表示部分です。

{relatedProducts.length > 0 && (
  <section className="mt-16 space-y-6">
    <div className="space-y-2">
      <p className="text-sm font-semibold text-gray-500">Related Products</p>
      <h2 className="text-2xl font-bold tracking-tight text-gray-900">
        関連商品
      </h2>
    </div>

    <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
      {relatedProducts.map((relatedProduct) => (
        <ProductCard key={relatedProduct.id} product={relatedProduct} />
      ))}
    </div>
  </section>
)}

関連商品の選び方

最初は単純で大丈夫です。

現在の商品以外を表示する
同じカテゴリの商品を表示する
価格帯が近い商品を表示する
ギフト商品を表示する
売れ筋商品を表示する

商品数が増えてきたら、カテゴリやタグを使って関連商品を選びます。

攻略ポイント

関連商品は、押し売りではなく案内です。

この商品が違うかもしれない
↓
では、こちらもあります

商品詳細ページからの離脱を減らすための導線として使います。


6.8 商品詳細ページの読み込み速度を上げる

商品詳細ページは、速く表示されることが大切です。

特にECでは、ページ表示が遅いと購入前に離脱されやすくなります。

Next.jsでは、動的なルートを事前生成するためにgenerateStaticParamsを使えます。公式ドキュメントでは、Dynamic Route Segmentsと組み合わせて、リクエスト時ではなくビルド時にルートを静的生成できると説明されています。(nextjs.org)

generateStaticParamsを使う

商品データが静的に決まっている場合は、商品詳細ページをビルド時に生成できます。

src/app/products/[slug]/page.tsxに追加します。

import { products } from '@/data/products';

export function generateStaticParams(): Array<{ slug: string }> {
  return products.map((product) => ({
    slug: product.slug,
  }));
}

これにより、商品ごとのページを事前に用意しやすくなります。

products配列
↓
各slugを取得
↓
商品詳細ページを事前生成

画像を最適化する

商品詳細ページでは、大きな商品画像を表示します。

ここでもnext/imageを使います。

Next.js公式ドキュメントでは、ImageコンポーネントはHTMLのimg要素を拡張し、画像の自動最適化を提供すると説明されています。また、画像読み込み時のレイアウトシフトを防ぐため、サイズ指定やfill利用時の親要素設定が重要です。(nextjs.org, nextjs.org)

import Image from 'next/image';

<div className="relative aspect-square overflow-hidden rounded-2xl bg-gray-100">
  <Image
    src={product.imageUrl}
    alt={product.name}
    fill
    priority
    sizes="(min-width: 1024px) 50vw, 100vw"
    className="object-cover"
  />
</div>

ファーストビューに出るメイン画像には、必要に応じてpriorityを指定します。

ただし、すべての画像にpriorityを付けるわけではありません。

メイン画像
↓
priorityを検討する

関連商品画像
↓
通常読み込みでよい

Client Componentを増やしすぎない

商品詳細ページの本文や商品説明は、Server Componentで表示します。

Client Componentにするのは、購入ボタンのようにユーザー操作が必要な部分だけです。

Server Component
↓
商品名、説明、価格、画像、配送情報

Client Component
↓
購入ボタン、数量変更、カート操作

Client Componentを増やしすぎると、ブラウザで動くJavaScriptが増え、ページが重くなる可能性があります。

攻略ポイント

商品詳細ページの高速化は、次の3つを意識します。

商品ページを事前生成する
画像を最適化する
Client Componentを必要最小限にする

特に、商品説明のように変わりにくい情報は、なるべく軽く表示します。

一方で、在庫や購入処理のように正確性が必要な部分は、購入ボタンを押したタイミングでサーバー側確認を行います。


完成イメージ

ここまでの内容を組み合わせると、商品詳細ページは次のような構成になります。

import type { Metadata } from 'next';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { BuyButton } from '@/components/product/BuyButton';
import { ProductCard } from '@/components/product/ProductCard';
import {
  getProductBySlug,
  getRelatedProducts,
  products,
} from '@/data/products';
import { formatPrice } from '@/utils/formatPrice';

type ProductDetailPageProps = {
  params: Promise<{
    slug: string;
  }>;
};

export function generateStaticParams(): Array<{ slug: string }> {
  return products.map((product) => ({
    slug: product.slug,
  }));
}

export async function generateMetadata({
  params,
}: ProductDetailPageProps): Promise<Metadata> {
  const { slug } = await params;
  const product = getProductBySlug(slug);

  if (!product) {
    return {
      title: '商品が見つかりません',
    };
  }

  return {
    title: `${product.name}|商品詳細`,
    description: product.shortDescription,
    openGraph: {
      title: `${product.name}|商品詳細`,
      description: product.shortDescription,
      images: [
        {
          url: product.imageUrl,
          alt: product.name,
        },
      ],
    },
  };
}

/**
 * 役割: 商品slugに応じた商品詳細ページを表示する
 * 入力: URLパラメータとして渡される商品slug
 * 出力: 商品画像、商品説明、価格、在庫、配送情報、購入ボタン、関連商品を含むページ
 */
export default async function ProductDetailPage({
  params,
}: ProductDetailPageProps) {
  const { slug } = await params;
  const product = getProductBySlug(slug);

  if (!product) {
    notFound();
  }

  const relatedProducts = getRelatedProducts(product.id, 3);

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    description: product.shortDescription,
    image: product.imageUrl,
    offers: {
      '@type': 'Offer',
      price: product.price,
      priceCurrency: product.currency,
      availability: product.isAvailable
        ? 'https://schema.org/InStock'
        : 'https://schema.org/OutOfStock',
    },
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(jsonLd).replace(/</g, '\\u003c'),
        }}
      />

      <main className="mx-auto max-w-6xl px-4 py-12">
        <div className="grid gap-10 lg:grid-cols-2">
          <div className="relative aspect-square overflow-hidden rounded-2xl bg-gray-100">
            <Image
              src={product.imageUrl}
              alt={product.name}
              fill
              priority
              sizes="(min-width: 1024px) 50vw, 100vw"
              className="object-cover"
            />
          </div>

          <section className="space-y-6">
            <div className="space-y-3">
              <p className="text-sm font-semibold text-gray-500">Product</p>

              <h1 className="text-3xl font-bold tracking-tight text-gray-900">
                {product.name}
              </h1>

              <p className="text-2xl font-semibold text-gray-900">
                {formatPrice(product.price, product.currency)}
              </p>
            </div>

            <p className="leading-8 text-gray-700">{product.description}</p>

            <div className="space-y-4 rounded-xl border border-gray-200 p-4">
              <div className="flex items-center justify-between gap-4">
                <span className="text-sm text-gray-500">在庫状況</span>
                <span className="text-sm font-semibold text-gray-900">
                  {product.stockLabel}
                </span>
              </div>

              <div className="flex items-center justify-between gap-4">
                <span className="text-sm text-gray-500">配送目安</span>
                <span className="text-sm font-semibold text-gray-900">
                  {product.shippingLabel}
                </span>
              </div>
            </div>

            <BuyButton
              productId={product.id}
              isAvailable={product.isAvailable}
            />
          </section>
        </div>

        {relatedProducts.length > 0 && (
          <section className="mt-16 space-y-6">
            <div className="space-y-2">
              <p className="text-sm font-semibold text-gray-500">
                Related Products
              </p>
              <h2 className="text-2xl font-bold tracking-tight text-gray-900">
                関連商品
              </h2>
            </div>

            <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
              {relatedProducts.map((relatedProduct) => (
                <ProductCard key={relatedProduct.id} product={relatedProduct} />
              ))}
            </div>
          </section>
        )}
      </main>
    </>
  );
}

この章のまとめ

この章では、商品詳細ページを作りました。

商品一覧ページは、商品を探してもらう場所です。

商品詳細ページは、購入を判断してもらう場所です。

この章で作った内容は、次の通りです。

Dynamic Routeで商品ごとのページを作る
slugから商品データを取得する
商品説明、価格、在庫、配送情報を表示する
購入ボタンを設計する
商品ごとにメタデータを生成する
JSON-LDで構造化データを追加する
関連商品を表示する
generateStaticParamsで読み込み速度を上げる
画像をnext/imageで最適化する

商品詳細ページでは、ユーザーの不安を減らすことが大切です。

何の商品か
いくらか
買えるのか
いつ届くのか
どこから購入するのか

これらがわかりやすく表示されていると、購入ボタンへ進んでもらいやすくなります。

また、商品詳細ページはSEOやSNS共有の入口にもなります。

そのため、generateMetadataやJSON-LDを使って、ページの意味を外部にも正しく伝えます。


次の章でやること

次の章では、いよいよSquare Checkout APIを使って購入導線を作ります。

これまでは、商品を表示するところまで作りました。

次は、購入ボタンを押したときにSquareの決済ページを作成し、ユーザーを決済へ進めます。

ここから、ECサイトが実際に「販売できる仕組み」へ近づいていきます。

FAQ

よくある質問

商品詳細ページを作るは医療関係者向けだけの内容ですか。
医療分野の例が含まれる場合もありますが、医療関係者だけに限定した内容ではありません。生成AI、AI活用、DX、業務改善、プロトタイプ開発など、一般的なAI学習の事例として読める内容です。
AI初心者でも読めますか。
はい。AIをこれから学ぶ方、数学が苦手な方、仕事でAIを使いたい方にも読み進めやすいように、教材の章と節の流れに沿って整理しています。
サムネイル画像は必ず表示されますか。
はい。教材にcoverUrlが設定されている場合はその画像を表示し、未設定の場合は代替サムネイル画像を表示します。
Square API × Next.js 実践EC開発|自社ECを低コストで作る、決済・在庫・注文管理の設計と実装のほかの章も読めますか。
はい。教材トップから章立てを確認でき、前後の節へもページ下部のナビゲーションから移動できます。