Next.js のv13.1.0
で追加されたskipTrailingSlashRedirect
を使うことで、 trailing slash に関する挙動を自由に設定できる。
この記事では、skipTrailingSlashRedirect
によって具体的にどのようなことが可能になったのかを見ていく。
動作確認はv13.1.1
で行った。
環境構築
まずは Next.js の環境構築から。
$ yarn create next-app sample --ts
こうするとsample
というディレクトリが作られるので、そこに移動して作業を進めていく。
まず、next.config.js
のbasePath
に"/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.js
でtrailingSlash
を有効にする。
/** @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.js
でskipTrailingSlashRedirect
を有効にしてサーバを再起動する。
/** @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;
これで正規化が行われなくなり、リクエストが繰り返される事象が解決される。