👏

Next.js + Vercel でSPAの動的OGPに対応する

2021/12/11に公開

結論

Next.js + VercelでISRを使う。

この結論に至るまで試行錯誤したので、その過程も含めて記事にします。

環境

React + Firebase

前提: SPAと動的OGP

SPAとFirebaseのデータベースサービスであるFirestoreの相性が非常にいいので最近よく使っているのですが、動的OGPに対応するには工夫が必要です。
OGPは現在では主にTwitter/Facebook等のSNSでシェアされた際の拡散を目的に、タイトルや概要・アイキャッチ画像を設定されています。

問題はTwitter/FacebookのクローラーがOGPを読み込む際、javascriptの処理を行わないことです。当たり前ですがSPAではOGPでさえjavascriptで生成します。動的コンテンツをもとにOGPを作るとSNSのクローラーに正しく読み込まれません。

その解決法としてNext.jsとVercelのホスティングサービスを使う方法を紹介しますが、その結論に至るまでに試した方法も合わせて書き残しておきたいと思います。

1. Firebase Hosting + Cloud Functions (失敗)

FirebaseにSSRを実現する方法が用意されています。
Cloud Functions を使用した動的コンテンツの配信とマイクロサービスのホスティング

Cloud Functionsをアプリケーションサーバーのように見立て、リクエストに応じたOGPを返す方法です。
ですがこの方法はFirebase HostingとCloud Functionsが連携しているリージョンでしか使えず、現在はus-central1だけが対応しています。


OMFG

要件によりますがホスティングサーバーを北米に置くことを許容できればありかもしれません。

2. Firebase Hosting + Next.js (失敗)

それならばSSRを提供してくれるフレームワークを使おうということでNext.jsに手を出しました。Next.jsの場合はSSG(Static Site Generator)です。getStaticPropsgetStaticPathsを利用して、OGPに必要なデータをあらかじめ生成しておく方法です。

ただしこの方法はFirebase Hostingでは使えません。Firebase HostingはファイルシステムがRead-Onlyで構築されています。デプロイ時のファイル以外の動的なファイル生成、この場合はOGP生成に必要なファイルの書き出しには対応していません。

3. Vercel Hosting + Next.js (成功)

結局Next.jsを開発しているVercelのホスティングサービスを利用することで解決しました。もちろんSSGに対応しています(ちなみにVercelはAWSで作られてます)

サンプルコードです。 /users/:userIdにユーザーのプロフィールページがあるとします。

src/pages/users/[userId].jsx
import Head from 'next/head'

const User = ({ user }) => {
  const title = `${user.nickname} のプロフィール`

  return (
    <>
      <Head>
        <title key="title">{title}</title>
        <meta key="twitter-card" name="twitter:card" content="summary" />
        <meta key="og-url" property="og:url" content={'https://...'} />
        <meta key="og-title" property="og:title" content={title} />
        <meta key="og-description" property="og:description" content={user.description} />
        <meta key="og-image" property="og:image" content={user.photoURL} />
      </Head>

      {/* ページコンテンツ ... */}
    </>
  )
}

export const getStaticProps = async (context) => {
  const userId = context.params.userId
  const user = await fetchUser(userId)

  if (user) {
    return {
      props: {
        user: {
          id: user.id,
          nickname: user.nickname,
          description: user.description || null,
          photoURL: user.photoURL || null,
        },
      },
      revalidate: 30,
    }
  }

  return {
    notFound: true,
  }
}

export const getStaticPaths = async () => {
  return {
    paths: [],
    fallback: 'blocking',
  }
}

export default User

getStaticPropsでユーザーのデータを取得します。revalidateを設定することで、設定した秒数後にリクエストが来た場合には再度データフェッチを行いキャッシュファイルを生成します(ISR: Incremental Static Regeneration)
また、getStaticPathsfallback: 'blocking'を設定するとgetStaticPropsでのデータフェッチを待ってからレスポンスを返します。

Next.jsのSSGの仕組みについては、この記事にまとめられており非常にわかりやすかったです。
https://zenn.dev/akino/articles/78479998efef55

ISRにも弱点はあります。ユーザーデータが更新された場合でも、getStaticPropsでの最後のデータフェッチからrevalidateの秒数が経過するまでは、更新されていな古いキャッシュデータをもとにOGP生成されます。
今回はrevalidateの時間を短めに設定すれば問題ないと判断しましたが、確実に最新のデータが欲しい場合はISRではなくSSRを使うのがいいでしょう

以上です。

Discussion