開発しているWebサービスでかかった費用を見ていると、
シェア用のOGP画像の保存量が結構占めていた。。(882円中の492円分で55%)
ランニングコストを減らしたいなと思っていたら、節約術があったので試してみた。
・NowのエッジキャッシュでCloud Storage節約サーバー作成 - Crieit
若干ハマったので、その備忘録。
ハマったポイント
いくつかハマったので、
- VercelではImageMagickが使えない。。
- node-canvasを使うように変更
- 背景画像など読み込むファイルはvercel.jsonに設定が必要
- Serverless Function内でファイルを書き出すときは、/tmp配下
- カレントディレクトリは読み込み専用のため、エラーになる
- 値が変わる画像の場合は、パスパラメタなどでURLを変える
- 総額や冊数などを表示しているが、キャッシュのため切り替わらなくなるため。
OPG画像を生成するソースコード
Vercel+node-canvasは以下の記事を参考にした。
・ 【個人開発】フローチャートで診断を作れるWebサービスをリリースしました【全コード公開】 - Qiita
利用するパッケージのインストール
$ npm i canvas sharp axios # typescriptの場合、以下も追加 $ npm i -D @types/sharp @vercel/node ts-node typescript
ディレクトリ構成
ディレクトリ構成はこんな感じ。
. ├── api │ └── index.ts ├── assets │ ├── fonts │ │ ├── NotoSansJP-Bold.otf │ │ └── NotoSansJP-Medium.otf │ └── image │ └── ogp.png ├── package.json ├── package-lock.json ├── tsconfig.json └── vercel.json
API部分のソース
// api/index.ts import { NowRequest, NowResponse } from "@vercel/node"; import axios, { AxiosResponse } from "axios"; import { Canvas, CanvasRenderingContext2D, createCanvas, loadImage, registerFont } from "canvas"; import sharp from "sharp"; const CANVAS_WIDTH = 1200; const CANVAS_HEIGHT = 630; const TITLE_COLOR = "#FFFFFF"; export default async (request: NowRequest, response: NowResponse) => { // *** Canvas用にフォントファイルの読み込み registerFont("./assets/fonts/NotoSansJP-Bold.otf", { family: "NotoSansJP_Bold" }); registerFont("./assets/fonts/NotoSansJP-Medium.otf", { family: "NotoSansJP_Regular" }); // *** Canvasの作成 const canvas: Canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT); const context: CanvasRenderingContext2D = canvas.getContext("2d"); // *** 背景画像の描画 const backgroundImage = await loadImage("./assets/image/ogp.png"); context.drawImage(backgroundImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); // *** 書影の書き出し: URLから画像を取得して、リサイズ後に、canvasに描画 const bookImageURL = "http://..."; const bookWidth = 520; const bookHeight = 454; // * 外部URLから画像データの取得 const res: AxiosResponse<Buffer> = await axios.get<Buffer>(bookImageURL, { responseType: "arraybuffer" }); // * sharpを使って、リサイズ const bookImageBuffer: Buffer = await sharp(res.data).resize(bookWidth, bookHeight, { position: "top" }).toBuffer(); // * リサイズ後のデータをcanvasに描画 const bookImage = await loadImage(bookImageBuffer); context.drawImage(bookImage, (CANVAS_WIDTH - bookWidth) / 2, (CANVAS_HEIGHT - bookHeight) / 2, bookWidth, bookHeight); // *** 文字の背景部分の四角い図形の描画 const rectHeight = 50; context.fillStyle = "rgba(0, 0, 0, 0.6)"; context.fillRect(0, CANVAS_HEIGHT - rectHeight, CANVAS_WIDTH, rectHeight); // *** 文字の書き出し const title = "タイトル"; context.font = `60px "NotoSansJP_Bold"`; context.fillStyle = TITLE_COLOR; context.textAlign = "center"; context.fillText(title, CANVAS_WIDTH / 2, 500); // ** canvasをBufferに変換 const buf: Buffer = canvas.toBuffer(); // ** responseの設定: キャッシュなど const contentLength = buf.length; const cacheAge = 7 * 24 * 60; // 1週間 response.setHeader("Content-Type", "image/png"); response.setHeader("Content-Length", contentLength); response.setHeader("Cache-Control", `public, max-age=${cacheAge}`); response.setHeader("Expires", new Date(Date.now() + cacheAge).toUTCString()); response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); response.status(200).end(buf); };
vercel.json
背景画像やフォントなど静的なファイルを読み込めるようにするため、vercel.jsonの設定が必要。
・Including Additional Files - Vercel
以下のような感じで、"includeFiles": "assets/**"
を記載し、
該当のfunctionが利用することを伝える必要がある。
// vercel.json { "version": 2, "routes": [{ "src": "/.*", "dest": "/api/index" }], "functions": { "api/index.ts": { "includeFiles": "assets/**" } } }
この設定がないと、function内からファイルを参照できない。
また、以下のような感じで__dirname
を使ってパスを指定すると、自動でincludeしてくれる。
// index.js const { readFileSync } = require('fs'); const { join } = require('path'); const file = readFileSync(join(__dirname, 'config', 'ci.yml'), 'utf8');
package.json
VercelのServerless Functionでnode-canvasを使うために少し設定がいる。
カスタムビルド(vercel-build
)を設定して、不足している.soファイルをコピーする必要がある。
// package.json { // ... 略 "scripts": { "vercel-build": "yum install libuuid-devel libmount-devel && cp /lib64/{libuuid,libmount,libblkid}.so.1 node_modules/canvas/build/Release/" }, }
・参考: Vercel Now(旧ZEIT Now)上でnode-canvasを動かす - Blanktar
これでVercel上でOGPが生成できるように!
#積読ハウマッチ アップデート🎉
— めもらば@公式📚積読ハウマッチ (@MemoryLoverz) October 9, 2020
ついにランキングのシェア画像ができました🎉
1位〜3位までが表示されるように😊
また、プロフィールなどのシェア画像にも冊数を表示✨
ぜひぜひお試しください😍
人気の購入本の月間ランキングhttps://t.co/iVEUffoIjp
細かい設定などは、node-canvasのドキュメントを見ながら調整すればOK!
これでユーザ生成画像以外は保存しなくても良くなったので、ストレージ容量を節約できた(´ω`)
VercelではImageMagickが使えないかも?
最初はImageMagickを使って生成することを考えていた。
「Build Step - Vercel Documentation」にもImageMagick-develが入っていると書かれているし、
「How do I install something else on the build image?」に書かれているように、yum install ImageMagick
でインストールを試みたけど、convert
コマンドが見つからない感じに。。
次に、amazon-linux-extras
でGraphicsMagickをインストールしてみたけど、同じくgm
コマンドが見つからないよう。。
結果、以下の記事を参考にしつつ、node-canvasに落ち着いた。
・ 【個人開発】フローチャートで診断を作れるWebサービスをリリースしました【全コード公開】 - Qiita
Canvasを使うChart.jsのグラフも画像化できそう?
色々見ていたら、以下のパッケージがあった。
・ SeanSobey/ChartjsNodeCanvas: A node renderer for Chart.js using canvas.
もしかしたら、Chart.jsのグラフもOGP画像にできそう?今度試してみる。