Next.jsでのIPジオロケーション:ブラウザとサーバーのパターン
Next.jsは同じページをサーバー上でもブラウザ上でもレンダリングでき、その選択によって実際にどのWatch-IPエンドポイントが訪問者の位置情報を解決するか—あるいは代わりに自分のサーバーの位置情報を解決してしまうか—が変わります。
始める前に
- クライアントパターン用の公開可能な訪問者用キー、またはサーバーパターン用のシークレットルックアップキー—認証を参照。
- Next.js App Routerプロジェクト(例はNext.js 16のServer/Client Componentの分割を前提としています)。
- サーバーパターン向けに、ホスティングプラットフォームが受信リクエストに設定する、訪問者の実際のIPを含むヘッダー(例:CloudflareのCF-Connecting-IP)—クライアント自身が設定できるものではないもの。
1. 各パターンが実際に何を呼び出しているかを理解する
GET /v1/geoは、APIに到達した接続の持ち主を特定します—訪問者のブラウザが直接呼び出す場合もあれば、サーバーレンダリング中に自分のコードが呼び出す場合、その場合はNext.jsサーバーです。Server ComponentやRoute HandlerからGET /v1/geoを呼び出すと、訪問者のものではなく自分のサーバーのネットワーク上の位置情報が解決されます。これは通常、望んでいる結果ではありません。実際に訪問者を解決するパターンは2つあります:ブラウザ(Client Component)でリクエストを実行するか、指定したIPに対してサーバー上でGET /v1/lookup/{ip}を実行するかです。
2. Client Component:ブラウザからGET /v1/geoを呼び出す
これはJavaScriptガイドとReactガイドで使われているのと同じリクエストです—公開可能キーでfetchするClient Componentです。ブラウザ自身のOriginヘッダーがキーの許可オリジンチェックを満たすため、アプリの公開URLをそのリストに含めておく必要があります。
TypeScript (Client Component)
"use client";
import { useEffect, useState } from "react";
export function VisitorLocation() {
const [city, setCity] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
fetch("https://api.watch-ip.com/v1/geo", {
headers: { "X-Api-Key": "YOUR_PUBLISHABLE_KEY" },
signal: controller.signal,
})
.then((res) => (res.ok ? res.json() : null))
.then((geo) => setCity(geo?.city ?? null))
.catch(() => {});
return () => controller.abort();
}, []);
return <span>{city ?? "お住まいの地域"}</span>;
}3. Server ComponentまたはRoute Handler:訪問者のIPを明示的にルックアップする
サーバーレンダリング中に訪問者の位置情報を解決するには、GET /v1/geoを呼び出すのではなく、シークレットキーと、ホスティングプラットフォームが受信リクエストに設定するヘッダーから読み取った訪問者の実際のIPを使ってGET /v1/lookup/{ip}を呼び出してください。プラットフォームがエッジでX-Forwarded-For値を除去・再設定していない限り、クライアント自身が設定できる単純なX-Forwarded-For値を決して信用しないでください。
TypeScript (Server Component)
import { headers } from "next/headers";
export async function VisitorLocationServer() {
const ip = (await headers()).get("cf-connecting-ip");
if (!ip) return null;
const response = await fetch(`https://api.watch-ip.com/v1/lookup/${ip}`, {
headers: { Authorization: "Bearer YOUR_SECRET_KEY" },
cache: "no-store",
});
if (!response.ok) return null;
const geo = await response.json();
return <span>{geo.city ?? "お住まいの地域"}</span>;
}4. ある訪問者の結果を別の訪問者に対してキャッシュしない
上記のcache: 'no-store'は意図的なものです:ルックアップの結果はリクエストした訪問者に固有のものです。このコンポーネントが、Next.jsやCDNがキャッシュしうるルート—静的ページ、ISRページ、共有されたfetchキャッシュ—の中でレンダリングされる場合、キャッシュされたレスポンスはある訪問者の都市を、その後すべての人に表示してしまいます。パーソナライズされた分岐は動的レンダリングのままにし、ルックアップのレスポンスをページレベルやCDNレベルのキャッシュに取り込まないでください。
エラーとエッジケース
IPヘッダーが欠落または空である場合(一部のローカル環境やプロキシ設定ではヘッダーが設定されないことがあります)、ルックアップすべき訪問者のIPが存在しないことを意味します—空文字列でエンドポイントを呼び出すのではなく、フォールバックを描画してください。GET /v1/lookup/{ip}からの403は通常、そのルートにシークレットキーが必要な箇所で公開可能キーが使われたことを意味し、400はヘッダーに有効なIPv4/IPv6アドレスではない値が入っていたことを意味します。エラーとレート制限をご覧ください。
次のステップ
クライアントのみのReactアプリとNext.jsの両方で再利用できるコンポーネントについてはReactガイドをご覧ください—上記のClient Componentパターンは、同じリクエストに対するNext.js固有のラッパーです。