Next.js Advent Calendar 2020の12日目の記事です。
2021/03/30 追記
Next.js 10.0.8から_resolveHref
メソッドがresolveDynamicRoute
メソッドに変更されています。また、この関数はexportされなくなったのでアプリ側から叩く方法がなくなってしまいました。しかし、resolveDynamicRoute
メソッドで使用しているNextのメソッドは全てexportされているため、アプリ側にresolveDynamicRoute
を定義すれば今まで通り実装可能です。
はじめに
Next.jsは3の頃からルートを事前にレンダリングし、静的ファイルとしてエクスポートする機能があります。
SSRをした方がブログなど動的に追加されるコンテンツではパフォーマンスやOGPなどでメリットがあります。ただ、SSRをするインフラを構築して運用するのはなかなか大変なものです。
例えばログインしてからでないと動的ルートを使用しない場合などはOGPとか考えなくて良いので、静的ファイルとして吐き出し、初回ロード以外はSPAとして動作させてしまえばHTTPサーバーを立てるだけでアプリを公開できます。楽!
さらに、Next.js 9からファイルパスベースで動的ルートの定義ができるようになりました。
今まではnext.config.js
に静的に吐き出したいパスを定義する必要がありましたが、9からはpages/path/[id].jsx
のようなファイルを作成することで/path/1
といったルーティングを自動的に生成できるようになりました。すごい!
さあ満を持して、サーバーに静的ファイルをアップロードした!アクセスした!表示される!やったー!そしておもむろに/path/1
をアドレスバーに打ち込んだ!
そこに表示されたのは…404!
そう、動的ルートは/path/[id].html
として吐き出されていたのでした。
本記事ではこれを解決しましょう。
解決案
ここでは考えうる解決策を比較しましょう。
サーバーでリライトする
この方法は既に @mottox2 さんの Next.jsのDynamic Routing + Static HTML exportを組み合わせて使う で解説されている通り、動的ルートにアクセスがきた場合サーバー側で対応するファイルにリダイレクトするものです。
Pros
- サーバー設定だけでいいのでNext.js側の対応は不要
Cons
- ルートが増える度に設定の書き換えが必要
動的ルートはSPAでルーティングする
この方法はnuxt.jsなどが採用しています。( https://ja.nuxtjs.org/docs/2.x/configuration-glossary/configuration-generate/#fallback )
動的ルートにアクセスされた時、カスタム404ページでフォールバック用ページを返します。
フォールバック用ページはSPAでルーティング可能なルートであればそのページに移動し、不可能なルートであれば404をレンダーします。
Pros
- SPAにルーティングがあるのでルートが増えても自動対応できる
Cons
- サーバーは404を返しているけど、正常にページがレンダリングされるので??ってなる
で、どっちにするん?
タイトルの通り、「動的ルートはSPAでルーティングする」案でいきます。諸事情で頻繁にサーバー設定が弄れない環境だったんです。
でも、そんな機能Next.jsにはないんですよ。
実装
じゃあ、作りましょう。なお、対象とするNext.jsは10.0.3とします。
下調べ
この方法のミソは「フォールバック用ページはSPAでルーティング可能なルートであればそのページに移動し、不可能なルートであれば404をレンダーします。」ってところです。
そのためには、今のパスとマッチするルートは何かという情報をクライアントで取得する必要があります。Next.jsがクライアントでそのような処理をしてそうなところの目星を付けます。
Next.jsの9.5.3以降ではLinkコンポーネントのasを指定しなくても自動的に動的ルートのページを推定してくれるようになりました。
つまり、pathnameからルートを判断する仕組みがありそうです。
Linkコンポーネントを追っていくとL221-229付近でresolveHref
というメソッドを呼んでいますね。hrefからasを推定していそうな匂いがします。
あとは、これをページから呼べれば完璧です。
resolveHref
はpackages/next/next-server/lib/router/router.ts
で定義されているのでみてみるとパッケージとしてexportされていないので叩けなさそうです。
しかし、L1185-L1209付近にクライアントからも叩けるRouterクラスに_resolveHref
メソッドが定義されているではありませんか。
では叩いてみましょう。この_resolveHref
の第2引数にルート一覧を渡しますが幸運なことにL700付近でpageLoader.getPageList
メソッドで取得できることがわかります。
import Router from "next/router";
import { useEffect, useState } from "react";
export default function Path() {
const [resolvedRoute, setResolvedRoute] = useState(undefined);
const [asPath, setAsPath] = useState("");
useEffect(() => {
(async () => {
const pages = await Router.router.pageLoader.getPageList();
setResolvedRoute(
Router.router._resolveHref({ pathname: Router.asPath }, pages)
);
})();
}, []);
return <div>{JSON.stringify(resolvedRoute)}</div>;
}
やったー!ルートが取得できました。
フォールバックページの実装
無事マッチするルートが取得できたので、カスタムエラーページを作成して、リダイレクトの処理を書いていきましょう。
_resolveHref
は存在しないルートでもとりあえず値を返すので、ルート一覧に存在する場合はリダイレクト、存在しない場合は404表示と切り分けています。
import Router from "next/router";
import { useEffect, useState } from "react";
import Error from "next/error";
export default function NotFoundPage() {
const [isError, setIsError] = useState(false);
useEffect(() => {
(async () => {
const pages = await Router.router.pageLoader.getPageList();
const resolvedRoute = Router.router._resolveHref(
{ pathname: Router.asPath },
pages
);
if (pages.includes(resolvedRoute.pathname)) {
Router.replace(resolvedRoute.pathname, Router.asPath);
} else {
setIsError(true);
}
})();
}, []);
return <>{isError && <Error statusCode={404} />}</>;
};
動作テスト
ビルドします。
npm run build && ./node_modules/.bin/next export
nginxで生成したファイルをホスティングしてアクセスしてみます。404の時は404/index.html
を返すようにします。
server {
listen 80;
location / {
root /var/www;
index index.html index.htm;
try_files $uri $uri/index.html =404;
}
error_page 404 /404/index.html
}
まずは動的ルートにアクセスして、リロードしてみます。
次に存在しない動的ルートにアクセスしてみます。
完璧!
おわりに
Next.jsの非公開APIを叩いてたり、Next.jsのいいところを無視している構成な気がしますが、同じ悩みを抱える誰かの助けになると嬉しいです。
あとbasePathの対応はしてないです。