起きていた問題
本ブログの日付を表示する箇所で、画面の読み込み時 (初回アクセス) に一瞬1日前のものが表示された後に期待通りの日付に戻るという事象が発生していた。また、コンソールからはHydration completed but contains mismatches.
というエラーが確認できた。
また、ローカルでnpm run generate
、npx serve .output/public
で表示させたときはこの事象は起きず、Hydration mismatchs のエラーも出ていなかった。
NuxtのHydrationとは
Nuxtのドキュメントによると、Universal Renderingモード (NuxtにおけるSSRのモード) ではブラウザからのリクエストを受け取るとサーバー側でJavaScriptを実行し静的なHTMLをレンダリングして返すが、ブラウザ側でも裏で同じJavaScriptを実行して動的なDOMをクライアントサイトでも用意する。これによって、単一のHTMLをベースにJavaScriptで内容を書き換えていくSPAのリアクティブ性をSSRにおいても確保している。Nuxt以外でもVue, React (Next.js)など主要なSPAのフレームワークでSSRを採用する場合は同様のHydrationのプロセスが行われていそうである。
npm run generate
によるSSGの場合も事前に静的なHTMLを生成してから配信するが、初回アクセス後はページ遷移などでSPAの仮想DOMによるレンダリングがクライアントサイトで行われることは変わらないため、SSGでもHydrationが行われていると考えられる。
本ブログをChromeの開発者ツールで開き、ダウンロードされたリソースを見てみると、確かに初めにダウンロードされる初期ページのHTMLには日付が「2024年07月18日」と書かれている一方、
_payload.json?...
(ContentfulのAPIに対してuseFetch
したレスポンスが格納されている)では2024-07-18T00:00+09:00
となっていて、クライアントサイドでのHydrationが終わったときにHTMLの日付が「2024年07月18日」に上書きされていたようである。
原因
day.jsによる日付のフォーマット処理として以下のようなNuxt3のPluginを用意していた。
import dayjs from 'dayjs';
export default defineNuxtPlugin(() => {
return {
provide: {
formatDate(datetime: string): string {
return dayjs(datetime).format('YYYY年MM月DD日');
},
},
};
});
formatDate()
にはContentfulから取得したブログ記事の日付のString"2024-07-18T00:00+09:00"
が入ってくるが、クライアントサイドでは日本のタイムゾーンで変換されるので「2024年7月18日」と変換される一方、GitHub ActionsでSSGする際はおそらくデフォルトUTCで実行されるため「2024年7月17日」と変換されてしまっていた。
そこで、以下のようにタイムゾーンを設定し、ビルド時もクライアントサイドの読み込み時も'Asia/Tokyo'で固定することにより、Hydration mismatchのエラーを回避することができた。
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
dayjs.extend(utc);
dayjs.extend(timezone);
export default defineNuxtPlugin(() => {
return {
provide: {
formatDate(datetime: string): string {
return dayjs(datetime).tz('Asia/Tokyo').format('YYYY年MM月DD日');
},
},
};
});
(追記)
vueの公式にて、Hydration Mismatchの考えられる主な原因の一つとしてサーバーとクライアントのタイムゾーンが異なることが挙げられていた。今回は日本のタイムゾーンに統一したが、クライアントのタイムゾーンに合わせて表示を変えたい場合は、client-onlyとしてサーバー側ではレンダリングしないことが推奨されている。
https://vuejs.org/guide/scaling-up/ssr.html#hydration-mismatch