第125章:練習:国別/言語別にリダイレクトするイメージ🌏🚦✨
この章では、/ に来た人を「国(JPかどうか)」「ブラウザ言語(Accept-Language)」「前回選んだ言語(Cookie)」で判断して、/ja か /en に自動で飛ばすミニ実装を作るよ〜😊💨
(Next.jsの Middleware を使う練習だよ🧤)
1) 今日のゴール🎯✨
-
http://localhost:3000/にアクセスしたら…- 🇯🇵 JPなら
/ja - 🌍 それ以外は
/en - ただし、🍪 Cookieで前回の言語が覚えられてたらそれを優先
- それも無ければ 🌐 Accept-Language(ブラウザ言語)で推測(日本語っぽければ
/ja)
- 🇯🇵 JPなら
-
/jaと/enのページを用意して、切り替えもできるようにする🔁✨
Next.js公式も、ブラウザ言語(Accept-Language)を使ってロケールを選ぶのを推奨してるよ〜📌 (nextjs.org)
2) ざっくり図解🧠🗺️(Mermaid)
3) 準備:ページを2つ作ろう📁✨
✅ app/ja/page.tsx
import Link from "next/link";
export default function JaPage() {
return (
<main style={{ padding: 24, lineHeight: 1.8 }}>
<h1>こんにちは!🇯🇵✨</h1>
<p>
ここは <code>/ja</code> だよ〜😊
</p>
<p>
<Link href="/en">👉 Englishページへ</Link>
</p>
</main>
);
}
✅ app/en/page.tsx
import Link from "next/link";
export default function EnPage() {
return (
<main style={{ padding: 24, lineHeight: 1.8 }}>
<h1>Hello! 🌍✨</h1>
<p>
You are on <code>/en</code> 🙂
</p>
<p>
<Link href="/ja">👉 日本語ページへ</Link>
</p>
</main>
);
}
✅ app/page.tsx(保険として置いとく🧸)
Middlewareが動いてると基本ここは見えないけど、念のため置いとくと安心☺️
export default function Home() {
return (
<main style={{ padding: 24 }}>
<p>Redirecting... ⏳</p>
</main>
);
}
4) 本題:middleware.ts を作ろう🧤🔥
プロジェクトのルート(package.json と同じ階層)に middleware.ts を作ってね✨
import { NextRequest, NextResponse } from "next/server";
const SUPPORTED_LOCALES = ["ja", "en"] as const;
type Locale = (typeof SUPPORTED_LOCALES)[number];
const DEFAULT_LOCALE: Locale = "en";
const LOCALE_COOKIE = "pref_locale";
// ローカル確認用:/?__country=JP みたいに付けると国判定を疑似れるよ🧪
const DEBUG_COUNTRY_PARAM = "__country";
function isSupportedLocale(value: string | null | undefined): value is Locale {
return !!value && (SUPPORTED_LOCALES as readonly string[]).includes(value);
}
function getLocaleFromPathname(pathname: string): Locale | null {
const first = pathname.split("/")[1]; // "" or "ja" or "en"
return isSupportedLocale(first) ? first : null;
}
// 例: "ja,en-US;q=0.9,en;q=0.8"
function getLocaleFromAcceptLanguage(header: string | null): Locale | null {
if (!header) return null;
const langs = header
.split(",")
.map((part) => part.trim().split(";")[0])
.filter(Boolean);
for (const lang of langs) {
const primary = lang.toLowerCase().split("-")[0]; // "ja-JP" -> "ja"
if (isSupportedLocale(primary)) return primary;
}
return null;
}
function getLocaleFromCountry(country: string | undefined): Locale | null {
if (!country) return null;
return country.toUpperCase() === "JP" ? "ja" : "en";
}
function redirectToLocale(url: URL, locale: Locale) {
const nextUrl = new URL(url);
// / -> /ja , /something -> /ja/something
nextUrl.pathname = `/${locale}${url.pathname === "/" ? "" : url.pathname}`;
// デバッグ用パラメータは見た目が汚れるので消す🧼
nextUrl.searchParams.delete(DEBUG_COUNTRY_PARAM);
const res = NextResponse.redirect(nextUrl);
// 次回はCookie優先にできるように記憶🍪
res.cookies.set(LOCALE_COOKIE, locale, {
path: "/",
sameSite: "lax",
});
return res;
}
export function middleware(request: NextRequest) {
const url = request.nextUrl;
const pathname = url.pathname;
// 1) すでに /ja や /en ならそのまま通す(ついでにCookieに保存🍪)
const localeInPath = getLocaleFromPathname(pathname);
if (localeInPath) {
const res = NextResponse.next();
res.cookies.set(LOCALE_COOKIE, localeInPath, {
path: "/",
sameSite: "lax",
});
return res;
}
// 2) Cookieに前回の言語があれば最優先🍪
const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value;
if (isSupportedLocale(cookieLocale)) {
return redirectToLocale(url, cookieLocale);
}
// 3) 国で判定🇯🇵(本番でVercel等なら request.geo が入ることがある)
// ローカルは undefined になりがちなので、?__country=JP で疑似テストできるようにしてるよ🧪
const debugCountry = url.searchParams.get(DEBUG_COUNTRY_PARAM) ?? undefined;
const country = debugCountry ?? request.geo?.country;
const localeFromCountry = getLocaleFromCountry(country);
if (localeFromCountry) {
return redirectToLocale(url, localeFromCountry);
}
// 4) ブラウザ言語(Accept-Language)で推測🌐
const localeFromHeader = getLocaleFromAcceptLanguage(
request.headers.get("accept-language")
);
if (localeFromHeader) {
return redirectToLocale(url, localeFromHeader);
}
// 5) 最後はデフォルトで en 🧸
return redirectToLocale(url, DEFAULT_LOCALE);
}
// Middlewareを当てたくないパスを除外(静的ファイル等)🧯
// こういうmatcherの書き方は公式例にもあるよ✅ :contentReference[oaicite:1]{index=1}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)",
],
};
Accept-Languageで言語を推測する流れは、i18nのガイドでもよく出てくる考え方だよ🌐 (nextjs.org)request.geoを使った国判定は、ホスティング環境(特にVercel)依存になりやすい点は覚えておくと安心だよ〜⚠️ (Vercel)
5) 動作確認しよ〜😆✅(Windows)
① まず起動🚀
- VSCodeのターミナルで:
npm run dev
② 国判定の疑似テスト🧪(ローカルで超便利!)
-
ブラウザでこれ開いてみて👇
- 🇯🇵 JP想定:
http://localhost:3000/?__country=JP→/jaに飛ぶ✨ - 🌍 US想定:
http://localhost:3000/?__country=US→/enに飛ぶ✨
- 🇯🇵 JP想定:
③ Cookieが効いてるかチェック🍪
1回 /en を開いたあとに、/ に戻ると /en に飛ぶはず!🔁
(逆も同じで /ja を開いたら次回は /ja 優先になるよ😊)
6) ハマりやすいポイント集🪤💦(ここ大事!)
- 無限リダイレクトになる🥶
→
/jaや/enみたいに「すでにロケール付きのURLなら素通り」してる?(今回のコードはOK👌) /_next/staticとかにもMiddlewareが当たって重い🐢 →matcherで除外しよ〜!(今回やってるよ✅)(nextjs.org)- ローカルで
request.geo?.countryが取れない😵 → それ普通!本番環境依存のことが多いよ〜。だから今回は?__country=JPを用意したよ🧪 (Vercel)
7) ミニ課題🎒✨(できたら強い!)
-
SUPPORTED_LOCALESに"ko"を足して、/koも作ってみよ🇰🇷✨ -
国判定ルールをちょい改造してみよ🛠️
- 例:
JP -> ja、KR -> ko、それ以外enみたいにする🌏
- 例:
-
Cookie名を変えてもちゃんと動くか試す(理解が深まるよ🍪🧠)
ここまでできたら、**「アクセスした瞬間にユーザーに合う入口へ案内する」**って感覚がつかめるはず〜😊🚪✨