メインコンテンツへスキップ
#Web制作 約8分で読めます

Next.js 15 Partial Pre-rendering でEコマースサイトのSEOを改善する実践ガイド

Next.js 15の新機能Partial Pre-renderingを使ってEコマースサイトのSEO・パフォーマンスを両立する実装方法を解説。商品詳細ページの最適化からビルド時間短縮まで実践テクニック満載。

Next.js 15 Partial Pre-renderingとは何か

Next.js 15で導入されたPartial Pre-rendering(PPR)は、静的なコンテンツと動的なコンテンツを同一ページ内で混在させるレンダリング手法です。従来のSSG(Static Site Generation)とSSR(Server-Side Rendering)の二者択一ではなく、ページ内の一部を静的に、残りを動的にレンダリングできます。

従来のEコマースサイトでは、商品詳細ページ全体をSSRにするか、完全に静的化するかの選択を迫られていました。SSRにすると初期表示が遅くなり、静的化すると在庫情報やユーザー固有データが反映されません。PPRはこのジレンマを解決します。

PPRの仕組み:

  • ページの静的部分(商品説明・画像など)をビルド時に生成
  • 動的部分(在庫数・カート状態など)はリクエスト時にサーバーでレンダリング
  • 静的部分を即座に表示し、動的部分はStreaming SSRで後から挿入
flowchart TD
    A["リクエスト受信"] --> B["静的シェルを即座に返却"]
    B --> C["ブラウザに初期HTML表示"]
    B --> D["サーバーで動的部分を生成"]
    D --> E["Streamingで動的コンテンツを挿入"]
    C --> F["完全な描画完了"]
    E --> F

上記フローで、ユーザーは静的コンテンツを待たずに閲覧開始でき、LCP(Largest Contentful Paint)が改善されます。

PPRは特にEコマースサイトで有効です。商品説明や画像は静的にキャッシュし、在庫数や価格は動的に取得することで、SEOとパフォーマンスを両立できます。

Eコマースサイトでの具体的な活用パターン

商品詳細ページの最適化

商品詳細ページは、Eコマースサイトで最も重要なSEO対象です。PPRを使えば、静的な商品情報(タイトル・説明・画像)をクローラーに即座に提供しつつ、動的なユーザー体験(在庫数・レビュー)を維持できます。

実装例(App Router):

// app/products/[id]/page.tsx
import { Suspense } from 'react';
import { ProductInfo } from '@/components/ProductInfo';
import { StockStatus } from '@/components/StockStatus';
import { Reviews } from '@/components/Reviews';

export const experimental_ppr = true; // PPRを有効化

export default async function ProductPage({ params }: { params: { id: string } }) {
  // 静的部分: ビルド時に取得
  const product = await fetchProductStatic(params.id);

  return (
    <div>
      {/* 静的コンテンツ: 即座に表示 */}
      <ProductInfo product={product} />

      {/* 動的コンテンツ: Streaming SSRで後から挿入 */}
      <Suspense fallback={<StockSkeleton />}>
        <StockStatus productId={params.id} />
      </Suspense>

      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={params.id} />
      </Suspense>
    </div>
  );
}

この実装のSEOメリット:

  • Googlebotは静的部分(商品名・説明・画像)を即座にクロール可能
  • 構造化データ(JSON-LD)を静的部分に含めればリッチリザルト対応
  • TTFBが短縮され、Core Web Vitalsが向上

カテゴリページのパフォーマンス最適化

カテゴリページでは、商品一覧は静的に生成し、フィルター結果やソート順は動的に処理する設計が有効です。

// app/category/[slug]/page.tsx
export const experimental_ppr = true;

export default async function CategoryPage({ params }: { params: { slug: string } }) {
  // 静的: 初期の商品一覧(デフォルト表示順)
  const products = await fetchCategoryProducts(params.slug);

  return (
    <div>
      <h1>{params.slug} カテゴリ</h1>
      
      {/* 静的な初期表示 */}
      <ProductGrid products={products} />

      {/* 動的: フィルター結果 */}
      <Suspense fallback={<FilterSkeleton />}>
        <FilteredResults slug={params.slug} />
      </Suspense>
    </div>
  );
}

ユーザー固有UIとSEOの両立

「カートに追加」ボタンや「お気に入り」アイコンなど、ユーザーログイン状態に依存するUIは動的にレンダリングし、商品情報は静的にすることでSEOクローラーには完全な情報を提供できます。

// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetchProductStatic(params.id);

  return (
    <>
      {/* 静的: SEO重要コンテンツ */}
      <script type="application/ld+json">
        {JSON.stringify({
          "@context": "https://schema.org",
          "@type": "Product",
          "name": product.name,
          "image": product.imageUrl,
          "description": product.description,
        })}
      </script>

      <h1>{product.name}</h1>
      <img src={product.imageUrl} alt={product.name} />

      {/* 動的: ユーザー依存UI */}
      <Suspense fallback={<ButtonSkeleton />}>
        <AddToCartButton productId={params.id} />
      </Suspense>
    </>
  );
}

構造化データとメタタグの最適配置

PPRを使う際、SEO重要要素(構造化データ・メタタグ)は必ず静的部分に配置する必要があります。動的部分に配置するとクローラーが認識できません。

構造化データの静的生成

// app/products/[id]/page.tsx
import { Metadata } from 'next';

