web

Next.js × microCMS × VercelでOGP画像をブログのタイトルで自動生成する

公開日:2022年1月15日
最終更新日:2022年1月15日

ブログを書いてて面倒なのがOGP画像の作成です。デザイナーならササっと作れるかもしれませんが、エンジニアではそういきません。
そこでZennやはてなブログであるような、ブログのタイトルをOGP画像として生成するのをやってみました。

完成イメージはこちらです。

ライブラリインストール

node-canvasを利用します。
https://github.com/Automattic/node-canvas

node-canvas2.6.0 で固定しないと、Vercelのデプロイ時にエラーになるため、2.6.0 を使用します。

yarn add 'canvas@2.6.0'


2022年1月現在でバージョン指定しないと 2.8.0 がインストールされますが、そのままデプロイするとVercelのFunctionsのログで以下のエラーがでます。

Error: /lib64/libz.so.1: version `ZLIB_1.2.9' not found

VercelはAWSのLambdaを使っていて、node-canvasがバージョンアップにより容量が増えて、容量超過しデプロイできないそうです。確かに 2.8.0 でデプロイしたときは、Function DetailsのSizeが50MBパンパンで赤くなってましたが、2.6.0 に下げると、31MBほどに下がっていました。

ディレクトリ構成・追加ファイル

今回主に使用するファイルを書き出したディレクトリ構成です。
pages以下のディレクトリ構成は、プロジェクトに合った構成にしてください。

.
├── canvas_lib64 (本番環境デプロイで必要なファイル)
│   ├── libblkid.so.1
│   ├── libfontconfig.so.1
│   ├── libmount.so.1
│   ├── libpixman-1.so.0
│   └── libuuid.so.1
├── fonts (OGP生成時のフォント指定する場合に必要)
│   └── NotoSansJP-Bold.otf
├── pages(OGPを使う個別記事ページとそれに対応するOGPのAPI Router)
│   ├── api
│   │   └── blog
│   │       └── [category]
│   │           └── [id]
│   │               └── ogp.ts
│   ├── blog
│   │   ├── [category]
│   │   │   ├── [id]
│   │   │   │   └── index.tsx


フォント

フォントはライセンスに注意して、好きなフォントファイルをfonts以下に配置しておきましょう。
Googleフォントなどを使うといいと思います。
https://fonts.google.com/

API

OGP画像を生成するAPIです。
パラメータから記事IDを取得し、microCMSからそのidの記事タイトルを取得し、Canvasに描画しています。

import { client } from '@/libs/client'
import { NextApiRequest, NextApiResponse } from 'next'
import { createCanvas, Canvas, registerFont } from 'canvas'
import * as path from 'path'

interface SeparatedText {
  line: string
  remaining: string
}

const createTextLine = (canvas: Canvas, text: string): SeparatedText => {
  const context = canvas.getContext('2d')
  const MAX_WIDTH = 1000 as const

  for (let i = 0; i < text.length; i += 1) {
    const line = text.substring(0, i + 1)

    if (context.measureText(line).width > MAX_WIDTH) {
      return {
        line,
        remaining: text.substring(i + 1),
      }
    }
  }

  return {
    line: text,
    remaining: '',
  }
}

const createTextLines = (canvas: Canvas, text: string): string[] => {
  const lines: string[] = []
  let currentText = text

  while (currentText !== '') {
    const separatedText = createTextLine(canvas, currentText)
    lines.push(separatedText.line)
    currentText = separatedText.remaining
  }
  return lines
}

const createOgp = async (
  req: NextApiRequest,
  res: NextApiResponse
): Promise<void> => {
  const { id } = req.query

  const data = await client.get({
    endpoint: 'blog',
    contentId: String(id),
  })

  const WIDTH = 1200 as const
  const HEIGHT = 630 as const
  const DX = 0 as const
  const DY = 0 as const
  const canvas = createCanvas(WIDTH, HEIGHT)
  const ctx = canvas.getContext('2d')

  registerFont(path.resolve('./fonts/NotoSansJP-Bold.otf'), {
    family: 'NotoSansJP',
  })

  ctx.fillStyle = '#FFF'
  ctx.fillRect(DX, DY, WIDTH, HEIGHT)

  ctx.font = '60px NotoSansJP'
  ctx.fillStyle = '#000000'
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'

  const lines = createTextLines(canvas, data.title)
  lines.forEach((line, index) => {
    const y = 314 + 120 * (index - (lines.length - 1) / 2)
    ctx.fillText(line, 600, y)
  })

  const buffer = canvas.toBuffer()

  res.writeHead(200, {
    'Content-Type': 'image/png',
    'Content-Length': buffer.length,
  })
  res.end(buffer, 'binary')
}

export default createOgp


ローカルでここまで書くと、 http://localhost:3000/api/blog/{categoryname}/[blogid]/ogp をブラウザで開くとタイトルが描画された画像が生成されてると思います。

記事でOGP画像を設定する

まずは環境変数でベースのURLを指定します。これはOGP画像を絶対パスで指定するのですが、ローカルと本番でURLが異なるためです。 .env.development.local.env.local に以下を指定、もしくは追記します。

NEXT_PUBLIC_BASE_URL="http://localhost:3000"


人によって記事ページの書き方違うと思いますが、 http://localhost:3000/api/blog/{categoryname}/[blogid]/ogp のlocalhostの部分を環境変数に変えて、取得したOGP画像をmetaタグに指定すればOKです。
以下参考までに書いておきます。

const BlogDetailPage: NextPage<PageProps> = (props) => {
  const { blog, draftKey, body, category, currentCategory } = props

  const meta = {
    path: 'blog',
  }

  const ogpImageUrl = `${process.env.NEXT_PUBLIC_WEB_URL}/api/blog/${currentCategory}/${blog.id}/ogp`

  return blog ? (
    <Layout path={meta.path} disableLoading>
      <Seo
        title={blog.title ? blog.title : ''}
        description={blog.description ? blog.description : ''}
        path={meta.path}
        ogpImageUrl={ogpImageUrl}
      />
      <BlogDetailContent
        data={blog}
        body={body}
        category={category}
        currentCategory={currentCategory}
      />
    </Layout>
  ) : (
    <div>no content</div>
  )

seo.tsx

metaタグに取得したパスを指定しておきます。

<meta property="og:image" key="ogImage" content={ogpImageUrl} />
<meta
  name="twitter:card"
  key="twitterCard"
  content="summary_large_image"
/>
<meta name="twitter:image" key="twitterImage" content={ogpImageUrl} />


本番デプロイ

Vercelでデプロイします。

環境変数

まずは環境変数を追加しておきます。

NEXT_PUBLIC_WEB_URL: サイトドメイン


canvas_lib64を追加

ルートにcanvas_lib64ディレクトリを追加し、以下の5つのファイルを配置しておきます。
https://github.com/mismith0227/mismith.me/tree/master/canvas_lib64

package.json

package.jsonの scripts に以下を追加しておきます。

"now-build": "cp canvas_lib64/*so.1 node_modules/canvas/build/Release/ && yarn build"


これでデプロイすればOGPが生成されるはずです。

Twitter Card Validator

以下でOGP画像のバリデーションができます。
https://cards-dev.twitter.com/validator

以下のメッセージが出た時は、robots.txtにapi以下をdisableにする記述があるかもしれません

specified by the 'twitter:image' metatag may be restricted by the site's robots.txt file, which will prevent Twitter from fetching it.


参考サイト

この記事ではテキストを描画するだけでしたが、背景が画像も設定したい場合は参考サイトで紹介されています。

About the author

大阪でフロントエンドエンジニアをしています。写真を撮るのが趣味です。よかったら500pxに載せてる写真も見てください。
web上に公開しているので、正確さに可能な限り努力してますが、個人の備忘録程度に書いてるので、ご自身の判断で参考程度に読んでください。
間違いやご意見があれば、コンタクトやSNSに気軽にご連絡ください。

Read next

Category

  • web
  • 雑記