プロダクト開発

Next.jsで画像をHTMLから自動生成する方法【Vercel】

こんにちは、ぜに(@zenizh)です。このブログはNext.jsで作っていて、記事のOGP画像はHTMLから自動で生成しています。ライブラリにはPuppeteerを使い、Vercelから配信しています。

この機能を実装する上で、どのライブラリを選ぶか悩んだり、いざVercelにデプロイしたらエラーになったりと、いくつかの問題がありました。

この経験をふまえて、この記事では次について解説しています:

  • 画像を自動生成するときの要件
  • 画像を自動生成する方法の選択肢
  • Puppeteerによる画像の生成方法

この記事を読むことで、画像をHTMLから自動で生成できるようになります。コストをかけることなく、ユーザーに視覚的に分かりやすい形でコンテンツを届けられます。

著者
ぜに/Hiroki Zenigami

Webエンジニア&プロダクトマネージャ←プログラミング教育で起業←東大院←熊本高専。 共著に「現場で使えるRuby on Rails 5」。

目次

画像を自動生成するときの要件

まず、この記事で解説する機能の要件を整理しておきます。要件としては、次の3つを満たすものにします:

  • 任意のHTMLから画像を自動で生成する
  • 画像を取得できるURLを公開する
  • Next.jsで実装し、Vercelにデプロイする

この機能の主な利用例としてはOGP画像が考えられます。例えば、ブログのタイトルから画像を生成したりします。これははてなブログZennが採用していますね。

HTMLから画像を生成して、パブリックなURLを通して画像を取得できるようにします。このURLをOGPとして設定することで、コンテンツを公開すれば自動的にOGP画像が生成・設定されるようになります。

この機能を、今回はVercelにデプロイします。Vercelの制約として、サーバレス関数を50MB以下にする必要があります。使用するライブラリなど、実装方法によってはこの数字を超える可能性があるので、注意が必要になります。

loading...

画像を自動生成する方法の選択肢

選択肢のイメージ

この機能を実装する選択肢としては、大きく次の3つが考えられます:

  1. vercel/og-imageを利用する
  2. ライブラリを用いる。例えばPlaywrightnode-html-to-image
  3. Puppeteerで実装する

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を用いています。

動作環境

この記事で解説するコード例は、次に示す各環境で動作を確認しています:

ライブラリバージョン
next10.2.3
chrome-aws-lambda9.1.0
puppeteer-core9.1.1

Puppeteerによる画像の生成方法

ここでは例として、タイトルから画像を自動で生成する機能について解説します。例えば次のような画像が生成されます:

自動生成した画像の例 生成される画像の例

実装の基本的な流れとしては、次のようになります:

  1. URLからタイトルを取得する
  2. タイトルがないときは400を返す
  3. 画像のHTMLを定義する
  4. HTMLをバッファに入れる
  5. レスポンスとしてバッファを返す

タイトルは、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...

おわりに

ここではタイトルを例にしましたが、例えば画像にユーザー名を表示したり、背景画像を設定するようにすれば、より視覚的にユーザーを惹きつける画像にできると思います。

著者
ぜに/Hiroki Zenigami

Webエンジニア&プロダクトマネージャ←プログラミング教育で起業←東大院←熊本高専。 共著に「現場で使えるRuby on Rails 5」。

関連記事関連書籍人気記事
applis
エンジニアとしてのんびり暮らす
お問い合わせ
ご意見・ご質問やお仕事のご依頼などは下記よりお願いいたします
お問い合わせ
© applis