開発中のSNSのOGP画像自動生成をCloud Functionsで実装する

![ogp_1130210.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244632/e498ab23-fe79-ce4e-1724-fb5f2e0bcaa7.png)

前から「[Snovel](https://snovel.app)」という駅ノートでつながるSNSを作ってます。
あわせて[LP(https://snovel.app)](https://snovel.app)も作りました。
アプリから駅にチェックインしたことがシェアされたときに、共通の画像だとちょっとさみしいので「〜駅でチェックインしよう」とチェックインされた駅に合わせてLPのOGP画像を自動生成されるような仕組みを作ってみました。
実際これを使うにはSSR等の技術が必要になるかと思いますが、SSRについては他の記事を読んでください。

## 対象読者
- フロントエンド中級者
- Firebase、Cloud Functionsが何か知ってる人

## 使うもの
- Cloud Functions for Firebase
- 生成する画像の元画像(例えば下のようなもの)
![ogpbase.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244632/10c24ca8-cfaa-4933-2121-693b8a637690.png)
- （必要に応じて）Firebaseの課金プラン
- 画像に書き込むフォント(TTF,OTF)

## Functionsでエンドポイントを作る
アプリのチェックイン時やLP表示時に叩くために、OGP生成関数のエンドポイントを作ります。

```js:index.js
const functions = require("firebase-functions");

// 他の関数もあるので、OGPの関数が呼び出されたときだけ読み込むようにする
if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === 'ogp') {
    exports.ogp = functions
    .region('asia-northeast1') // 特に使わない理由がないので東京リージョン
    .https
    .onRequest(require('./ogp')); // onCallとかを使うとCORS対策にもなるらしけどやったことがない
}
```
```js:ogp.js
const admin = require("firebase-admin");
const spawn = require("child-process-promise").spawn;
const path = require("path");
const os = require("os");
const admin = require("firebase-admin");

admin.initializeApp();

// ご自身で用意してください
const FONT_NOTO = "fonts/NotoSansCJKjp-Bold.otf";

const ogp = async (req, res) => {
    // CORS対策
    res.set('Access-Control-Allow-Origin', '*');
    if (req.method === 'OPTIONS') {
        res.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
        res.set('Access-Control-Allow-Headers', 'Content-Type');
        res.set('Access-Control-Max-Age', '3600');
        res.status(204).send('');
        return
    }

    // 必要な変数を定義する
    const bucket = admin.storage().bucket();
    const STORAGE_ROOT = "https://firebasestorage.googleapis.com/v0/b";
    const bucketName = bucket.name;
    const uploadPath = 'ogp/PUT_ID_HERE.png';
    const dlPath = encodeURIComponent(uploadPath);
    const dlURL = `${STORAGE_ROOT}/${bucketName}/o/${dlPath}?alt=media`;

    // すでに生成されたファイルがある場合はそのままそれを返す
    const uploadedFile = bucket.file(uploadPath);
    const alreadyExists = await uploadedFile.exists();
    if (alreadyExists[0]) {
        res.send(dlURL);
        return;
    }

    // 実際画像に出てくる文言。実際はAPIから取ってきたのを入れたりとか。
    const msg = '高田馬場駅にチェックインしよう';
    // ベースの画像
    const template = 'img/ogpbase.png';
    // 生成した画像のパス。tmpディレクトリに配置する
    const outFile = path.join(os.tmpdir(), 'generated.png');

    // ImageMagicで画像生成
    await spawn("convert", [
        "-font", FONT_NOTO, // フォントの指定。カスタムフォントの場合はパスを指定
        "-pointsize", "48", // フォントサイズの指定
        "-fill", "white", // 文字色の指定。白文字に設定
        "-gravity", "Center", // 位置の基準を指定。
        "-annotate", "+0-16", msg, // 文字の指定。+0+0は位置の基準からの相対位置。
        template, // 入力画像のパス
        outFile // 出力画像のパス
    ]);

    // Cloud Storageへのアップロード
    await bucket.upload(outFile, {
        destination: uploadPath
    });

    // return Download URL
    res.status(201).send(dlURL);
}
module.exports = ogp;
```

割と簡単でしたね。

実際のところは、[https://snovel.app/station/1130210](https://snovel.app/station/1130210)のようなURLにアクセスされたり、アプリでチェックインされたときに上記のAPIが叩かれて、シェアするときに一緒にOGPとしてシェアされる感じです。

## 注意点
割と重い処理なので、関数のメモリ割り当てを上げたり、1200x630を超えるような画像を扱わないようにしてください。

## リポジトリ
[SnovelLP](https://github.com/TinyKitten/SnovelLP)