Remix on Cloudflare Pagesで、@vercel/ogを使ってOGP画像を生成する
こんにちは、Webサイト作ってますか? Webサイトを作ってると、SNSやSlackにURLが貼られた時の見栄えを良くするために、OGP画像(og:image)を設定したくなりますよね。
筆者はCloudflare PagesとRemixでサイトを作ることが多くなってきたのですが、OGP画像を動的に生成する上手い方法を調べていたら一定の成果が出たので、共有しておこうと思います。
要点としては次のとおりです。
- Cloudflare Pages向けの
@vercel/og
ラッパーを使う - できたらURLの末尾を
.png
にする(Cloudflareのキャッシュを効かせるため)
@vercel/og
VercelのOGP画像生成ライブラリ OGP画像を動的に生成する分野には一定の需要がありますが、簡単に実現する方法というのはあまり多くありません。そんな中、Vercel社が提供している @vercel/og
というライブラリが一時期話題になりました。
@vercel/og
は、HTMLとCSSで組まれたレイアウトを、PNG形式で出力するためのライブラリです。内部的には、HTML+CSSをSVGに変換するsatoriと、SVGをPNG等のラスター画像に変換するresvgで構成されています。
Vercel社が作っているライブラリということもあって、VercelプラットフォームのEdge Runtimeで使用される前提で作られています。エッジでOGP画像を動的生成して、そのままキャッシュすることで、コンピューティングリソースを効率的に使うことができるわけです。
先行研究
では、Cloudflare Workersでも同様のことをしたい場合は、どうすればよいのでしょうか。先人たちが取り組んでくれていました。
wasm版を使うのはなるほどなあとは思いつつ、satoriやresvgのwasm版を頑張って使う、という感じで、継続して使うには少しハードルが高いように感じました。
Cloudflare Pages向けのラッパーがあった
という情報を集めていたのが4月ごろだったのですが、6月になってから改めて調べたところ、Cloudflare Pages向けのラッパーを見つけました。
先行研究ではCloudflare Workersが使われていましたが、「あるWebページに対応した」「キャッシュされてほしい画像」を作るという目的を考えると、Cloudflare PagesのFunctionsで処理を行うのが適当ということなのでしょう。
これを使うと簡単に @vercel/og
を使うことができます。
もう紹介したいことの9割は紹介してしまったので、ドキュメントを読んで「はいはいそういうことね」と納得した方は時間がもったいないのでここで読み終わっておきましょう。残りは蛇足です。
使い方
というわけで、ここからはRemixでのちょっと実践的な使い方を紹介します。いうても /functions
フォルダの中でドキュメントの通りにライブラリを使うだけで、ちょっとだけRemixらしい味付けをする程度です。
まずは、Cloudflare Pages向けにセットアップされたRemixプロジェクトを作っておきましょう。
$ npm create cloudflare@latest -- cloudflare-pages-vercel-og-remix-sample --framework=remix --deploy=false
$ cd cloudflare-pages-vercel-og-remix-sample
ついでにライブラリもインストールしておきます。
npm install @cloudflare/pages-plugin-vercel-og
ヨシ!
下準備
@vercel/og
は非常に柔軟な表現ができるので、綺麗な背景を描画することも可能なのですが、どう考えても筆者のCSS力が足りないので、今回は背景に画像を貼り付けて豪華さを演出することにします。ChatGPTパイセンに「なんかいい感じのOGP用背景を作ってよ」とお願いしたら、こんな感じの画像を作ってくれました。
この背景の上に、別のアイコンやらテキストやらを重ねていく感じで進めてみましょう。
というわけで、適当に public/img/ogp-background.png
というパスで保存しておきます。これはデプロイ後に /img/ogp-background.png
としてアクセスできるようになることを期待して配置します。
これで下準備ができました。
各ページにOGP画像を設定する
今度は実際にOGP画像を生成する関数を作っていきましょう。app/routes/articles.$slug.tsx
に定義したページ(つまり /articles/:slug
で表示されるページ)のURLを貼ったときに出てくるOGP画像を生成したいと思います。
/articles
以下のページにアクセスされたときのリクエストやレスポンスに介入する処理なので、Middleware機能を利用するために /functions/articles/_middleware.tsx
を作成します。
import vercelOGPagesPlugin from "@cloudflare/pages-plugin-vercel-og";
export const onRequest = vercelOGPagesPlugin({
imagePathSuffix: "/og-image.png", // ファイル名を定義
component: ({ pathname }) => {
// pathnameから最後の要素を抜き出す
const paths = pathname.split("/");
const slug = paths[paths.length - 1];
return (
<div
style={{
// flexboxで中央寄せ
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
// 画像の幅いっぱいまでdivを広げる
width: "100%",
height: "100%",
}}
>
<h1 style={{ fontSize: "80px" }}>
This is {slug} article
</h1>
</div>
);
},
options: {
// 画像のサイズを指定
width: 1200,
height: 630,
},
autoInject: {
// ページのhead要素内ににOGの<meta>要素を追加
openGraph: true,
},
});
URLのパスからslugを取り出して、それを使って component
プロパティにJSXでレイアウトを組んでいます。画像の中央にテキストを表示するだけのシンプルなものです。パラメータ自体はほとんどsatoriと共通なので、特には説明しません。
imagePathSuffix
は、元のURLに対応するOGP画像がどのパスに保存されるかを指定します。今回は、ページURLの末尾に og-image.png
がついたパスに保存されるようにしていますので、/articles/hogehoge
にアクセスされたときには /articles/hogehoge/og-image.png
のパスでOGP画像が取得できるようになります。
実際にCloudflare Pageにデプロイして、取得した画像は次のような感じになります。
背景が透過しているので、このままだと環境によっては見づらいかもしれません。
見栄えを良くするために、先ほど作った背景画像もセットしてみましょう。
// (略)
return (
<div
style={{
// flexboxで中央寄せ
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
// 画像の幅いっぱいまでdivを広げる
width: "100%",
height: "100%",
// 背景画像を設定
+ backgroundImage: 'url("https://cloudflare-pages-vercel-og-remix-sample.pages.dev/img/ogp-background.png")',
}}
>
- <h1 style={{ fontSize: "80px" }}>
+ <h1 style={{ fontSize: "30px" }}>
This is {slug} article
</h1>
</div>
);
// (略)
backgroundImage
はフルパスで指定するのがコツです。上手く背景に収めるために、テキストのフォントサイズは小さくしておきました。
これをデプロイして、 /articles/hogehoge/og-image.png
にアクセスすると、次のような画像が取得できます。
上手いこと背景画像が表示されました。凝った背景は事前にラスター画像として用意しておくことで、少ない労力で見栄えを良くすることができます。
HTMLへの埋め込みは自動
前述の通り、 autoInject.openGraph = true
が設定されているので、HTMLには自動で設定されます。 /articles/hogehoge
にアクセスした場合のHTMLを確認してみましょう。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charSet="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/assets/root-BFUH26ow.css"/>
<meta property="og:image" content="https://cloudflare-pages-vercel-og-remix-sample.pages.dev/articles/hogehoge//og-image.png" />
<meta property="og:image:height" content="630" />
<meta property="og:image:width" content="1200" /></head>
<!-- 略 -->
meta要素が自動で挿入されています。便利ですね。
任意のパスでもOGP画像を生成できる
@cloudflare/pages-plugin-vercel-og
のデフォルトの関数を使った場合は、middleware機能を前提としたAPIになっていますが、もちろん任意のパスでOGP画像を生成することもできます。
import { ImageResponse } from "@cloudflare/pages-plugin-vercel-og/api";
export const onRequest: PagesFunction = async () => {
return new ImageResponse(
<div style={{ display: "flex" }}>Hello, world!</div>,
{
width: 1200,
height: 630,
}
);
};
こちらの方法のほうが、@vercel/ogのサンプルに近い形ですね。
この方法で作成した場合は、 /greet
にアクセスすることでOGP画像を取得できます。通常のFunctionsのルーティングに従ってパラメータを取得できるので、ぜひ活用してみてください。
注意点として、公式サンプルでは /greet
というパスで画像を返していますが、デフォルトではこのパスにはキャッシュが効きません。キャッシュが効くように明示的に設定することも可能だとは思いますが、Cloudflareのデフォルトのキャッシュ設定に含まれるようにパス名を変更した方が簡単です。コード側のファイル名を /functions/greet.png.tsx
に変更しておくと、アクセス時のパスが /greet.png
になるので、キャッシュが効くようになります。
成果物
というわけで、今回のソースコードはこちらにまとめてあります。
実際に動作している様子はこちら。
まとめ
Cloudflare Pages環境でも簡単に @vercel/og
を使ってOGP画像を生成する方法を紹介しました。ルーティングや画像置き場についてはRemixに準拠して解説しましたが、Hono等でも同様の手順で実施できるはずです。
Discussion
素晴らしい情報をありがとうございます。
一点修正お願い致します。
記事本文の中のファイル名が
/functions/articles/_middleware.ts
指定になっており若干ハマってしまいました。/functions/articles/_middleware.tsx
へ差し替えお願い致します。ご指摘ありがとうございました!本文を修正しました!
コードのタイトルのほうはtsxにしてあったので、うっかりミスですねえ……