こんにちは、ぜに(@zenizh)です。このブログはNext.jsで作っていて、記事のOGP画像はHTMLから自動で生成しています。ライブラリにはPuppeteerを使い、Vercelから配信しています。
この機能を実装する上で、どのライブラリを選ぶか悩んだり、いざVercelにデプロイしたらエラーになったりと、いくつかの問題がありました。
この経験をふまえて、この記事では次について解説しています:
この記事を読むことで、画像をHTMLから自動で生成できるようになります。コストをかけることなく、ユーザーに視覚的に分かりやすい形でコンテンツを届けられます。
まず、この記事で解説する機能の要件を整理しておきます。要件としては、次の3つを満たすものにします:
この機能の主な利用例としてはOGP画像が考えられます。例えば、ブログのタイトルから画像を生成したりします。これははてなブログやZennが採用していますね。
HTMLから画像を生成して、パブリックなURLを通して画像を取得できるようにします。このURLをOGPとして設定することで、コンテンツを公開すれば自動的にOGP画像が生成・設定されるようになります。
この機能を、今回はVercelにデプロイします。Vercelの制約として、サーバレス関数を50MB以下にする必要があります。使用するライブラリなど、実装方法によってはこの数字を超える可能性があるので、注意が必要になります。
loading...この機能を実装する選択肢としては、大きく次の3つが考えられます:
vercel/og-imageは、Vercelが公開しているOGP画像生成ツールです。URLに文字列を指定すれば、画像を生成してくれます。ブログなどの簡単な用途なら、これでもいいですね。
ただ、カスタマイズ性が低いのと、日本語を表示するにはforkしての対応が必要になります。運用コストを払うなら、自作した方がいいと思います。
Playwrightはマイクロソフト製で信頼感がありますが、本来の用途はテストであり守備範囲が広いです。node-html-to-imageは依存ライブラリの関係で、Vercelの制約である50MBを超えてしまいます。
PuppeteerはChromeの操作を自動化するライブラリで、これを使えば簡単な実装で画像を自動生成できます。
Puppeteer自体はサイズが大きくVercelにデプロイできないですが、puppeteer-coreという本体のみがふくまれるライブラリを使えば問題ありません。
この記事ではPuppeteerを用いた実装について解説します。vercel/og-imageも、内部ではこのpuppeteer-coreを用いています。
この記事で解説するコード例は、次に示す各環境で動作を確認しています:
ライブラリ | バージョン |
---|---|
next | 10.2.3 |
chrome-aws-lambda | 9.1.0 |
puppeteer-core | 9.1.1 |
ここでは例として、タイトルから画像を自動で生成する機能について解説します。例えば次のような画像が生成されます:
生成される画像の例
実装の基本的な流れとしては、次のようになります:
タイトルは、Next.jsのDynamic Routesを使って取得します。まず、ライブラリをインストールします:
$ yarn add chrome-aws-lambda puppeteer-core
chrome-aws-lambdaはAWS Lambda用のChromiumバイナリです。Vercelはサーバレス関数をLambdaにデプロイする仕様のため、LambdaでChromiumを使うためにこれをインストールします。
Chromiumはオープンソースのブラウザで、PuppeteerはChromiumを制御するためのライブラリです。Puppeteerを通して、ChromiumでHTMLを描画・画像化する、ということになります。
コードは次のようになります。ファイル名は [title].tsx
です:
import { GetServerSideProps } from 'next'
import chromium from 'chrome-aws-lambda'
import puppeteer from 'puppeteer-core'
const Image: React.FC = () => {
return <></>
}
export const getServerSideProps: GetServerSideProps = async ({
res,
params,
}): Promise<any> => {
const { title } = params
if (!title) {
res.statusCode = 400
res.end('Bad Request')
return { props: {} }
}
const browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: { width: 1200, height: 675 },
executablePath: await chromium.executablePath,
headless: chromium.headless,
})
const html = `<html>
<head>
<style>
body {
width: 1200px;
height: 675px;
background-color: #f9fafb;
}
div {
display: flex;
justify-content: center;
align-items: center;
width: 60%;
height: 100%;
margin: auto;
color: #374151;
font-size: 3rem;
font-weight: bold;
line-height: 1.5;
}
</style>
</head>
<body>
<div>${title}</div>
</body>
</html>`
const page = await browser.newPage()
await page.setContent(html)
const buffer = await page.screenshot()
res.setHeader('Content-Type', 'image/png')
res.setHeader('Cache-Control', 'public, immutable, no-transform, s-maxage=31536000, max-age=31536000')
res.end(buffer, 'binary')
return { props: {} }
}
export default Image
Puppeteerのオプションについては公式ドキュメントをご覧ください。画像はPNGなどでも保存できますが、URLにアクセスしたときに画像を返すので、バイナリ形式にしています。
コード内で重要なのはキャッシュの設定です。キャッシュがないと、例えばTwitterでシェアしたときに画像が表示されません。
これは、アクセスのたびに画像が生成されることによるタイムアウトが原因です。サーバーへの負荷もかかります。
キャッシュについてはMDN Web Docsをご覧ください。コード内のキャッシュの具体的な値はvercel/og-imageを参考にしました。
loading...ここではタイトルを例にしましたが、例えば画像にユーザー名を表示したり、背景画像を設定するようにすれば、より視覚的にユーザーを惹きつける画像にできると思います。