kk-web

Next.jsでurlのルートパスを動的に切り替える方法

2024-04-27

タイトルだけだとなんのこっちゃって感じですね。

今回仕事において、以下の仕様を満たして欲しいと言われました。


仕様

ログイン、ログアウトの状態を問わず https://hoge.com/12345678/fugahttps://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"> を押下された場合

  1. /fuga へ遷移しようと試みる
  2. パスに appId が含まれていないため、appId を付与して redirect させる ex) /12345678/fuga
  3. 認証を行う
  4. /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] フォルダー以下に配置されていないため、PagePropsparamsappId が格納されていないことに注意が必要です。

そのため appId が必要な際は Cookie から取得するようにしましょう。

import { cookies } from "next/headers";

...

const appId = cookies().get("appId");

Layout 側も同様です。

あともう一点注意として、middleware では appId の整合性までは確認していません。

そのため、appId が正しいかどうかは Layout 側などで確認するようにしましょう。


そんな感じです、結構実装感に苦しんだんですが、なんとか実装できて良かったです。

ただ上に書いた通り、可能であれば next.config で実装すべきかと思います。

おそらく今回の動作感は next.config で組める範囲だと認識していますので、組めた方がおられましたらぜひ情報をいただけますと…。

© 2018 kk-web