[OGP画像] ブログ記事タイトルを含んだ画像を自動生成する方法

概要

SNS シェア用に記事タイトルを含んだ画像を自動生成する。

完成イメージ

下記に対応する。

<meta name="twitter:card" content="summary_large_image"></meta>

環境

現在、当ブログは Hexo で記事を生成している。
そのため、 Hexo で記事生成と同時に Node.js を利用して、summary_large_imageに対応する画像を生成する。

方法

node-html-to-imageを利用する。このライブラリは、puppeteer を使って指定の HTML を画像に変換することができる。そうすることで直接画像を生成するための調整をしなくて良い。

const generateOgpImage = require("./generate-ogp-image");

hexo.extend.generator.register("posts", (locals) => {
  const posts = locals.posts;
  const ogpImages = [];

  posts.forEach((post) => {
    ogpImages.push({
      title: post.title,
      output: `${ogpDirectoryPath}/${post.slug}.png`,
    });
  });

  // OGP image を生成
  generateOgpImage(ogpImages);
});
// generate-ogp-image.js
const nodeHtmlToImage = require("node-html-to-image");
const fontData = require("./fontdata"); // ファイルサイズが膨大なので分割

module.exports = function (content) {
  nodeHtmlToImage({
    html: `<html lang="ja">
    <head>
      <style>
        @font-face {
          font-family: 'NotoSansJP';
          src: url("data:font/woff;base64,${fontData}");
        }
        body {
          display: flex;
          align-items: center;
          justify-content: center;
          padding: 1em 2em;
          font-family: 'NotoSansJP', sans-serif;
        }
        .title {
          margin: 0;
          color: #212529;
          font-size: 1.85em;
          line-height: 1.75;
        }
        .url {
          position: absolute;
          right: 1em;
          bottom: 1em;
          z-index: 1;
          color: #343a40;
          font-size: 1em;
        }
      </style>
    </head>
    <body>
      <h1 class="title">{{title}}</h1>
      <div class="url">b.0218.jp</div>
    </body>
  </html>
  `,
    content: content,
    puppeteerArgs: {
      defaultViewport: {
        width: 600,
        height: 315,
      },
    },
  });
};

生成のポイント(複数枚の画像を生成する)

node-html-to-image は、バッチ処理を行うことができる(v3.0.0)。

nodeHtmlToImage({
  html: "<html><body>Hello {{name}}!</body></html>",
  content: [
    { name: "Pierre", output: "./image1.png" },
    { name: "Paul", output: "./image2.png" },
    { name: "Jacques", output: "./image3.png" },
  ],
});

contentパラメーター内のoutputにパスを指定すると、指定したパスで画像を生成してくれる。

ブログの記事数が数百件あるので、それを 1 つずつ生成していくと生成に時間がかかるどころか、実際 10 インスタンスぐらいで落ちた。

フォントの問題

Google Fonts などから日本語ウェブフォントを読み込んでテキストに適応したかったのだが、上手く行かなかったため Base64 にしてインラインに展開した。(先述のコードだとfontData.jsがそれにあたる)

Noto Sans JPを使用しているが、woff ファイルなどが GitHub 上で公開されているので下記のコマンドで Base64 のデータを取得した。

curl -L 'woffのrawデータのURL' | base64 | pbcopy

その他

GitHub Actions 内での実行で画像が生成されない問題が発生したが、yarn clean && yarn buildを事前に指定することで解消した。