はじめに
この記事は、Next.js で next-intl と Supabase を使用する際の middleware の実装メモです。
version
各ライブラリのバージョンは以下の通りです。
ライブラリ | バージョン |
---|---|
next | 15.0.2 |
next-intl | 3.24.0 |
@supabase/ssr | 0.5.1 |
@supabase/supabase-js | 2.46.1 |
middleware の実装
import { type NextRequest, NextResponse } from "next/server";
import createMiddleware from "next-intl/middleware";
import { createServerClient } from "@supabase/ssr";
// routing の場所はアプリケーションによって異なる可能性があるので、適切なパスを指定してください。
import { routing } from "@/lib/i18n/routing";
const handleI18nRouting = createMiddleware(routing);
export default async function middleware(request: NextRequest) {
const response = handleI18nRouting(request);
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value),
);
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options),
);
},
},
},
);
// 重要: createServerClient と supabase.auth.getUser() の間に
// ロジックを書かないようにしてください。
// 単純なミスで、ユーザがランダムにログアウトする問題をデバッグするのが非常に難しくなります。
const {
data: { user },
} = await supabase.auth.getUser();
const localeRemovedPath = removeLocaleFromPath(
routing,
request.nextUrl.pathname,
);
// この実装では、/private 以下のページにアクセスするためにはログインが必要な場合を想定しています。
// それぞれのアプリケーションに合わせて、適切な条件に変更してください。
if (!user && localeRemovedPath.startsWith("/private")) {
// ユーザーがいない場合、ユーザーを会員登録ページにリダイレクトして応答する可能性がある。
const url = request.nextUrl.clone();
const locale = resolveLocale(routing, request);
// この実装では、会員登録ページは /register にあると仮定しています。
url.pathname = `/${locale}/register`;
return NextResponse.redirect(url);
}
return response;
}
export const config = {
matcher: [
"/((?!_next/static|api|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
/**
* In this case, the locale is detected based on these priorities:
* 1. A locale prefix is present in the pathname (e.g. /en/about)
* 2. A cookie is present that contains a previously detected locale
* 3. A locale can be matched based on the accept-language header
* 4. As a last resort, the defaultLocale is used
*/
function resolveLocale(config: typeof routing, request: NextRequest) {
// 1. Check the pathname
// @ts-expect-error locale is a string
if (config.locales.includes(request.nextUrl.pathname.split("/")[1])) {
return request.nextUrl.pathname.split("/")[1];
}
// 2. Check the cookie
const localeFromCookie = request.cookies.get("NEXT_LOCALE")?.value;
// @ts-expect-error locale is a string
if (localeFromCookie && config.locales.includes(localeFromCookie)) {
return localeFromCookie;
}
// 3. Check the accept-language header
const acceptLanguage = request.headers.get("accept-language");
const detectedLocale = acceptLanguage
? acceptLanguage.split(",")[0].split("-")[0]
: undefined;
// @ts-expect-error locale is a string
if (detectedLocale && config.locales.includes(detectedLocale)) {
return detectedLocale;
}
// 4. Default to the defaultLocale
return config.defaultLocale;
}
function removeLocaleFromPath(config: typeof routing, path: string) {
const locales = config.locales;
const localePattern = new RegExp(`^/(${locales.join("|")})(/|$)`);
return path.replace(localePattern, "/");
}