loaderにサムネイル画像を返すAPIを追加する
では次に、サムネイル画像を生成する処理を作ってみましょう。今回は、vercel/ogというライブラリの力を借りて、画像生成をしてみようと思います。このライブラリは本来、VercelのEdge Functionsという環境で使うために作られたものですが、Cloudflare PagesのFunctions環境でも使えるラッパーがCloudflareから提供されています。
vercel/ogの画期的な点は、JSX記法で記述したHTMLを画像として出力できるところです。実際の動作を確認してみましょう。まずは、Cloudflare版のvercel/ogをインストールします(リスト4)。
npm install @cloudflare/pages-plugin-vercel-og
次に、サムネイル画像を生成するための関数を作成します。/top20/:記事ID/ogp.png
のようなパスにしたいので、ルートファイルの名前はtop20.$id.ogp[.]png.jsx
にします。[]
で囲んだ部分はパスの区切りではなく文字列としてそのまま扱われるため、ogp/png
ではなくogp.png
がパスとして認識されます。
また、このファイルを作ると、元々あったtop20.$id.jsx
がレイアウトファイルとして認識されてしまうので、ページであることを明示するためにtop20.$id._index.jsx
にリネームします(図10)。
それでは、実際の画像生成の処理を書いてみましょう(リスト5)。
png.jsx] import { getItem } from '~/utils/hackerNews.server'; export const loader = async ({ params }) => { // (1) APIからデータを取得する const { id } = params; const item = await getItem(id); // (2) vercel/ogのモジュールをインポートする const { ImageResponse } = await import('@cloudflare/pages-plugin-vercel-og/api'); // (3) JSXでサムネイル画像のレイアウトを定義する return new ImageResponse( <div style={{ display: "flex", width: '100%', height: '100%', alignItems: 'center', justifyContent: 'center', fontSize: 40, }} > {item.title}{/* (4) テキストを埋め込む */} </div>, // (5) 画像のサイズを指定する { width: 1200, height: 630 }, ); };
(1)でAPIからデータを取得するのは、通常の記事ページと同じ処理です。(2)では、vercel/ogのモジュールを動的インポートしています。通常のインポートを行うとローカル環境でビルドエラーになるためです。実環境でしか動かないので、動作確認も実環境で行なってください。
(3)では、どんな画像にするかを設定しています。ImageResponse
の第一引数にはJSXでHTMLを記述しました。(4)で記事のタイトルを埋め込んでいます。(5)の第二引数には、オプションとしてサムネイル画像のサイズを定義しています。このファイルをCloudflare Pagesにデプロイして、実際に画像が生成されるか確認してみましょう。デプロイが完了したら、ブラウザでhttps://[サイトのURL]/top20/41371106/ogp.png
にアクセスします(図11)。
画像が生成されていることが確認できました。JSXで指定した通り、記事のタイトルが画像の中央にレイアウトされています。
キャッシュの設定を行う
さて、画像の生成はできましたが、キャッシュの設定はどうなっているでしょうか。ドキュメントによれば、CloudflareはデフォルトでPNG拡張子のレスポンスをキャッシュすることになっています。開発者ツールを開いたまま/top20/41371106/ogp.png
にアクセスして、どんなキャッシュ設定になっているかみてみましょう(図12)。
どうやら、Cache-Control: public, max-age=31536000, immutable, no-transform
が設定されているようです。新鮮な期間は1年間、再検証は行わない、変換は行わないという設定ですね。これだとタイトルが変わったときに、画像が更新されなくて困ってしまうので、キャッシュの設定を変更してみましょう。
ImageResponse
の第二引数に、ヘッダーを指定するオプションを追加することで、キャッシュの設定を変更できます(リスト6)。
png.jsx] // (略) { width: 1200, height: 630, headers: { 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=60', }, } // (略)
このように設定することで、画像のキャッシュが60秒間有効で、60秒間は古いキャッシュを返しつつ、オリジンサーバーに再検証を要求するようになります。これで、画像の更新が反映されやすくなるはずです。次は実際の動きを見てみましょう。CDNキャッシュがない状態でアクセスすると、まずはMISS状態になります(図13)。
Cache-Control
はPagesがつけたデフォルトのヘッダーが混ざっておりmax-age
とs-maxage
が併記されていますが、併記した場合はs-maxage
が優先されるため、60秒間はキャッシュが新鮮な状態で返されます。再度アクセスするとHIT状態になります(図14)。
さらにその後、60秒以上経過してからアクセスすると、新鮮ではなくなっているのでEXPIREDになります(図15)。
その裏側では再検証が働いているので、すぐ後にまたアクセスすると、再検証によって得られたキャッシュが使えるのでHIT状態に戻ります(図16)。
意図したキャッシュ更新が行われていそうなことが確認できました。loader側からCache-Control
を設定することで、Webサイトの強力な味方であるCDNキャッシュを設定することができました。
今回はCloudflareの例でしたが、他のCDNサービスではHTMLやJSONのキャッシュも含めて、また違った戦略を取ることができるかもしれません。各環境に合わせて、工夫してみましょう。
さいごに
今回は、Remixの得意な領域であるWebサイト制作とは切っても切れない関係にあるCDNキャッシュの制御について解説しました。キャッシュを活用することで、例に挙げた画像生成の重い処理が実行される頻度を下げることができます。速度向上によるユーザーへのメリットだけでなく、サーバー費用の削減にも繋がるため、積極的に活用していきたいですね。
さて、Remixを題材にしてWeb標準を学んできた本連載も、今回で最終回となります。ルーティングについては独自の考え方もあったものの、通信に関してはWeb標準を最大限活用した設計が行われているフレームワークであることが、本連載を通じてわかっていただけたのではないでしょうか。
Remixは今後、React Router v7という名前で再出発することがアナウンスされています。本連載で学んだことの大部分は、React Router v7でも役立つはずですし、もし運悪くそうでなかったとしても、皆さんが学んだWeb標準の知識は、どんなに新しいフレームワークが出てきても活かせるものであるはずです。
RemixとReact Routerの今後の発展にも期待しつつ、本連載を読んでいただいた皆さんが、Web標準を活用したWebサイト制作に挑戦するきっかけになれば幸いです。