1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsで高速eコマースアプリを構築する | エピソード3: 商品詳細ページとSEO最適化

Posted at

こんにちは!前回のエピソードでは、Shopify Storefront APIをNext.jsに統合し、商品一覧ページ(PLP)を構築しました。今回は、商品詳細ページ(PDP: Product Detail Page)を作成し、SEO最適化や画像の遅延読み込みを追加します。Next.jsの動的ルーティングやメタタグの設定を通じて、検索エンジンでの可視性とパフォーマンスを向上させましょう!

このエピソードのゴール

  • 動的ルーティングを使ってPDPを構築。
  • getStaticPathsgetStaticPropsで静的生成(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コンポーネントは、自動リサイズや遅延読み込みを提供します。上記のコードでは:

  • widthheightを指定して画像のサイズを最適化。
  • 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: 動作確認

  1. 開発サーバーを起動(npm run dev)。
  2. http://localhost:3000/products/商品のハンドルにアクセス(例: /products/t-shirt)。
  3. 以下の点を確認:
    • 商品のタイトル、価格、説明、画像が正しく表示される。
    • バリエーションの選択肢が表示される。
    • ページのメタタグが正しく設定されている(デベロッパーツールで確認)。
    • 画像が遅延読み込みされ、レスポンシブに表示される。

エラーがあれば、Shopify APIのレスポンスやハンドルを確認してください。


まとめと次のステップ

このエピソードでは、動的ルーティングとgetStaticPropsを使ってPDPを構築し、SEO最適化と画像最適化を実装しました。Next.js Imageやメタタグの設定により、パフォーマンスと検索エンジンの可視性が向上しました。

次回のエピソードでは、カート機能を構築し、React ContextまたはZustandを使って状態管理を行います。商品の追加や数量変更の処理も実装しますので、引き続きお楽しみに!


この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?