30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Next.js の skipTrailingSlashRedirect で trailing slash の設定をカスタマイズする

Next.js のv13.1.0で追加されたskipTrailingSlashRedirectを使うことで、 trailing slash に関する挙動を自由に設定できる。
この記事では、skipTrailingSlashRedirectによって具体的にどのようなことが可能になったのかを見ていく。
動作確認はv13.1.1で行った。

環境構築

まずは Next.js の環境構築から。

$ yarn create next-app sample --ts

こうするとsampleというディレクトリが作られるので、そこに移動して作業を進めていく。

まず、next.config.jsbasePath"/app"を指定する。

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  basePath: "/app", // これを追加
};

module.exports = nextConfig;

そして以下の内容のpages/foo.tsxを作成する。

export default function Foo() {
  return <h1>Foo Page</h1>;
}

これで下準備が完了。

trailing slash

skipTrailingSlashRedirectはその名の通り trailing slash に関するリダイレクトをスキップする機能なのだが、これを理解するためにはまず、 trailing slash について理解している必要がある。

trailing slash とは URL の末尾についている/のこと。
Next.js では、 trailing slash をつけるかどうか設定することができ、設定内容に応じて Next.js がリダイレクト処理を行ってくれる。

デフォルトでは/を取り除くようになっているので、確認してみる。

trailing slash のない URL にリクエストを送ると、ステータスコード200が返ってくる。

$ curl -IL http://localhost:3000/app
HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/foo
HTTP/1.1 200 OK

そして trailing slash のある URL にリクエストを送ると、/を取り除いた URL へとリダイレクトされる。

$ curl -IL http://localhost:3000/app/
HTTP/1.1 308 Permanent Redirect
Location: /app

HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/foo/
HTTP/1.1 308 Permanent Redirect
Location: /app/foo

HTTP/1.1 200 OK

trailing slash をつけたい場合は、next.config.jstrailingSlashを有効にする。

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  basePath: "/app",
  trailingSlash: true, // これを追加
};

module.exports = nextConfig;

サーバを再起動して、動作確認してみる。

$ curl -IL http://localhost:3000/app/
HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/foo/
HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app
HTTP/1.1 308 Permanent Redirect
Location: /app/

HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/foo
HTTP/1.1 308 Permanent Redirect
Location: /app/foo/

HTTP/1.1 200 OK

先程とは逆に trailing slash のある URL へとリダイレクトされるようになる。

trailingSlash の問題

このようにtrailingSlashオプションで trailing slash の有無を選べるのだが、ひとつ問題があり、 URL によって設定を変えることができない。
そのため例えば、ルートパスのみ trailing slash を付与してそれ以外のパスでは付与しない、ということはできない。

Next.js には、リクエストを受け取ってリダイレクト処理などを行える middleware という機能があるが、この機能を使っても上記の問題は解決しない。
trailingSlashによるリダイレクト処理が、 middleware よりも先に実行されてしまうためである。

実際に middleware を用意して確認してみる。

ルートディレクトリ(今回の例ではsample)にmiddleware.tsを作って以下のように書くと、 middleware が受け取ったリクエストのパスがログに流れるようになる。

import type { NextRequest } from "next/server";

export function middleware(request: NextRequest): void {
  const requestPathname = new URL(request.url).pathname;
  console.log(requestPathname);
}

この状態で trailing slash のない URL にリクエストを送っても、 middleware にリクエストが渡される前にリダイレクトされてしまい、ログには trailing slash 付きのパスのみが流れてくる。

/app/
/app/foo/

trailingSlashを無効にしても同じで、そうすると今度は trailing slash がつかないパスのみが流れてくるようになる。

このように、 middleware にリクエストが渡されるよりも先に、trailingSlashによるリダイレクトが実行されてしまう。
そのため middleware による制御もできず、 Next.js で trailing slash を柔軟に設定することは難しかった。

skipTrailingSlashRedirect による解決

だがv13.1.0からは、skipTrailingSlashRedirectによってこの問題を解決できるようになった。

まず、next.config.jsskipTrailingSlashRedirectを有効にしてサーバを再起動する。

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  basePath: "/app",
  skipTrailingSlashRedirect: true, // trailingSlash を削除してこれを追加する
};

module.exports = nextConfig;

こうすることで、trailingSlashに関するリダイレクトが何も行われなくなる。
trailing slash をつけるためのリダイレクトも、取り除くためのリダイレクトも、行われない。

$ curl -IL http://localhost:3000/app
HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/
HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/foo
HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/foo/
HTTP/1.1 200 OK

あとは middleware で任意の処理を追加すればよい。
今回は root path のみ/をつけるようにする。

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest): NextResponse {
  const requestPathname = new URL(request.url).pathname;

  // basePath の値は /app
  const { basePath } = request.nextUrl;

  // /app にリクエストがあった場合は /app/ にリダイレクトする
  if (requestPathname === basePath) {
    return NextResponse.redirect(new URL(`${basePath}/`, request.url));
  }

  // /app/ 以外で末尾が / になっているパスの場合は、末尾から / を取り除いたパスにリダイレクトする
  if (requestPathname.endsWith("/") && requestPathname !== `${basePath}/`) {
    return NextResponse.redirect(
      new URL(`${requestPathname.slice(0, -1)}`, request.url)
    );
  }

  // それ以外のパスはそのまま処理を続ける
  return NextResponse.next();
}

リクエストを送ってみると、意図した通りの挙動になっている。

$ curl -IL http://localhost:3000/app
HTTP/1.1 307 Temporary Redirect
location: /app/

HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/
HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/foo
HTTP/1.1 200 OK

$ curl -IL http://localhost:3000/app/foo/
HTTP/1.1 307 Temporary Redirect
location: /app/foo

HTTP/1.1 200 OK

skipMiddlewareUrlNormalize

上述したように curl でリクエストを送ると上手く動くのだが、実はブラウザで開くと問題が起きる。
http://localhost:3000/app/fooは問題ないのだが、http://localhost:3000/app/にアクセスすると発生する。

ブラウザの開発者ツールで通信状況を確認してみると、http://localhost:3000/app/_next/data/development/index.jsonへのリクエストが無限に発生し続けていることが分かる。

なぜこのようなことが起こるのかというと、この JSON ファイルへのリクエストを middleware で扱う際に URL の正規化が行われ、/appへのリクエストと解釈されてしまうのである。
その結果、この JSON ファイルにアクセスしようとする度にリダイレクトが発生し、それがいつまでも繰り返されるという状況になってしまったのである。

URL の正規化を無効にするには、skipTrailingSlashRedirectと同様にv13.1.0で導入されたskipMiddlewareUrlNormalizeを有効にする必要がある。

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  basePath: "/app",
  skipTrailingSlashRedirect: true,
  skipMiddlewareUrlNormalize: true, // これを追加する
};

module.exports = nextConfig;

これで正規化が行われなくなり、リクエストが繰り返される事象が解決される。

参考資料