トレイリングスラッシュによるURL重複インデックス問題
症状
Google Search Consoleで、同一ページが2つのURLとしてインデックス登録されている:
https://log.eurekapu.com/2025-12-04/self-reminderhttps://log.eurekapu.com/2025-12-04/self-reminder/
原因
デフォルトでトレイリングスラッシュの正規化を行わないNuxt 3では:
- 両方のURLでアクセス可能: スラッシュあり・なし両方のURLが有効なルートとして扱われる
- リダイレクトなし: どちらかに正規化(canonical化)するリダイレクトが発生しない
- Googleが両方をインデックス: 同一コンテンツを別URLとして認識し、重複インデックス
これはSEO的に以下の問題を引き起こす:
- PageRankの分散: 被リンクが2つのURLに分散される
- クロールバジェットの浪費: Googleが同じページを2回クロールする
- 重複コンテンツ警告: 将来的にSearch Consoleで警告が出る可能性
試した解決策(すべて失敗)
方法1: routeRulesでリダイレクト設定 → ビルドエラー
nuxt.config.tsのrouteRulesでスラッシュ付きURLをリダイレクト:
export default defineNuxtConfig({
routeRules: {
'/**/**/': {
redirect: {
to: path => path.slice(0, -1),
statusCode: 301
}
}
}
})
失敗理由: cloudflare-pages-staticプリセットでは、redirect.toに関数を使用できない。
[error] segment.replace is not a function
at joinURL ([email protected]/node_modules/ufo/dist/index.mjs:316:32)
at writeCFPagesRedirects (nitropack/.../cloudflare/utils.mjs:127:105)
方法2: サーバーミドルウェア → 静的ビルドでは動作しない
server/middleware/trailing-slash.tsを作成しても、cloudflare-pages-staticプリセットではサーバーミドルウェアは実行されない。
方法3: Cloudflare Redirect Rules → 無限リダイレクトループ
試した設定
Cloudflareダッシュボードで以下のリダイレクトルールを設定:
- 条件:
ends_with(http.request.uri.path, "/")かつhttp.request.uri.path ne "/" - アクション: Dynamic Redirect
- Expression:
concat(substring(http.request.uri.path, 0, -1)) - ステータスコード: 301
発生した問題:ERR_TOO_MANY_REDIRECTS
ブラウザで「リダイレクトが繰り返し行われました」エラーが発生。
根本原因:Cloudflare Pagesの仕様
トレイリングスラッシュを強制的に追加するのがCloudflare Pagesの仕様である:
Cloudflare Pages is "an opinionated system that thinks trailing slashes should be there." — Cloudflare Community
実際に確認した挙動:
https://log.eurekapu.com/financial-quiz/jleagueにアクセス- 自動的に
https://log.eurekapu.com/financial-quiz/jleague/に308リダイレクト
リダイレクトループの発生メカニズム:
1. ユーザーが /path/ にアクセス
2. 我々のルールが /path にリダイレクト (301)
3. Cloudflare Pagesが /path/ にリダイレクト (308)
4. 我々のルールが /path にリダイレクト (301)
5. ... 無限ループ
この挙動は無効化できない(Cloudflare Communityより)
方法4: _redirects ファイル → 動的パス変換ができない
全URLに対応するには全パスを列挙する必要があり、現実的ではない。
方法5: Cloudflare Workers → ループのリスクあり
Pagesの処理より前にCloudflare PagesのWorkerは実行されるが、Pagesの308リダイレクトとの相互作用で同様のループが発生するリスクがある。
推奨解決策:canonicalタグで対応
なぜcanonicalタグが最適か
Google公式ドキュメントによると:
| 方法 | シグナル強度 | 用途 |
|---|---|---|
| 301リダイレクト | 最強 | ページを廃止・統合する場合 |
| rel="canonical" | 強い | 重複ページが並存する場合 |
| サイトマップ | 弱い | 補助的な方法 |
今回のケース:
/pathと/path/の両方が存在し続ける(Cloudflare Pagesの仕様上、変更不可)- リダイレクトは無限ループを引き起こす
- → canonicalタグが唯一の現実的な解決策
canonicalタグの効果
SE Rankingによると:
canonicalタグは「重複ページが並存する場合に、どちらが正規版かを検索エンジンに伝える」ために使用する。 バックリンクのシグナルも統合される(301リダイレクトと同様の効果)。
実装内容
1. nuxt.config.ts の設定
site.trailingSlash: false を追加:
// nuxt.config.ts
export default defineNuxtConfig({
site: {
url: 'https://log.eurekapu.com',
trailingSlash: false // canonicalURLをスラッシュなしで生成(SEO重複対策)
}
})
この設定により、@nuxtjs/sitemap が生成する sitemap.xml のURLもスラッシュなしになる。
2. canonicalタグ自動生成プラグイン
app/plugins/canonical.ts を作成:
/**
* Canonical URL Plugin
*
* 全ページに自動的にcanonicalタグを追加する。
* Cloudflare Pagesがトレイリングスラッシュを強制するため、
* 301リダイレクトではなくcanonicalタグでSEO重複問題を解決する。
*/
export default defineNuxtPlugin({
name: 'canonical',
setup() {
const route = useRoute()
const siteUrl = 'https://log.eurekapu.com'
// パスから末尾スラッシュを削除してcanonical URLを生成
const getCanonicalUrl = (path: string) => {
const cleanPath = path.replace(/\/$/, '') || '/'
return `${siteUrl}${cleanPath}`
}
// SSR/SSGとクライアント両方で動作
useHead({
link: [
{
rel: 'canonical',
href: () => getCanonicalUrl(route.path)
}
]
})
}
})
3. 生成されるHTML
すべてのページに以下のようなcanonicalタグが自動追加される:
<link rel="canonical" href="https://log.eurekapu.com/financial-quiz/jleague">
確認済み: ローカル開発サーバーで http://localhost:3001/financial-quiz/jleague にアクセスし、HTMLに上記のcanonicalタグが含まれていることを確認。
各アプローチの比較(最終版)
| アプローチ | 静的サイト対応 | CF Pages互換 | 結果 |
|---|---|---|---|
| Nuxt routeRules(関数) | ❌ | - | ビルドエラー |
| サーバーミドルウェア | ❌ | - | 動作しない |
| Cloudflare Redirect Rules | ✅ | ❌ | 無限ループ |
| _redirects ファイル | ✅ | ❌ | 動的変換不可 |
| Cloudflare Workers | ✅ | ⚠️ | ループのリスク |
| canonicalタグ | ✅ | ✅ | 推奨 |
結論
Cloudflare Pagesの仕様上、トレイリングスラッシュの301リダイレクトは実現不可能。SEO的な重複問題をcanonicalタグで解決するのが唯一の現実的な解決策。
推奨アクション
Cloudflare Redirect Rulesでリダイレクト設定→ 無限ループが発生するため不可- 推奨: Nuxt SEOでcanonicalタグを自動設定(
trailingSlash: false) - Sitemap確認: 生成されるsitemap.xmlのURLがスラッシュなしになっているか確認
- GSC対応: canonicalタグ設定後、1-2週間待つ(Googleの再クロールを待つ)