CloudFunctions+bootstrapでアイキャッチを作る


Twitterとかに投稿した時に表示されるアイキャッチ画像を自動生成したい。
しかし、画像処理とか段組の計算とかイロイロメンドクサイ。
bootstrapでいい感じにできないか試した結果突破できたので書き残す。

概要

まず、Cloud Functionsではpuppeteerが動いた。
ということは、HTMLで作れるものは大体画像として作れるということだ!
具体的には、node-html-to-imageというライブラリを使えばいい。

注意点

フォント

まず、そのままだといわゆる中華フォントになる。

段組み

bodyのwidthの解釈など、色々ハマりどころが多い。
僕は中央寄せがしたいだけなのに!!
ということで、色々頑張った結果とりあえずまともに動くStyleを特定したので
特にこだわりなければこれをそのまま使ってくれたし。

メモリ

puppeteerは動くが、そのままではメモリが足りなくてエラーになる。
メモリは多めに確保しておこう

キャッシュ

当たり前だと思うけど、この処理は結構重い。
上手くキャッシュしよう。
Cloud Functionsは公開キャッシュが可能なので長めに設定しよう。
ここで、古いキャッシュがヒットしてしまわぬようにCloud FunctionsのURL設計を考えたい。
例えば、ユーザー名を表示するアイキャッチを作っていたとする。
この時、引数にユーザーIDを取りFunctionsのなかでDBを参照してユーザー名を表示するよりも、
Functionsの引数にユーザー名をとったほうがCDNフレンドリーだと思う。
ユーザー名の変更に追従する時のめんどくささがなくなるので。

成果物

functions.ts
export const og = functions
    .runWith({
      memory: "1GB",
    })
    .https.onRequest(async (req: express.Request, res: express.Response) => {
      const userName = escape(req.query["user_name"] as string);
      const buffer = await createOg(userName);
      res.header("cache-control", "public, max-age=10000");
      res.write(buffer);
      res.end();
    });

const nodeHtmlToImage = require("node-html-to-image");

export async function createOg(
    userName: string,
): Promise<Buffer> {
  const res = (await nodeHtmlToImage({
    quality: 100,
    type: 'png',
    html: `<html>
<head>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Noto+Sans+JP" rel="stylesheet">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body style="width:700px; height: 500px;font-family: "Noto Sans JP;">
<div style="width:700px; height: 500px" class="bg-primary d-flex align-items-center">

<div class="card mx-auto" style="width: 600px;">
<div class="card-header">
<div class="card-subtitle h6">
<a class="text-dark" href="/">
${userName}さんは
</a>
</div>
</div>
<div class="card-body">
<div class="card-title h5">
今日のお昼ご飯は
<a class="text-dark" href="/">
<h1>豚の生姜焼き定食にしたい</h1>
</a>
</div>
</div>
<div class="card-footer">
と主張しています。
</div>
</div>
</div>

</body>
</html>`,
  })) as Buffer;
  return res;
}

// 
// http://localhost:5001/fam-song/us-central1/og?user_name=%E3%83%AA%E3%82%AF