技術ブログ

Next.js App Routerでsitemap.xmlを実装する完全ガイド

Next.js App Routerでsitemap.xmlを実装する完全ガイド

sitemapとは何か

sitemapは検索エンジンクローラーに対してサイトの構造や更新頻度を伝えるXMLファイルです。特に以下のようなケースで威力を発揮します。

  • 新しいページが頻繁に追加されるブログやECサイト
  • 深い階層構造を持つサイト
  • 外部リンクが少なく、クローラーが発見しにくいページがあるサイト

基本的な静的sitemap

最もシンプルな実装から始めましょう。小規模なアプリケーションでは、sitemap.xmlファイルをappディレクトリのルートに配置できます。

<!-- app/sitemap.xml -->
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com</loc>
    <lastmod>2024-06-22T00:00:00.000Z</lastmod>
    <changefreq>yearly</changefreq>
    <priority>1</priority>
  </url>
  <url>
    <loc>https://example.com/about</loc>
    <lastmod>2024-06-22T00:00:00.000Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

この方法は手軽ですが、ページが増えるたびに手動で更新する必要があります。

動的Sitemapの実装

実際のプロジェクトでは、動的にsitemapを生成する方が現実的です。

sitemap.tsを使用して自動でsitemapを生成できます。

// app/sitemap.ts
import type { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://example.com',
      lastModified: new Date(),
      changeFrequency: 'yearly',
      priority: 1,
    },
    {
      url: 'https://example.com/about',
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
    {
      url: 'https://example.com/blog',
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 0.5,
    },
  ]
}

これでhttps://example.com/sitemap.xmlにアクセスすると、適切なXMLが生成されます。

ブログサイトでの実践例

ブログサイトを例に、より実践的な実装を見ていきましょう。

// app/sitemap.ts
import type { MetadataRoute } from 'next'

// ブログ記事を取得する関数(例)
async function getBlogPosts() {
  // 実際のプロジェクトでは、CMSやデータベースから取得
  const posts = await fetch('https://api.example.com/posts').then(res => res.json())
  return posts
}

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://example.com'
  
  // 静的ページ
  const staticPages: MetadataRoute.Sitemap = [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1,
    },
    {
      url: `${baseUrl}/about`,
      lastModified: new Date('2024-01-01'),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
    {
      url: `${baseUrl}/contact`,
      lastModified: new Date('2024-01-01'),
      changeFrequency: 'yearly',
      priority: 0.5,
    },
  ]
  
  // 動的ページ(ブログ記事)
  const posts = await getBlogPosts()
  const blogPages: MetadataRoute.Sitemap = posts.map((post: any) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }))
  
  return [...staticPages, ...blogPages]
}

この実装により、新しいブログ記事が追加されるたびに自動的にsitemapに反映されます。

画像Sitemapの活用

画像を多く使用するサイトでは、画像Sitemapを活用できます。これにより、Google画像検索での表示機会を増やせます。

// app/sitemap.ts
import type { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://example.com/gallery',
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 0.8,
      images: [
        'https://example.com/images/photo1.jpg',
        'https://example.com/images/photo2.jpg',
        'https://example.com/images/photo3.jpg',
      ],
    },
  ]
}

多言語サイトでの実装

多言語サイトでは、alternatesプロパティを使用してhreflang属性を追加できます。

// app/sitemap.ts
import type { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://example.com',
      lastModified: new Date(),
      alternates: {
        languages: {
          ja: 'https://example.com/ja',
          en: 'https://example.com/en',
          zh: 'https://example.com/zh',
        },
      },
    },
    {
      url: 'https://example.com/about',
      lastModified: new Date(),
      alternates: {
        languages: {
          ja: 'https://example.com/ja/about',
          en: 'https://example.com/en/about',
          zh: 'https://example.com/zh/about',
        },
      },
    },
  ]
}

大規模サイト向け:複数Sitemapの生成

大規模なWebアプリケーションでは、sitemapを複数のファイルに分割する必要があります。Googleの制限により、1つのsitemapには最大50,000のURLしか含められません。

