Notion APIのように外部で期限付きで発行された画像URL(AWS S3 signed URL)とHTMLのキャッシュは相性が悪く、対策をしなければ簡単に画像が切れた状態のHTMLを配信することになってしまいます。
この問題に頭を悩ませている開発者はたくさんいることでしょう。私もその一人でした。
そんな開発者たちに向けて、この記事ではNotion APIが返す期限付きの画像URLの画像が切れてしまうメカニズムを解説し、その対策を3つ紹介します。
目次
画像が切れてしまうメカニズムを理解するためには、まずCDNの仕組みを理解しておく必要があります。
Webページが表示されるまでのステップを見てみましょう。
ユーザーがWebページをリクエストしたとき、まず最初にCDNにWebページがリクエストされます(上図①)。
CDNはユーザーから物理的に近いサーバー(エッジサーバー)からコンテンツを配信することで高速化を実現するサービスで、サービスを提供しているサーバー(オリジンサーバー)の負荷軽減の用途としても利用されます。
CDNの有名どころとしては Cloudflare や Fastly などがあります。Vercel もCDNに強みを持つ会社の一つです。
CDNにWebページがリクエストされたとき、CDNに対象のWebページのキャッシュがない場合は、そこからさらにオリジンサーバーにリクエストされます(上図②)。
オリジンサーバーは対象のWebページをCDNにレスポンスします(上図③)。
このとき、対象のWebページを事前に生成しておく方式を静的生成、リクエストされたときにWebページを生成する方式を動的生成と言います。
WebページにはNotion APIが毎回ランダムに返す画像URL(期限付きURL)が含まれています。
期限付きURLの期限はURLが発行されたとき、つまりWebページを生成するためにAPIリクエストしたときが起点となります。
オリジンサーバーからWebページを受け取ったCDNは、Webページのデータを自身に保存(キャッシュ)し、ユーザーにWebページのデータをレスポンスします(図④⑤)。
CDNがWebページのデータをキャッシュするのは、次に同じWebページがリクエストされた際に高速にレスポンスするためです。
下図をご覧ください。
CDNはリクエストされた対象のWebページのキャッシュを持っていれば、オリジンサーバーにリクエストすることなくユーザーにレスポンスします。
大幅にステップが減っていますね。
次に、どうして期限付きURLの画像が切れてしまうのか見ていきましょう。
先ほど、期限付きURLの期限はWebページが生成されたときから起算されていると言いました。
画像URLの期限が24時間だとします。
もし仮に、CDNが最初にキャッシュを作ったきり更新しないとどうなるでしょうか?
キャッシュはすでに生成されたWebページなので、そこに含まれている画像URLは古くなっています。
生成から24時間以上経過すると画像が切れてしまうというわけです。
実際には、CDNにそういう指示をしない限りキャッシュが更新されないままということはありません。
しかし、いくつかの状況ではそれが起こり得ます。
そのうちの一つがISRで、もう一つはTwitterでURLをシェアした際のアイキャッチ画像(OG画像)のようにサービスにOG情報がキャッシュされてしまうケースです。
ISRは Incremental Static Regeneration の略で、日本語では「段階的な静的サイト生成」と訳されます。
ISRは静的生成と動的生成の双方のメリットを取り入れた手法で、仕組みとしては最初にWebページを静的生成しておき、リクエストがあれば静的生成しておいたWebページ(キャッシュ)を返しつつ、Webページが古くなっていれば裏で再生成するというものです。
ポイントは、再生成が完了するまでは古くなったページをとりあえず返すというところで、この考え方を Stale While Revalidate と言います。
もうお気付きかと思いますが、ISRではどれだけページが古くなっていてもリクエストされればとりあえず古いページをレスポンスします。
ページに含まれている画像URLの期限が切れてしまっているということが起こり得るわけです。
Notion APIから取得した期限付きの画像URLをOG画像に使っている場合、ページが生成されて間もない頃はTwitterでシェアした際に画像がきちんと表示されるのに、しばらくすると画像が切れてしまうということが起こります。
これは推測ですが、おそらくTwitterでページをシェアした際にページに含まれるOG情報をTwitterが記憶(キャッシュ)しているのではないかと思っています。
もしそうであれば、最初にページをシェアしたときのOG画像URLを持ち続けているために、時間経過によって画像が切れてしまうということが起こり得ます。
ここまで期限付きURLによって画像が切れてしまうケースを説明してきました。
次は、これらの問題への対策としてどうすれば良いかを見ていきましょう。
HTMLで画像を使うには img
タグを用いますが、画像ソースを指定する src
プロパティに指定できるはURLやパスだけではないことをご存知でしょうか?
URLやパスといった画像の場所ではなく、画像自体のデータをエンコードしてHTMLに埋め込むData URIスキームという手法があります。
画像の場所を期限付きURLで指定しているから開くたびに期限に左右されるのであって、画像データそのものを埋め込んでしまえば期限は関係ないというわけです。
ただし、Data URIスキームにはデメリットもあります。
画像URLであればブラウザやCDNがキャッシュすることでページの高速化が見込めますが、Data URIスキームではデメリットとしてページを開くたびに画像をデコードする処理が必要になることが挙げられます。
Data URIスキームについては下記の記事をご覧ください。
また、OG画像はURLで指定する必要があるためData URIスキームは使えないことにも注意が必要です。
ページが表示されたときに、クライアント側のJavaScriptで画像の期限切れを判定して再度画像URLを取得し直すことができれば、一瞬だけ期限切れ画像が表示された後、きちんと画像を表示することができます。
これを実現するには、前述したSWRライブラリが便利です。
筆者が開発している easy-notion-blog でもこの方法を採用しています。
Notion APIは、Notionにアップロードした画像ごとに期限付きURLの期限を返してくれます。
この情報を使って画像の期限切れを判定し、必要に応じて画像を取得し直すことができます。
例えば easy-notion-blog ではSWRを使って下記のようにしています。
const isExpired = (block: Block): boolean => {
const now = Date.now()
if (block.Type === 'image') {
const image = block.Image
if (image.File && image.File.ExpiryTime && Date.parse(image.File.ExpiryTime) < now) {
return true
}
}
return false
}
const ImageBlock = ({ block: initialBlock }) => {
const { data: block } = useSWR(isExpired(initialBlock) && initialBlock.Id, fetchBlock, { fallbackData: initialBlock })
// ...
useSWR
フックの第1引数にはリクエストごとにユニークなキーを、第2引数にはデータを取得する関数を渡します。
重要な点として、第1引数が falsy な値の場合にはデータ取得関数が呼ばれません。
上記のコード例では第1引数は isExpired(initialBlock) && initialBlock.Id
になっています。
isExpired()
メソッドでは画像の期限切れを判定しており、期限が切れていると true
を返します。
論理積 &&
では左側の値から順に評価されます。
期限が切れていない場合には false && ...
となり、その時点で論理積の評価が終了し false
を返します。
期限が切れている場合には true && initialBlock.Id
となり右側の initialBlock.Id
が評価されます。
このとき一番右側の評価値が返されます。この場合はブロックIDです。
ブロックIDは falsy な値ではないので、期限切れの場合には第2引数のデータ取得メソッドが実行されます。
この方法の欠点としては、画像の期限情報のようにデータ再取得の要不要を判定するための情報が必要なことが挙げられます。
また、SWRはあくまでJavaScriptによって画像を取得し直す仕組みであるため、OG画像では使えません。
さて、ここまで挙げた2つはどちらもOG画像が切れてしまう問題の対策にはなりませんでした。
OG画像が切れてしまう原因は、TwitterなどのSNSが画像URLを記録してしまうこと、画像URLに期限があること、そしてそれに付随して画像URLが発行毎に変わることです。
つまり、無期限で固定の画像URLを用意することができればこの問題を解決することができます。
期限付きURLを直接OG画像に指定するのではなく、OG画像用にAPIを新たに定義して、みかけ上は固定されたURLで画像を取得できるようにします(下図)。
APIでは期限付きURLから画像を取得し画像自体をレスポンスします。
画像自体をレスポンスすることで期限を無くしているところは、前述したData URIスキームを使う方法と同じ考え方ですね。
APIでは都度画像を取得するので、画像が期限切れになることはありません。
下記は筆者の easy-notion-blog のコード例です。
リクエストパラメータとして与えられた slug
から記事を取得し、その記事のOG画像を取得してバイナリとしてレスポンスしています。
// ...
const post = await getPostBySlug(slug)
// ...
const { rawBody: image, headers: headers } = await got(post.OGImage)
res.setHeader('Content-Type', headers['content-type'])
res.setHeader('Cache-Control', 'public, s-maxage=86400, max-age=86400, stale-while-revalidate=86400')
res.write(image)
res.statusCode = 200
res.end()
// ...
これで筆者も長い間悩まされていたOG画像が切れてしまう問題を解決することができました。
この記事ではNotion APIが返す期限付きの画像URLの画像が切れてしまうメカニズムを解説し、その対策を3つ紹介しました。
今回は画像を前提として話をしましたが、Notion APIでは画像以外にも動画やPDFも同じように期限付きURLとしてレスポンスされるため、もし期限付きURLの扱いで困っている方はこの記事で紹介した方法をお試しください。
コメントを送る
コメントはブログオーナーのみ閲覧できます