SvelteKit/SSG(adapter-static)で404.htmlを出力する方法

# はじめに
Jamstack構成でSvelteKit/SSG(adapter-static)を採用しWebサイトを構築する際に、カスタマイズされた404 Not Foundページを表示したい場合には、ホスティングサービスにあわせた404 Not Foundページを出力するように構築する必要がある。

例えばCloudflare Pagesの場合には、リクエストされたページが存在しない場合には、指定されたパスと同一ディレクトリ上の`404.html`を探し、なければディレクトリツリーを検索し続けることで最終的には`/404.html`をレンダリングする仕様になっている。

https://developers.cloudflare.com/pages/platform/serving-pages/#not-found-behavior

この`404.html`のスマートな出力方法が意外なことに見当たらなかったため、独自の実装方法で対応した。

:::note warn
**注意**
本記事の実装例はSvelteKit及びadapter-staticの公式ドキュメントに記載されている方法ではありません。
:::

# 概要
SvelteKitでは、`+error.svelte`を実装することで、[各routeごとにエラーページをカスタマイズすることができる。](https://kit.svelte.jp/docs/routing#error)
細かなエラー画面の出し分けなどは一旦置いておくとして、存在しないページへのリクエストのみ共通の404 Not Foundページを表示したいという場合には`/routes/+error.svelte`を設置するだけで共通の404 Not Foundページが出力できる。

しかし、SSG(Static Site Generator)においては、カスタマイズされた404 Not Foundページを表示したい場合に、ホスティングサービスにあわせたエラー用の静的なHTMLを設置する必要があり、冒頭で例に挙げたCloudflare Pagesの場合には`404.html`というファイルが必要になる。

ここまでを踏まえて、`/routes/+error.svelte`をビルドディレクトリに任意のファイル名(今回の例では`404.html`)で出力する方法が公式に提供されているのでは？と思い公式ドキュメント(主にadapter-staticに関する情報)を見たが、はっきりとマッチする情報が見つからない。。

ということでSvelteKitのGitHubリポジトリのIssueを探してみると、まさに全く同じことを実現する方法について、質問が出ていた。

https://github.com/sveltejs/kit/issues/1172

このIssueは結局[こちらのIssue](https://github.com/sveltejs/kit/issues/1209)に引き継がれることとなったのだが、最終的に[コントリビューターの方のコメント](https://github.com/sveltejs/kit/issues/1209#issuecomment-1092089414)とともに既に適切な機能提供がされているという理由でクローズされている。

しかし、実際に提示されている方法を試してみたところ当初の質問に対する回答、機能提供としては少し不足がある気がする。

というのも最終的に`adapter-static`の`fallback`オプションで出力したいエラーページ(404.html)を指定し、`prerender`を`true`にするという方法が提示されているのだが、[このコメント](https://github.com/sveltejs/kit/issues/1209#issuecomment-826205695)の通りこの方法は`/routes/+error.svelte`を404.htmlとして出力するわけではなく、404.htmlを空ページで出力しているだけに見受けられる。

また、[Configuration](https://kit.svelte.jp/docs/configuration)の`errorTemplate`など色々と試してみたが、`adapter-static`の`fallback`に指定したページをカスタマイズする方法がどこにも見当たらない。
※ここは私の調査が不足している可能性もあるのでご存じの方がいましたらコメントいただければと

ということで一旦、公式に提供されている機能での実現を諦め、Issueで他の方がコメントしているやり方をベースに独自の実装方法で対応することとした。

# 対応
まず、シンプルにビルドディレクトリに`/404.html`が出力されれば良いだけなので、直接`/routes/404/+page.svelte`を実装すれば解決では？と考えた。

:::note warn
**注意**
`trailingSlash`が`always`だと、`/404.html`ではなく、`/404/index.html`が出力されてしまうが、Cloudflare PagesではRoute matchingが[このような仕様](https://developers.cloudflare.com/pages/platform/serving-pages/#route-matching)のため`trailingSlash`が`never`という前提のもとの対応。
:::

実際、この方法でビルドしてみると`/routes/404/+page.svelte`の内容が`/404.html`として出力できた。

また、`$ vite dev`で稼働する開発環境では引き続き`/routes/+error.svelte`が404 Not Foundページになるが、別ファイルで同じレイアウトを管理するのは冗長なので、`/routes/404/+page.svelte`で`/routes/+error.svelte`をレンダリングするような実装としておくことで一つのページで管理することができる。
```/routes/404/+page.svelte
<script lang="ts">
  import ErrorPage from '$routes/+error.svelte'
</script>

<ErrorPage />
```

# svelte.config.jsのkit.paths.relativeについて
今回の方法でエラーページを共通化しCloudflare Pagesにデプロイする場合には、kit.paths.relativeをfalseにする必要がある。
```svelte.config.js
import adapter from '@sveltejs/adapter-static'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter(),
    paths: {
      relative: false
    }
  }
}

export default config
```
この設定がないと、第一パスの404 Not Found(`/{404}`)は問題ないが、第二パス以降の404 Not Found(`/posts/{404}`)ではassetsファイルの参照でエラー(404 Not Found)が発生してしまう。

この原因がsvelte.config.jsのkit.paths.relativeの初期値(true)とエラー画面の構成にあり、まずkit.paths.relativeは初期値がtrueとなり、より移植性の高いHTMLとするためにassetsファイルのパスは**相対アセットパス**となる。
```HTML
<link href="./_app/immutable/assets/0.j8NQl2rK.css" rel="stylesheet">
```

しかし、今回の方法でCloudflare Pagesにデプロイした場合、実際の404 Not Foundページは、`/404.html`となるがこのページを第二パス以降の404 NotFound時にもレンダリングすることとなり、`/404.html`と`/posts/{id}`はパスの階層が異なるため、相対アセットパスが参照エラーとなってしまう。
※`/posts/{id}`で`/404.html`をレンダリングした場合、相対パス`./_app/`は`/posts/_app`への参照となる

この問題を解消するために、kit.paths.relativeをfalseに設定するとassetsファイルのパスは**ルート相対パス**となる。
```HTML
<link href="/_app/immutable/assets/0.j8NQl2rK.css" rel="stylesheet">
```

# まとめ
今回の実装例は公式ドキュメントで提示されている方法ではなく少しハック感が強いため、今だに「適切な方法が提供されているのでは？」と思っているのでご存じの方がいましたらコメントいただけますと幸いです。

# 参考
https://developers.cloudflare.com/pages/platform/serving-pages

https://kit.svelte.jp/docs/routing

https://kit.svelte.jp/docs/configuration

https://github.com/sveltejs/kit/issues/1172

https://github.com/sveltejs/kit/issues/1209