0
0

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 App Routerで4万件の多言語動的サイトマップを実装する方法

Posted at

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実装

解決方法

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

// 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:

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

参考文献

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?