Next.js App Routerでのnext-intlを用いた多言語Sitemap実装

# Next.js App Routerでのnext-intlを用いた多言語Sitemap実装

## はじめに

Next.js App Routerを使用した多言語サイトの開発において、最初に以前使用したnext-sitemapではうまくいかず、最終的にApp Router標準のSitemap生成にした経緯について記す。

## 開発環境

```
- Next.js 14.x（App Router）
- TypeScript 5.x
- next-intl 3.x
- next-sitemap 4.x
```

## 要件定義

- /en, /jaなどが多言語に対応したSitemap生成
- 言語別の適切なalternate links設定
- 検索エンジン最適化（SEO）に準拠したXML形式
- ページの優先度と更新頻度の適切な設定

## next-sitemapでの実装と課題

### 初期実装

```javascript
// next-sitemap.config.js
module.exports = {
  siteUrl: 'https://example.com',
  generateRobotsTxt: true,
  exclude: ['/_not-found', '/404', '/500'],
  generateIndexSitemap: false,
  
  transform: async (config, path) => {
    return {
      loc: path,
      changefreq: 'daily',
      priority: 0.7,
      lastmod: new Date().toISOString(),
    }
  }
}
```

### 発生した技術的課題

1.言語別alternate linksの生成エラー

`/ja/about`のあとに`/en/about`が続いている

```xml
<!-- 誤った生成例 -->
<url>
  <loc>https://example.com/en/about</loc>
  <xhtml:link rel="alternate" hreflang="ja" 
    href="https://example.com/ja/about/en/about"/>
</url>
```

2.サイトマップインデックスの構造問題

```xml
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- 空のインデックスが生成される -->
</sitemapindex>
```

## 解決までの試行錯誤
`npm run build`時の出力から、静的生成（SSG）と動的生成（Dynamic）のページを確認できる：

```
bashCopyRoute (app)
┌ ○ /_not-found 
├ ● /[locale]   
├   ├ /en
├   └ /ja
├ ● /[locale]/about   
├   ├ /en/about
├   └ /ja/about
├ ƒ /[locale]/blog/[id]
├ ● /[locale]/contact
├   ├ /en/contact
├   └ /ja/contact
└ ● /[locale]/products
    ├ /en/products
    └ /ja/products

○  (Static)   prerendered as static content
●  (SSG)      prerendered as static HTML (uses getStaticProps)
ƒ  (Dynamic)  server-rendered on demand
```
これらの情報を踏まえて試行錯誤を行った。

### 1. transformアプローチでの試み

```javascript
// next-sitemap.config.js
module.exports = {
  transform: async (config, path) => {
    const parts = path.split('/').filter(Boolean);
    const locale = parts[0];
    
    if (!['en', 'ja'].includes(locale)) {
      return null;
    }
    
    return {
      loc: `${config.siteUrl}${path}`,
      changefreq: 'weekly',
      priority: 0.7,
      alternateRefs: [{
        href: `${config.siteUrl}/${locale === 'en' ? 'ja' : 'en'}${path}`,
        hreflang: locale === 'en' ? 'ja' : 'en'
      }]
    }
  }
}
```

このアプローチでは、alternateRefsのURLに余分なパスが追加される問題（/ja/about/en/about）を解決できなかった。

### 2. additionalPathsアプローチ

```javascript
module.exports = {
  additionalPaths: async (config) => {
    const urls = [];
    const locales = ['en', 'ja'];
    const routes = [
      { path: '', priority: 1.0 },
      { path: '/about', priority: 0.8 },
      { path: '/contact', priority: 0.8 },
      { path: '/products', priority: 0.8 }
    ];
    
    locales.forEach(locale => {
      routes.forEach(({ path, priority }) => {
        urls.push({
          loc: `/${locale}${path}`,
          changefreq: 'weekly',
          priority,
          alternateRefs: [/* ... */]
        });
      });
    });
    return urls;
  }
}
```

このアプローチでは、手動でのURL管理が必要となり、メンテナンス性の低下を招いた。


これらの試行錯誤を経て、next-sitemapでの解決がうまく行かず、App Router標準機能への移行を決定した。

## App Router標準機能による解決策

[next-intl/examples/example-app-router/src/app
/sitemap.ts](https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/app/sitemap.ts)を参考にした。

```typescript
// app/sitemap.ts
import { MetadataRoute } from 'next'
mport { routing } from "@/i18n/routing";

const host = process.env.NEXT_PUBLIC_HOST || "http://localhost:3000";

export default function sitemap(): MetadataRoute.Sitemap {
    const baseUrl = 'https://example.com'
  
    const routes = [
        { path: '', priority: 1.0 },
        { path: '/about', priority: 0.8 },
        { path: '/contact', priority: 0.8 },
        { path: '/products', priority: 0.8 }
    ]

    return routes.flatMap((route) =>
        routing.locales.map((locale) => ({
            url: `${host}/${locale}${route === "/" ? "" : route}`,
            alternates: {
                languages: Object.fromEntries(
                    routing.locales.map((altLocale) => [altLocale, `${host}/${altLocale}${route === "/" ? "" : route}`])
                ),
            },
        }))
    );
}
```

上記をsrc/app/sitemap.tsに配置することで、Next.jsが自動的に`sitemap.xml`を生成する。
たとえば、開発環境の`localhost:3000/sitemap.xml`にアクセスすれば自動生成されたSitemapを見ることが可能である。

## 実装手法の比較分析

### next-sitemap
**長所:**
- robots.txtの自動生成機能
- 豊富な設定オプション
- サイトマップインデックス生成

**短所:**
- 多言語対応時の設定複雑化
- App Routerとの互換性問題
- デバッグ困難性

### App Router標準機能
**長所:**
- TypeScript完全対応
- App Router統合の容易さ
- シンプルな実装構造
- デバッグの容易性

**短所:**
- robots.txt個別実装の必要性
- 限定的な設定オプション
- サイトマップインデックス機能欠如

## 実装選択の判断基準

### next-sitemap
- Pages Router使用環境
- 複雑なサイトマップ要件
- 単一言語サイト

### App Router標準機能
- App Router使用環境
- 多言語サイト要件
- TypeScript活用環境

## 結論

Next.js App Routerを使用した多言語サイトのSitemap実装において、標準機能の活用が最適解となった。
特にTypeScriptサポートと実装の単純さが開発効率を向上させる要因となっている。
多言語サイトでのSEO対応において、App Router標準機能の採用を推奨したいと個人的に思う。

## 参考文献
- [Next.js App Router Documentation](https://nextjs.org/docs)
- [next-sitemap GitHub](https://github.com/iamvishnusankar/next-sitemap)
- [next-intl/examples/example-app-router/src/app
/sitemap.ts](https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/app/sitemap.ts)