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での実装と課題
初期実装
// 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
が続いている
<!-- 誤った生成例 -->
<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 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アプローチでの試み
// 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アプローチ
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を参考にした。
// 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標準機能の採用を推奨したいと個人的に思う。