skip to content
me nonoakij

Next-intl と Supabase を使用する時の Next.js の middleware の実装メモ

/ 3 min read

はじめに

この記事は、Next.js で next-intl と Supabase を使用する際の middleware の実装メモです。

version

各ライブラリのバージョンは以下の通りです。

ライブラリバージョン
next15.0.2
next-intl3.24.0
@supabase/ssr0.5.1
@supabase/supabase-js2.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, "/");
}

reference