Next.jsでurlのルートパスを動的に切り替える方法
タイトルだけだとなんのこっちゃって感じですね。
今回仕事において、以下の仕様を満たして欲しいと言われました。
仕様
ログイン、ログアウトの状態を問わず https://hoge.com/12345678/fuga
や https://hoge.com/87654321/fuga
など、url の最上位ルートパスを切替可能にしたい。
シンプルですね、実装自体はさほど苦戦しないと思います。
ただし愚直に実装したらこんな感じになると思うのですが。
- src
- app
- [appId]
- (auth)
- fuga
- page.tsx
- (public)
- login
- page.tsx
この場合以下の問題点が発生します。
- ログイン必須の場合
appId
は必須となるため、わざわざ[appId]
フォルダーを置きたくない - 画面遷移を組む際毎回
appId
を渡してやらないといけない ex)<Link href={``/${appId}/piyo``}>
ということで、色々と試行錯誤した結果、以下のいずれかの方法で実装が可能なことがわかりました。
- next.config で切り替える
- middleware で切り替える
多分どちらでも実装自体は問題なく、今回のケースだと next.config で実装すべきだと思うんですが。
ちょっとまだ next.config における正規表現が把握しきれておらず、今回は妥協して middeware で組んでみました。
前提
- サーバーサイドでも
appId
を取得可能とする - ログイン時は認証を必須とする
動作フロー
ex) <Link href="/fuga">
を押下された場合
/fuga
へ遷移しようと試みる- パスに
appId
が含まれていないため、appId
を付与して redirect させる ex)/12345678/fuga
- 認証を行う
/fuga
へ rewrite させる
実装感
まずは最終的なフォルダー構造を。
- src
- app
- (auth)
- fuga
- page.tsx
- (public)
- [appId]
- login
- page.tsx
- middleware.ts
(auth)
側、つまりログインが必須な page container 側の親に [appId]
フォルダーを置かなくて済むようになったので少しだけさっぱりしました。
またログインに成功した歳に Cookie に appId
を埋める処理を実装しています。
続いて middleware です。
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest): Promise<NextResponse> {
const response = NextResponse.next();
const {
nextUrl: { pathname },
} = request;
// TODO: mathcer ではじきたい
if (pathname.endsWith("/login")) {
return response;
}
const appId = request.cookies.get("appId");
if (typeof appId === "undefined") {
return NextResponse.redirect(new URL("/not-found", request.url));
}
const [_, pathnameAppId, ...restPathnames] = pathname.split("/");
// appId が path の先頭に含まれていない場合
if (pathnameAppId.length !== 8) {
return NextResponse.redirect(
new URL(`/${appId.value}${pathname}`, request.url),
);
}
if (pathnameAppId !== appId.value) {
return NextResponse.redirect(new URL("/not-found", request.url));
}
// 認証済みか否か
const authenticated = ...
if (!authenticated) {
return NextResponse.redirect(new URL("/not-found", request.url));
}
return NextResponse.rewrite(
new URL(`/${restPathnames.join("/")}`, request.url),
);
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|not-found).*)"],
};
また (auth)
以下の page container 側は [appId]
フォルダー以下に配置されていないため、PageProps
の params
に appId
が格納されていないことに注意が必要です。
そのため appId
が必要な際は Cookie から取得するようにしましょう。
import { cookies } from "next/headers";
...
const appId = cookies().get("appId");
Layout 側も同様です。
あともう一点注意として、middleware では appId
の整合性までは確認していません。
そのため、appId
が正しいかどうかは Layout 側などで確認するようにしましょう。
そんな感じです、結構実装感に苦しんだんですが、なんとか実装できて良かったです。
ただ上に書いた通り、可能であれば next.config で実装すべきかと思います。
おそらく今回の動作感は next.config で組める範囲だと認識していますので、組めた方がおられましたらぜひ情報をいただけますと…。