// app/sitemap.ts
import type { MetadataRoute } from 'next'

export async function generateSitemaps() {
  // 商品の総数を取得し、必要なsitemap数を計算
  const totalProducts = await getProductCount()
  const sitemapCount = Math.ceil(totalProducts / 50000)
  
  return Array.from({ length: sitemapCount }, (_, i) => ({ id: i }))
}

export default async function sitemap({
  id,
}: {
  id: number
}): Promise<MetadataRoute.Sitemap> {
  const start = id * 50000
  const end = start + 50000
  
  const products = await getProducts({
    limit: 50000,
    offset: start,
  })
  
  return products.map((product) => ({
    url: `https://example.com/products/${product.slug}`,
    lastModified: new Date(product.updatedAt),
    changeFrequency: 'weekly' as const,
    priority: 0.6,
  }))
}

これにより、/sitemap/0.xml/sitemap/1.xmlといった形で複数のsitemapが生成されます。

パフォーマンス最適化のポイント

キャッシュの活用

sitemap.jsは特別なRoute Handlerで、Dynamic APIやdynamic config optionを使用しない限り、デフォルトでキャッシュされます。

// app/sitemap.ts
import type { MetadataRoute } from 'next'

// 静的データのみを使用する場合、自動的にキャッシュされる
export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://example.com',
      lastModified: new Date('2024-01-01'), // 固定日付
      changeFrequency: 'yearly',
      priority: 1,
    },
  ]
}

効率的なデータ取得

大量のデータを扱う場合は、必要最小限の情報のみを取得しましょう。

// 効率的なデータ取得の例
async function getPostsForSitemap() {
  // 必要な列のみを取得
  const posts = await db.post.findMany({
    select: {
      slug: true,
      updatedAt: true,
    },
    where: {
      published: true,
    },
  })
  
  return posts
}

実装時の注意点

URLの正規化

URLは必ず完全なURL(プロトコル含む)で指定し、末尾のスラッシュは統一しましょう。

// 良い例
url: 'https://example.com/about'

// 悪い例
url: '/about' // プロトコルとドメインが不足
url: 'example.com/about' // プロトコルが不足

lastModifiedの適切な設定

lastModifiedは実際の更新日時を反映させることが重要です。

{
  url: 'https://example.com/blog/example-post',
  lastModified: post.updatedAt, // 実際の更新日時
  // lastModified: new Date(), // 常に現在時刻は避ける
}

priorityとchangeFrequencyの適切な設定

priorityは相対的な重要度を示し、changeFrequencyは実際の更新頻度に合わせて設定します。

// ホームページ:最重要、年1回程度の更新
{ priority: 1.0, changeFrequency: 'yearly' }

// ブログ記事:重要、週1回程度の更新
{ priority: 0.7, changeFrequency: 'weekly' }

// アーカイブページ:普通、更新なし
{ priority: 0.3, changeFrequency: 'never' }

Google Search Consoleでの確認

sitemapの実装が完了したら、Google Search Consoleで正しく認識されているか確認しましょう。

  1. Google Search Consoleにログイン
  2. 対象のプロパティを選択
  3. 左メニューの「サイトマップ」をクリック
  4. sitemap.xmlのURLを入力して送信

エラーが発生した場合は、生成されたXMLを直接確認し、構文エラーがないかチェックしてください。

まとめ

Next.js App Routerでのsitemap実装は、静的ファイルから動的生成まで柔軟に対応できる強力な機能です。小規模サイトでは静的XML、中規模サイトでは動的生成、大規模サイトでは複数sitemap生成と、プロジェクトの規模に応じて適切な手法を選択しましょう。

重要なのは、検索エンジンとユーザーの両方にとって価値のあるsitemapを作成することです。定期的にGoogle Search Consoleでインデックス状況を確認し、必要に応じて改善を重ねていくことが、効果的なSEO対策につながります。

参考サイト

Web制作のご相談はこちら

この記事に関連するご質問やWeb制作のご相談がございましたら、お気軽にお問い合わせください。

お問い合わせ