🍓

Nuxt 3でtrailing slashありのURLを使うときの設定

2023/06/20に公開

Nuxt 3でtrailing slashありのURLを使うのに特別な設定は必要ありません。しかし、webサイトと違いwebアプリではtrailing slashなしで統一するのが一般的で、Nuxtもtrailing slashなしを前提に開発されています。その結果、たまに罠にハマってしまうことがあります。

大前提

同じページにtrailing slashありとなしの2つのURLがあるのは望ましくありません。「このページはtrailing slashなしであのページはtrailing slashあり」みたいなことをするとミスの原因になるので、基本的にはプロジェクト内で統一するべきです。

https://developers.google.com/search/blog/2010/04/to-slash-or-not-to-slash?hl=ja

プリレンダリング(SSG)時の設定

デフォルトではSSG(nuxi generate)する際にtrailing slashなしのルートがレンダリングされます。すると、useRouteから参照したパスにtrailing slashが付いておらず、たとえば生成されたHTMLのog:urlcanonicalタグにtrailing slashなしのURLが設定されてしまう恐れがあります[1]

幸い、Nuxt 3ではSSGの対象ページ(ルート)を列挙する処理にフックを挟むことができます。これを利用し、フック内で対象URLの全てにtrailing slashをつけることで、この問題を解決することができます。

nuxt.config.ts
export default defineNuxtConfig({
  // ...
  hooks: {
    'prerender:routes': (context) => {
      // context.routesは対象URLのSet
      for (const path of [...context.routes]) {
        // 200.htmlや404.htmlは無視する
        if (!path.endsWith('.html') && path !== '/') {
          context.routes.delete(path)
          context.routes.add(`${path}/`)
        }
      }
    }
  },
  // ...
})

(本当はgenerate.routesオプションでSSG対象に追加したserver routesなども無視しないといけないはずですが……)

NuxtLink

defineNuxtLinktrailingSlashオプションを使うことで、trailing slashを自動的につけるNuxtLinkのカスタムコンポーネントを定義することができます。

https://nuxt.com/docs/api/components/nuxt-link#definenuxtlink-signature

https://github.com/nuxt/nuxt/pull/19458

なお、Nuxt 2ではルータのレベルでtrailing slashを制御するrouter.trailingSlashオプションが存在したようですが、Nuxt 3にはありません。後述するroute middlewareを使うと良さそうです。

route middleware

https://nuxt.com/docs/guide/directory-structure/middleware

middleware/trailingSlash.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
  if (!to.path.endsWith('/')) {
    // origin部分は使わないのでexample.comそのままでいい
    const { pathname, search, hash } = new URL(to.fullPath, 'https://example.com')
    return navigateTo(`${pathname}/${search}${hash}`)
  }
})

ルータでのページ遷移に介入するミドルウェア[2]を利用して、trailing slashありのURLに強制的に遷移させることができます。ただし、SSG時にはリダイレクト(302)がエラー扱いになってしまうので、これだけで問題を解決することはできません。上述したフックの設定が必要です。

余談: Nuxt 3のSSGの挙動について

Nuxt 3のSSGは内部的にnitroによって実装されています。nitroがクローラとしてページの内容を取得し、それを.output/public下にHTMLとして出力する仕組みです。さらに、元のページからリンクされたページもクローリング対象として追加されます。この挙動のおかげで、静的ルーティングのページからリンクをつたって到達できる範囲なら動的ルーティングのページもSSGされます。

ここで、たとえばpages/blog.vueが存在する場合を考えます。このページには/blog/blog/の両方でアクセスできてしまいますが、SSG時にはデフォルトで/blogの方にアクセスされます。しかし、もし他のページから/blog/に対してリンクが貼られていた場合、/blog/もクローリングの対象となります。/blog//blogはあくまで異なるURLですが、SSG時の出力先はともに.output/public/blog/index.htmlです。すると、先にクローリングされた方が後の方で上書きされてしまいます。

nitroのクローリングの順番は当然ながら実装依存です。もし深さ優先探索で、かつ/から/blog/へリンクされている場合、/blog//blogより先にクロールされます。一方、幅優先探索であれば/blogの方が先です。私が試した限りでは、現時点では幅優先探索っぽい挙動でした。つまり、この場合/blog//blogを上書きします。結果的には意図通りtrailing slashありが生き残りましたが、安定性が見込めないnitro内部の実装・仕様に依存するべきではありません。

脚注
  1. 後述しますが、ならないこともあります。 ↩︎

  2. ややこしいですが、server middlewareとは別です。 ↩︎

Discussion