Nuxt3 SSG 周りのメモ

環境など
技術 | バージョン |
---|---|
Nuxt | 3.16.0 (Nuxt4互換モード) |
Nitro | 2.11.6 |
Node | 20.15.1 |
デプロイ先:Cloudflare Pages (static)
参考資料

v3.12になってからprerender:routesに/しか渡されなくなり、この対処法が使えなくなりました。対処法が見つかっておらず、かなり困っています。
確認した限りだと、nitro.prerender.routes
に手動で渡したパスや、prerender:routes
より前に実行されるライフサイクルフック(nitro:config
など)で追加したパスはコールバックの引数として狙った通りに参照することが可能なはず。
hookで動的なパスを設定する例
export default defineNuxtConfig({
hooks: {
"nitro:config": ( async config => {
const posts = await fetch("https://jsonplaceholder.typicode.com/posts").then(res => res.json())
config.prerender?.routes?.push(
...posts.map(c => `/posts/${c.id}`)
)
})
}
})

generate
実行時のログを見ても、trailing slashなしのパスは出力されていないので、参考資料に記載されている方法は現在においても有効だとうかがえる
export default defineNuxtConfig({
hooks: {
"prerender:routes": (({ routes }) => {
for ( const route of routes ){
if( !route.endsWith("/") && route !== "/" ){
routes.delete(route)
routes.add(`${route}/`)
}
}
}),
"nitro:config": ( async config => {
const posts = await fetch("https://jsonplaceholder.typicode.com/posts").then(res => res.json())
config.prerender?.routes?.push(
...posts.map(c => `/posts/${c.id}`)
)
})
}
})

クローリング周りの挙動
nitroのクローリングの順番は当然ながら実装依存です。もし深さ優先探索で、かつ/から/blog/へリンクされている場合、/blog/が/blogより先にクロールされます。一方、幅優先探索であれば/blogの方が先です。私が試した限りでは、現時点では幅優先探索っぽい挙動でした。つまり、この場合/blog/が/blogを上書きします。
計100ページ程度の量であれば静的ページの扱いは上記の通りだが、(dynamic routes含め) 1580ページを生成するPJでは逆に trailing slash ありのページが先にクロール・生成されていた。
なので閾値を境に探索方式が切り替わるようになっている可能性あり・・・?
(こればかりはNitroの内部実装を読まないとなので今は何とも)
対策
nitroの generate hook で trailing slash なしのパスをスキップさせることで実現できそう
export default defineNuxtConfig({
nitro: {
hooks: {
"prerender:generate" (route) {
if( !(/\.(json|html|js)$/.test(route.route) || route.route.endsWith("/")) ){
route.skip = true
}
}
}
},
}
!/\.(json|html|js)$/.test(route.route)
を入れないと各ページの payload.json までスキップされてしまい、表示が壊れてしまうので必ず条件式に含める必要あり。

生成前のタイミングで、対象のルートを上書きすることもできるので、スキップの代わりにパスの末尾にスラッシュを足す処理を入れることで trailing slashを強制するという目的は達成できるはず
nitro: {
hooks: {
"prerender:generate" (route) {
if( !(/\.(json|html|js)$/.test(route.route) || route.route.endsWith("/")) ){
route.route += "/"
}
}
}
},

ただ、この場合同じルートに対して2度レンダリングが実行されてしまう可能性があるので、Set
とかに生成済みのルートを保持しておいて、すでに生成済みならスキップする処理を入れたほうがコードの実行時間的にはいいかも (CodeBuildとかの実行時間による課金の場合など)
const renderedRoutes = new Set<string>()
//中略
nitro: {
hooks: {
"prerender:generate" (route) {
if( !(/\.(json|html|js)$/.test(route.route) || route.route.endsWith("/")) ){
route.route += "/"
}
if( route.contentType?.includes("html") ){
if( renderedRoutes.has(route.route) ){
route.skip = true
} else {
renderedRoutes.add(route.route)
}
}
}
}
},
計測した範囲では100ページ程度の生成であれば有意な差はみられないが、実行結果のルート数は処理を入れる前に比べて重複分は無視されてカウントされているのを確認

NEXT
nitro のクローラーの実装を見る

prerender 周りの nitro hooks
nitro の src/prerender/prerender.ts
の中で呼び出されている hookをまとめていく

prerender:routes
コールバックの引数の型: Set<string>
メモ:このタイミングでは nuxt.config.ts
の nitro.prerender.routes
か nitro.routeRules
で条件に一致した一部のルートしか入っていないため、それ以外のページに対しての処理を入れたりはできない(手動で追加したりはできるが)

prerender:config
コールバック引数の型: NitroConfig
メモ: Nitroの初期化前に呼び出される。基本的にこの設定は nuxt.config.ts で設定できるようになっているはずなので、必要になる機会は稀な気はする。

prerender:init
コールバック引数の型: Nitro
メモ:プリレンダー用の Nitro が初期化されたタイミングで呼び出される。 Nuxt 本体でも呼び出されている箇所が見当たらなかったので用途はいまいちわからない・・・

prerender:generate
コールバック引数の型: PrerenderRoute
PrerenderRoute
の型定義
メモ:各ページの静的生成の前に呼び出される。ここで手続き的に設定した値は、そのあとのページ生成ステップで反映された状態で実行される。

prerender:route
コールバック引数の型: PrerenderRoute
メモ:各ページの静的生成の後に呼び出される。prerender:generate
とは違い、ここで手続き的に値を設定してもページ生成には反映されない。(というのもその後に実行される処理はログの出力だけのため)
ページ生成がスキップ/エラーの場合
ページ生成が正常に実行された場合

prerender:done
コールバック引数の型
{
prerenderRoutes: PrerenderRoute[],
failedRoutes: PrerenderRoute[]
}
メモ:すべてのルートのプリレンダリングが完了したタイミングで呼び出される。後続にプリレンダリングに関して何か処理が挟まれるというわけでもないので、ログのファイル出力くらいしか用途はなさそう。

クローラーの挙動
crawlLinks オプションが有効な場合、ページ生成の最後のタイミングでそのページに存在するリンク先を抽出して生成対象に追加する処理を行っている
ここでの実装を読む限り、生成対象のリストを先頭から順番に処理しているようなので参考資料で言及されている通り幅優先探索になているはず。