export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> {
  const product = await fetchProductStatic(params.id);

  return {
    title: `${product.name} | ECサイト名`,
    description: product.description.slice(0, 160),
    openGraph: {
      title: product.name,
      description: product.description,
      images: [{ url: product.imageUrl }],
    },
  };
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetchProductStatic(params.id);

  const structuredData = {
    "@context": "https://schema.org",
    "@type": "Product",
    "name": product.name,
    "image": product.imageUrl,
    "description": product.description,
    "offers": {
      "@type": "Offer",
      "priceCurrency": "JPY",
      "price": product.price,
      "availability": "https://schema.org/InStock", // 静的な基本値
    },
  };

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
      />
      <ProductInfo product={product} />
    </>
  );
}

注意点:

  • generateMetadataはビルド時に実行されるため、動的データは含められない
  • 在庫状態など変動するデータはavailabilityを省略するか、クライアント側で更新する

動的在庫情報の後付け更新

構造化データのoffers.availabilityを動的に更新したい場合、クライアント側でJSON-LDを追加します。

// components/StockStatus.tsx
'use client';

import { useEffect } from 'react';

export function StockStatus({ productId }: { productId: string }) {
  useEffect(() => {
    fetch(`/api/stock/${productId}`)
      .then((res) => res.json())
      .then((data) => {
        const script = document.createElement('script');
        script.type = 'application/ld+json';
        script.textContent = JSON.stringify({
          "@context": "https://schema.org",
          "@type": "Product",
          "offers": {
            "@type": "Offer",
            "availability": data.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
          },
        });
        document.head.appendChild(script);
      });
  }, [productId]);

  return <div>在庫確認中...</div>;
}

ビルド時間とコストの最適化

PPRはビルド時に全ページを生成する必要がないため、大規模Eコマースサイトでビルド時間を大幅に削減できます。

generateStaticParamsとの併用

従来のISR(Incremental Static Regeneration)では、全商品ページを事前生成すると10万商品で数時間かかる場合がありました。PPRでは人気商品のみ静的生成し、残りはオンデマンド生成する戦略が有効です。

// app/products/[id]/page.tsx
export const experimental_ppr = true;
export const dynamicParams = true; // 未生成パスも許可

export async function generateStaticParams() {
  // 人気商品トップ1000のみビルド時に生成
  const popularProducts = await fetchPopularProducts(1000);
  return popularProducts.map((p) => ({ id: p.id }));
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  // 静的生成されていないページは初回アクセス時に生成
  const product = await fetchProductStatic(params.id);
  return <ProductInfo product={product} />;
}

ビルド時間の比較(10万商品の場合):

方式ビルド時間初回表示速度
全ページSSG4〜6時間高速(即座)
全ページSSR30秒遅い(600〜800ms)
PPR(人気1000のみ生成)5分高速(200〜300ms)

Edge Runtimeとの組み合わせ

動的部分をEdge Runtimeで処理すると、レイテンシをさらに削減できます。

// app/products/[id]/page.tsx
export const runtime = 'edge'; // Edge Runtimeを使用

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetchProductStatic(params.id);

  return (
    <>
      <ProductInfo product={product} />
      <Suspense fallback={<StockSkeleton />}>
        <StockStatus productId={params.id} /> {/* Edgeで動的生成 */}
      </Suspense>
    </>
  );
}

実装時の注意点とトラブルシューティング

Suspenseの配置ミス

PPRではSuspenseで囲んだ部分が動的になります。SEO重要要素を誤ってSuspense内に配置すると、クローラーが認識できません

❌ 誤った実装:

export default async function ProductPage({ params }: { params: { id: string } }) {
  return (
    <Suspense fallback={<Skeleton />}>
      {/* ❌ h1やメタ情報が動的になりSEOに悪影響 */}
      <h1>{product.name}</h1>
      <ProductInfo product={product} />
    </Suspense>
  );
}

✅ 正しい実装:

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetchProductStatic(params.id); // Suspense外で取得

  return (
    <>
      {/* ✅ 静的部分: SEO重要要素 */}
      <h1>{product.name}</h1>
      <ProductInfo product={product} />

      {/* ✅ 動的部分: ユーザー体験向上 */}
      <Suspense fallback={<StockSkeleton />}>
        <StockStatus productId={params.id} />
      </Suspense>
    </>
  );
}

キャッシュ戦略の見直し

PPRは静的部分をCDNでキャッシュしますが、動的部分のキャッシュ戦略も設計が必要です。

// app/api/stock/[id]/route.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
  const stock = await fetchStockFromDB(params.id);

  return Response.json(stock, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120', // 60秒キャッシュ
    },
  });
}

開発環境での動作確認

PPRは本番ビルド時のみ有効です。npm run devでは動作しないため、確認はnpm run build && npm startで行います。

# ローカルでPPRをテスト
npm run build
npm start

まとめ

Next.js 15のPartial Pre-renderingは、EコマースサイトのSEOとパフォーマンスを同時に最適化する強力な手法です。

重要ポイント:

  • 商品情報など静的コンテンツはビルド時に生成し、在庫状態など動的データはStreaming SSRで後から挿入
  • 構造化データ・メタタグは必ず静的部分に配置し、クローラーが認識できるようにする
  • generateStaticParamsで人気商品のみ事前生成し、ビルド時間を短縮
  • Suspenseの配置ミスに注意し、SEO重要要素を動的部分に含めない
  • Edge Runtimeとの併用で動的部分のレイテンシを最小化

PPRを活用することで、10万商品規模のEコマースサイトでも高速な初期表示とリッチなユーザー体験を両立できます。

参考リンク

#Next.js #SEO #Partial Pre-rendering #Eコマース #パフォーマンス最適化
シェア