ISIRMT Logo

【Next.js】日付カードコンポーネントとHydrationWarning

Next.js
Tailwind CSS
シリーズ『旧ブログのCSS設計』

完成物

clipboard.png

上のようなものを作ります。当ブログにもある年・月・日を表示します。

作成

イメージ

イメージは「日めくりカレンダー」を意識しています。日付を横書きで記すだけでも良いと思いましたが、アクセントを付けたいということから始まりました。

コンポーネンの作成

執筆時のソースコードを用います。

https://github.com/isirmt/NextjsBlogWithGitPAT/tree/1a763a1af88c7d9e111a25fde6d86f87a7494274

まずは全文から。

/src/components/post/DateCard.tsxtsx
import { mplus2 } from '@/lib/font';

export default function DateCard({ date }: { date?: string }) {
  const formattedDate = date ? new Date(date.replace(/-/g, '/')) : undefined;

  return (
    <time className={mplus2.className} dateTime={formattedDate?.toISOString()}>
      <div title={formattedDate?.toISOString()} className='flex items-center'>
        <span className='ml-2 w-6 translate-y-4 -rotate-90 text-sm leading-4'>
          {formattedDate ? formattedDate.getFullYear() : ''}
        </span>
        <div className='mr-4 flex size-16 flex-col items-center justify-center rounded-xl bg-gray-100 transition-colors dark:bg-slate-700'>
          {formattedDate ? (
            <div className='text-center'>
              <div className='text-sm leading-4'>{formattedDate.getMonth() + 1}</div>
              <div className='text-2xl leading-6'>{formattedDate.getDate()}</div>
            </div>
          ) : (
            <span className='text-3xl'>🎉</span>
          )}
        </div>
      </div>
    </time>
  );
}

フォントについて

まずはフォントから、M PLUS 2を使っています!

https://fonts.google.com/specimen/M+PLUS+2

<無題>tsx
import { M_PLUS_2 } from 'next/font/google';

export const mplus2 = M_PLUS_2({
  subsets: ['latin'],
  weight: '400',
});

このように、next/font/googleからフォントをインポートし、インスタンスを作成することで使用可能になりexportで他のファイルで利用可能にします。

HTML/CSS 設計について

まずは、timeタグで括ります。ここで、datetime属性を付与しておくことでSEO効果が..?

渡す値はISOフォーマットで問題ないでしょう。

https://developers.google.com/search/blog/2019/03/help-google-search-know-best-date-for?hl=ja#how-to-specify-a-date-on-a-page

ここによると、以下の記載があります。

明確な日付を表示する: ページ上で目立つように日付を表示します。

HTML内で明確な記載があることに越したことはありません。

clipboard.png

実際、このように出力されている状態は正確です。

次に、表示の設計です。大きなdivで2つの要素を並べます。1つは「年」を回転した状態で表示する。もう1つはカレンダー形式で表示する、です。

「年」の表示jsx
<span className='ml-2 w-6 translate-y-4 -rotate-90 text-sm leading-4'>
  {formattedDate ? formattedDate.getFullYear() : ''}
</span>

ここで、leading-4は1行あたりの高さを表しており、複数行ある場合の間の調整に使うオプションです。90度回転し、inline-blockとして判定を持っているので直接横のカレンダーとの隙間に響きます。

「月」「日」の表示jsx
<div className='mr-4 flex size-16 flex-col items-center justify-center rounded-xl bg-gray-100 transition-colors dark:bg-slate-700'>
  {formattedDate ? (
    <div className='text-center'>
      <div className='text-sm leading-4'>{formattedDate.getMonth() + 1}</div>
      <div className='text-2xl leading-6'>{formattedDate.getDate()}</div>
    </div>
  ) : (
    <span className='text-3xl'>🎉</span>
  )}
</div>

まずは、formattedDateの定義によって分岐します。日付が存在しない場合を考慮して、取り敢えず🎉を表示するようにしました。

日付を中央ぞろえで表示し、formattedDate.getMonth() + 1のように、月は+1で表示します。そのままでは0~11の出力になるので注意です。

あとは、leadingで表示間を調整して完成です!

HydrationWarning (Error)について

今回のブログ作成で個人的に一番遭遇したエラーです。サーバーサイドとクライアントサイドのレンダリングに差があると生じます。

今回の件では同じ引数でも面白いことが起こります。

clipboard.png

とPCでは表示されて、iPhoneでは、

clipboard.png

まさかの NaN 出力!

スマホでローカルサーバにアクセスしたとき驚きました。原因解決を図っていたところ、違う表記にすればPCと同様の結果が得られました。

それが今回の

<無題>tsx
const formattedDate = date ? new Date(date.replace(/-/g, '/')) : undefined;

ですね。-を/に置き換えただけです。つまり、-が無ければ問題回避ができました。そのため、現状はこのまま運用しています。

良い修正案はあるか?

しかし、あまり美しい修正方法とは言えません。今回の発生原因はクライアントサイドでコンポーネントを呼び出すことで発生します。サーバーサイドレンダリングでは完成済みで提供してくれますからね。

つまり、-が無ければ問題回避ができました。

電話番号として認識されたりするのでしょうか。

<無題>html
<meta name="format-detection" content="telephone=no"/>

iOSの親切な設計が表示を変えてしまいエラーを起こすならこれで解決するかも...?

もしくは、inline-blockとして確立してもらうために<span>で挟めば解決するかも...?

また検証した際に執筆しようと思います。

関連記事