Next.js + Vercel でSPAの動的OGPに対応する
結論
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)です。getStaticProps
とgetStaticPaths
を利用して、OGPに必要なデータをあらかじめ生成しておく方法です。
ただしこの方法はFirebase Hostingでは使えません。Firebase HostingはファイルシステムがRead-Onlyで構築されています。デプロイ時のファイル以外の動的なファイル生成、この場合はOGP生成に必要なファイルの書き出しには対応していません。
3. Vercel Hosting + Next.js (成功)
結局Next.jsを開発しているVercelのホスティングサービスを利用することで解決しました。もちろんSSGに対応しています(ちなみにVercelはAWSで作られてます)
サンプルコードです。 /users/:userId
にユーザーのプロフィールページがあるとします。
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)
また、getStaticPaths
でfallback: 'blocking'
を設定するとgetStaticProps
でのデータフェッチを待ってからレスポンスを返します。
Next.jsのSSGの仕組みについては、この記事にまとめられており非常にわかりやすかったです。
ISRにも弱点はあります。ユーザーデータが更新された場合でも、getStaticProps
での最後のデータフェッチからrevalidate
の秒数が経過するまでは、更新されていな古いキャッシュデータをもとにOGP生成されます。
今回はrevalidate
の時間を短めに設定すれば問題ないと判断しましたが、確実に最新のデータが欲しい場合はISRではなくSSRを使うのがいいでしょう
以上です。
Discussion