
背景
多言語Webサイトの課題は、ユーザーの言語設定を自動判定し、最適な言語バージョンへシームレスにリダイレクトすることです。
VitePressで構築した多言語サイト(英語 / と日本語 /ja/)において、従来はユーザーが手動で言語を切り替える必要がありました。初回訪問時にAccept-Languageヘッダーから自動判定し、Cookieで言語選択を永続化することで、UXを大幅に改善できます。
エッジサイド処理(Cloudflare Pages Functions) を活用することで、以下のメリットが得られます:
- 低レイテンシ: オリジンサーバーへのリクエスト前に言語判定が完了
- 高パフォーマンス: CDNレベルでのリダイレクト処理で不要なオリジンリクエストを削減
- SEO維持: ボット検出により検索エンジンクローラーにはリダイレクトしない
本ガイドでは、このEdge-side i18n実装を段階的に解説します。
こんな人におすすめ
- Cloudflare Pages/Workersで多言語対応を考えている方
- エッジサイドでのリダイレクト実装に興味のある方
- VitePressで国際化対応に取り組んでいる方
- セキュアなCookie実装やボット検出について学びたい方
- Edge FunctionsとAccept-Languageの統合方法を知りたい方
目次
よくある質問(FAQ)
Q: Edge Functions を使う理由は何か?
A: エッジサイドで言語判定を実行することで、オリジンサーバーへのリクエスト前に処理が完了します。結果として、ネットワークレイテンシが削減され、ユーザーにより高速な体験を提供できます。また、不要なオリジンリクエストが減少するため、サーバー負荷も軽減されます。
Q: Accept-Language ヘッダーだけで十分では?
A: Accept-Languageヘッダーは、ユーザーのブラウザ設定に基づいています。しかし、ユーザーが一度言語を手動選択した場合、その選択を忘れずに保持する必要があります。Cookieを併用することで、ユーザーの明示的な選択を優先しながら、初回訪問時にはAccept-Languageを参照できます。
Q: SEO的にリダイレクトは大丈夫?
A: 301(恒久的)リダイレクトではなく、302(一時的)リダイレクトを使用し、かつボット検出によりGooglebot等のクローラーにはリダイレクトしません。これにより、検索エンジンは各言語版を個別にインデックスでき、SEOへの悪影響を回避できます。
要件
- 初回訪問時にAccept-Languageヘッダーから言語を自動判定
- Cookie
pref-langによる言語選択の永続化(1年間有効) - Cloudflare
cf.countryによる地理位置情報フォールバック - SEO維持のためクローラー(Googlebot、Bingbot等)にはリダイレクトしない
- クエリパラメータの保持(
?tag=wordpress&page=2など) - VitePressの既存言語切り替え機能との互換性維持
実装方針
- Cloudflare Pages Functions (
functions/_middleware.ts) で言語判定ロジックを実装 - 5段階の優先度で言語を判定:
- Cookie
pref-lang→ Accept-Language → cf.country → デフォルト(en)
- Cookie
- 定数管理 (
functions/constants.ts) で設定を一元化 - クライアント側永続化 (
language-persistence.ts) でVitePressテーマと統合 - 包括的テスト (35テストケース) で品質担保
言語判定フロー
以下のフローチャートで、5段階の優先度に基づいた言語判定プロセスを図示しています:
flowchart TD
A[リクエスト受信] --> B{Bot/Crawler<br/>か?}
B -->|はい| Z[リダイレクトなし]
B -->|いいえ| C{Cookie<br/>pref-lang<br/>存在?}
C -->|はい| D["lang = Cookie値<br/>を使用"]
C -->|いいえ| E{Accept-Language<br/>ヘッダー<br/>解析可能?}
E -->|はい| F["lang = Accept-Language<br/>から言語を抽出"]
E -->|いいえ| G{cf.country<br/>マップに<br/>存在?}
G -->|はい| H["lang = 地理位置情報<br/>から言語を判定"]
G -->|いいえ| I["lang = デフォルト<br/>en を使用"]
D --> J{現在の言語<br/>と異なる?}
F --> J
H --> J
I --> J
J -->|はい| K["リダイレクト実行<br/>Set-Cookie: pref-lang"]
J -->|いいえ| L[リダイレクトなし]
K --> M[新しい言語ページへ]
L --> M
Z --> M
この判定フローは、あくまで人間のブラウザ訪問に対する動的リダイレクトです。
「Bot/Crawlerか?」の分岐でクローラーはリダイレクトされずにそのままページを受け取るため、Googlebot等は各言語版のURLをそれぞれ個別にクロール・インデックスできます。
一方で、「どのURLが同一コンテンツの別言語版か」を検索エンジンに伝えるのは、この動的リダイレクトの役目ではありません。それは静的なhreflangタグ(後述のhreflang タグの実装を参照)が担う役割です。実装意図としては、次のように役割が明確に分かれています。
- エッジの動的リダイレクト: 人間ユーザーの初回訪問時に、最適な言語版へ自動的に案内する(UX目的)
- 静的なhreflangタグ: 検索エンジンに対して、各言語版ページの対応関係を明示する(SEO目的)
この2つを混同して「hreflangがあるからリダイレクトも兼ねられる」「リダイレクトしているのでhreflangは不要」と考えてしまうと、クローラーが各言語版を正しく対応付けてインデックスできなくなる恐れがあります。両者は独立した仕組みとして併用するのが正しい設計です。
ポイント
Immutable Headers問題の解決
Cloudflare WorkersではResponse.redirect()やcontext.next()から返されるResponseオブジェクトのヘッダーは不変(immutable)。
問題のあるコード:
const response = Response.redirect(redirectUrl, 302);
response.headers.append('Set-Cookie', ...); // エラー: 不変ヘッダーを変更できない
解決策: 新しいHeadersオブジェクトを作成してからResponseを構築
function createResponseWithCookie(response: Response, lang: Language): Response {
const headers = new Headers(response.headers);
headers.append('Set-Cookie', generateCookieString(lang));
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
}
ボット検出パターン
10以上の主要クローラーを正規表現で検出し、SEOを維持:
export const BOT_PATTERNS = {
USER_AGENTS: /bot|crawler|spider|crawling|googlebot|bingbot|slurp|duckduckbot|baiduspider|yandexbot|facebookexternalhit|twitterbot|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator/i,
} as const;
hreflang タグの実装
ボット検出によってクローラーへのリダイレクトを止めても、それだけでは「どのURLが同一コンテンツの別言語版か」を検索エンジンに伝えることはできません。ここで必要になるのがhreflangです。
エッジでの出力方法
Cloudflare Pages Functionsでhreflangを出力する方法は、大きく分けて2通りあります。
- HTMLの
<head>内に<link rel="alternate" hreflang="...">タグとしてビルド時に埋め込む - エッジのレスポンスに
Linkヘッダーとして動的に付与する
VitePressの静的ビルド済みHTMLに対して、エッジ側でLinkヘッダーを追加する場合の実装例は以下の通りです。
function addHreflangHeaders(response: Response, pathname: string): Response {
const headers = new Headers(response.headers);
const alternates = [
{ hreflang: 'en', prefix: '' },
{ hreflang: 'ja', prefix: '/ja' },
{ hreflang: 'x-default', prefix: '' },
];
const linkValue = alternates
.map(({ hreflang, prefix }) =>
`<https://example.com${prefix}${pathname}>; rel="alternate"; hreflang="${hreflang}"`
)
.join(', ');
headers.append('Link', linkValue);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
「Immutable Headers問題の解決」で紹介した「新しいHeadersオブジェクトを作成してからResponseを構築する」パターンをそのまま流用できるため、Cookieの設定とhreflangヘッダーの付与は同じ関数内で両立できます。
エッジ特有の注意点
- キャッシュとの関係: Cloudflareのエッジキャッシュはレスポンスをヘッダーごとキャッシュします。
Linkヘッダーを動的に付与する場合、キャッシュキーに言語情報を含めるかCache-Controlでキャッシュ対象から除外しないと、あるパス向けに生成したhreflangヘッダーが、キャッシュ経由で異なる言語のユーザーにも配信されてしまう可能性があります。 - ビルド時埋め込みとの二重管理を避ける: VitePressのビルド出力に既に
<link rel="alternate">タグが含まれている場合、エッジ側でもLinkヘッダーを付与すると同じ情報が二重に存在することになります。どちらか一方の方式に統一し、エッジ側で完結させるならビルド時のhreflang出力は無効化しておくと管理コストが下がります。 - x-defaultの明示: Accept-Languageが未知の言語のみで判定できない場合のフォールバック先を
x-defaultとして明示しておくと、検索エンジンが適切なデフォルトページを選択しやすくなります。
Cookie セキュリティ設定
export const COOKIE_OPTIONS = {
path: '/',
httpOnly: true, // XSS攻撃防止:JavaScriptからのアクセスをブロック
secure: true, // HTTPS接続時のみ送信(中間者攻撃防止)
sameSite: 'lax', // CSRF攻撃防止:クロスサイトリクエストでのCookie送信を制限
} as const;
セキュリティ属性の詳細:
httpOnly: Cookie がクライアント側の JavaScript(XSS攻撃)からアクセスされることを防止しますsecure: HTTPS接続時のみ Cookie を送信し、平文のHTTP通信での盗聴を防ぎますsameSite: 'lax': クロスサイトリクエストでの Cookie 送信を制限し、CSRF攻撃を防止します
これらの設定により、ユーザーの言語選択情報は高いセキュリティレベルで保護されます。
学び
- Cloudflare Workers APIの特性: Immutable Responseの扱いは標準Node.jsと異なる
- エッジサイド処理の利点: オリジンへのリクエスト前に言語判定が完了し、レイテンシが大幅に削減される
- サーバー/クライアント連携: サーバー側httpOnly Cookieとクライアント側オブザーバーの組み合わせで、セキュリティとUXを両立
- 包括的テストの重要性: 35件のテストケースにより、エッジケースを含む全検出パスをカバー
- SEOとユーザビリティの両立: ボット検出により、検索エンジンには全言語版を適切にインデックスさせつつ、人間ユーザーには最適な言語を自動提供
まとめ
本ガイドでは、Cloudflare Pages Functions を活用したEdge-side i18n(国際化)リダイレクト の実装方法を解説しました。
実装のポイント
-
Immutable Response の理解: Cloudflare Workers の
Responseオブジェクトは不変のため、新しいHeadersを作成してからResponseを構築する必要があります -
5段階の優先度フロー: Cookie → Accept-Language → cf.country → デフォルト という段階的な言語判定により、ユーザーの意図を最優先しながら初回訪問時には Accept-Language から言語を自動判定できます
-
SEO 対策とボット検出: 302リダイレクト(一時的)と BOT検出の組み合わせにより、検索エンジンクローラーには各言語版を個別にインデックスさせながら、一般ユーザーには最適な言語版を自動提供できます
-
セキュアな Cookie 実装:
httpOnly、secure、sameSite属性により、XSS攻撃、中間者攻撃、CSRF攻撃を防止しながら、ユーザーの言語選択を安全に保持できます
適用可能なシーン
このパターンは、以下のような多言語 Web サイトに適用可能です:
- 国際的な SaaS プロダクト: ユーザーの地理位置情報や言語設定に基づいた自動リダイレクト
- オープンソースプロジェクト: ドキュメントサイトの多言語対応
- コンテンツメディア: 複数言語で提供するニュースサイトやブログ
35件のテストケースによる包括的な検証により、エッジケースを含む全検出パスをカバーしているため、実装の信頼性も高く、本番環境への適用も安心です。
さらに学ぶ
このパターンに関連する技術について、さらに詳しく学びたい場合は以下のトピックを推奨します:
- Cloudflare Workers: エッジサイド処理の基礎と応用
- HTTPヘッダー仕様: Accept-Language、User-Agent の正規表現マッチング
- セキュアなCookie実装: httpOnly、secure、sameSite 属性の組み合わせ
- 多言語SEO: hreflang タグと言語別ページのインデックス戦略
- エッジコンピューティング: CDNレベルでのロジック実装とパフォーマンス最適化


