Next.js App Routerで4万件の多言語動的サイトマップを実装する方法

# Next.js App Routerで4万件の多言語動的サイトマップを実装する方法

## はじめに

Next.js App Routerを使用した4万件以上のコンテンツを持つ大規模多言語対応サイトでの動的サイトマップ実装について共有する。

## 結論から

Next.jsのサイトマップAPIと24時間キャッシュを組み合わせることで、大規模サイトでも実用的な動的サイトマップを実装できた。
フロントエンドのビルド時間への影響を最小限に抑えつつ、日次でのデータ更新が可能となる。

## 開発環境

* Frontend: Next.js (App Router)
* Backend: Express + MongoDB
* Infrastructure: Vercel (Frontend), Heroku (Backend)
* 言語数: 2言語

## 状況

* ページが20,000件以上存在
* 各ページに2言語のバリエーション（計40,000 URL以上）
* データの更新が日次で発生
* SEO要件としてサイトマップの提供が必須

## サイトマップ生成について

### サイトマップ生成のアプローチ

* 静的生成：ビルド時に全URLを生成
* 動的生成：リクエスト時に都度生成
* ISR：静的生成と動的更新のハイブリッド

それぞれの特徴は以下の通り：

1. **静的生成**
   - レスポンス速度が速い
   - サーバー負荷が少ない
   - ビルド時間が長くなる
   - データ更新への即時対応が難しい

2. **動的生成**
   - 最新データの反映が容易
   - ビルド時間への影響なし
   - サーバー負荷が高い
   - レスポンス時間が遅くなる可能性

3. **ISR**
   - 静的生成と動的更新のバランスが取れる
   - キャッシュ制御が容易
   - 実装が若干複雑になる

Sitemap生成については過去の記事も参考に。
[Next.js App Routerでのnext-intlを用いた多言語Sitemap実装](https://qiita.com/s4yuba/items/67d5f19f2dfb60137e44)

## 解決方法

Next.jsのサイトマップAPIと24時間キャッシュを組み合わせた実装を採用した。

```typescript
// app/sitemap.ts
import { MetadataRoute } from "next";

const BASE_URL = process.env.NODE_ENV === "production" 
  ? "https://example.com"
  : "localhost:3000";

const API_BASE_URL = process.env.NODE_ENV === "production"
  ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/util`
  : "http://localhost:3500/api/util";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const locales = ["en", "ja"];
  const routes = ["/", "/about", "/products", "/contact"];
  
  // Generate static page
  const staticRoutes = routes.flatMap((route) =>
    locales.map((locale) => ({
      url: `${BASE_URL}/${locale}${route === "/" ? "" : route}`,
      lastModified: new Date(),
      changeFrequency: "monthly" as const,
      priority: route === "/" ? 1 : 0.8,
    }))
  );

  try {
    // product page
    const response = await fetch(`${API_BASE_URL}/products`, {
      next: { revalidate: 86400 },
      headers: { "Cache-Control": "public, max-age=86400" },
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch products: ${response.statusText}`);
    }

    const products = await response.json();

    // generate product page url
    const productRoutes = products.flatMap((product: any) =>
      locales.map((locale) => ({
        url: `${BASE_URL}/${locale}/product/${product.id}`,
        lastModified: new Date(product.updatedAt),
        changeFrequency: "daily" as const,
        priority: 0.7,
      }))
    );

    return [...staticRoutes, ...productRoutes];

  } catch (error) {
    console.error("Error generating sitemap:", error);
    return staticRoutes;
  }
}
```

Backend API:

```typescript
// Express route handler
router.get("/products", async (req, res) => {
  try {
    // Fetch data from MongoDB
    const products = await Product.find(
      {},
      {
        id: 1,
        lastUpdateDate: 1,
      }
    ).lean();

    res.json(products);
  } catch (error) {
    console.error("Failed to fetch products:", error);
    res.status(500).json({
      message: "Error fetching products",
      error: error instanceof Error ? error.message : String(error),
    });
  }
});
```

## 実装結果

* 総URL数：42000件
* ファイルサイズ：4.6MB
* 生成時間：5秒未満
* ビルド時間への影響：最小限
* キャッシュ有効期間：24時間

## 確認方法

生成されたsitemap.xmlは開発環境の`localhost:3000/sitemap.xml`で確認可能である。（通常の設定であれば）

## まとめ

Next.jsのSitemap APIは大規模多言語サイトでも実装しようと思えば可能。
キャッシュ戦略とエラーハンドリングを適切に実装することで、パフォーマンスとメンテナンス性の両立が可能である。

## 参考文献

* [Next.js App Routerでのnext-intlを用いた多言語Sitemap実装](https://qiita.com/s4yuba/items/67d5f19f2dfb60137e44)
* [Next.js Sitemap API Documentation](https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap)
* [Google Search Central - Build and submit a sitemap](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap)