Next.jsとfirebaseで動的にOGP画像を生成する

# 概要
質問箱とかqiitaとかって、twitterに投稿すると自動的にタイトル部分が画像に反映されているのって、あたりまえのことに思っているが、実際とうやってるの？って言われると？？？ってなってしまう。

そして、実際に作ってみてわかったが、結構、力技で解決できるみたいなので、その方法を書いていく。

# 全体設計
全体的な流れでいうと

```
SVGのテキスト部分をpropsで補えるコンポーネントを用意する（これがそのままOGPの画像になる）
↓
とあるページに来たときに、FireStorageに意図したOGP画像がない場合は、画像を作る処理に入る（あったら作らない）
↓
canvasにSVGを描画する
↓
描画したあとにtoDataURLメソッドでpngデータを作成する
↓
FireStorageに名前をつけて保存する
```

それぞれはまりポイントも解説しながらそれぞれを説明する

## SVGのテキスト部分をpropsで補えるコンポーネントを用意する

```tsx
import React, { memo, useEffect } from 'react';
import firebaseUtil from '../firebase';

export default memo(({ text, path }: { text: string, path: string }) => {
  useEffect(() => {
    if (process.browser) {
      const svgData = document.getElementById('__twitter_card');
      firebaseUtil.uploadOgpFile(path, text, svgData);
    }
  }, []);
  const text1 = text.slice(0, 20);
  const text2 = text.slice(20, 40);
  return (
    <svg id="__twitter_card" style={{ display: 'none' }} xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="1200" height="630" viewBox="0 0 1200 630">
      <defs>
        <clipPath id="clip-twitter_card">
          <rect width="1200" height="630"/>
        </clipPath>
      </defs>
      <g id="twitter_card" data-name="twitter card" clipPath="url(#clip-twitter_card)">
        <rect width="1200" height="628" fill="#fff"/>
        <g id="グループ_4" data-name="グループ 4">
          <text transform="translate(600 261)" stroke="rgba(0,0,0,0)" strokeWidth="1" fontSize="52" fontFamily="HiraMinProN-W6, Hiragino Mincho ProN" letterSpacing="0.05em" textAnchor="middle" dominantBaseline="central">{text1}</text>
          <text transform="translate(600 334)" stroke="rgba(0,0,0,0)" strokeWidth="1" fontSize="52" fontFamily="HiraMinProN-W6, Hiragino Mincho ProN" letterSpacing="0.05em" textAnchor="middle" dominantBaseline="central">{text2}</text>
        </g>
      </g>
    </svg>
  );
});

```

### ハマりポイント
- Reactなので各タグのプロパティはキャメルケースに置き換えることが必要
- textは中央揃えにしないと、短いときに左寄せになってしまうので```textAnchor="middle" dominantBaseline="central"```をtextタグに入れてあげるとうまくいく
- canvasにdrawするときに、そもそも画像を画面に表示させたくないときは```style={{ display: 'none' }}```をsvgタグにつけちゃってもいい

## 画像生成処理判定と保存処理

```firebaseUtil.ts
  public uploadOgpFile = (path: string, text: string, svgData: any) => {
    const sRef = firebase.storage().ref();
    const fileRef = sRef.child(`ogp/${path}.png`);

    fileRef.getDownloadURL()
    .catch(() => {
      // catchのときは、storageに画像データが無いって判定なので、画像を作る処理をする
      const callback = (pngData) => {
        fileRef.putString(pngData, 'data_url');
      }
      svgToPng(text, svgData, callback);
    });
  }
```

### ハマりポイント
- FireStorageに画像があるかどうかチェックする方法のうちlistとlistAllは配列の中から画像があるかを検索しなければいけないので、画像が多くなったときに時間かかるなーと思ったので、今回は、直接見に行って、なかったときにcatchの処理の中で作成処理をしている

## 画像生成処理
```svgToPng.ts
const svgToPng = (svgData: any, callback: Function) => {
  const _svgData = new XMLSerializer().serializeToString(svgData);
  const canvas = document.createElement("canvas");
  canvas.width = 1200;
  canvas.height = 630;
  const ctx = canvas.getContext("2d");
  const image = new Image;
  image.src = 'data:image/svg+xml;charset=utf-8;base64,' + btoa(unescape(encodeURIComponent(_svgData)));
  image.onload = () => {
    ctx.drawImage(image, 0, 0, 1200, 630);
    const pngData = canvas.toDataURL();
    callback(pngData);
  }
}

export { svgToPng };
```

### ハマりポイント
- image.onloadの中の関数でdrawImageをしないとうまく動かなかった（実際に画面に表示して確認してみたら、onloadに入れたときだけちゃんと描画された）
- drawImageが終わったあとにtoDataURLをしないと、真っ黒な画像が生成されてしまった。順番を考えなければ。
- pngDataが出来上がったあとに```callback(pngData);```をしてここでFireStorageに画像をアップロードする処理を書く

## FireStorageのURL取得のハマりポイント
正直、なんかうまく取れなかったので、これでできるってどこかの記事に書いてあったので、そのままやったらできました

```js
const url = `https://firebasestorage.googleapis.com/v0/b/${bucket_name}/o/${encodeURIComponent(filePath)}?alt=media`;
```

# まとめ
手をつけてみて、かなり闇っぽいことをしながら頑張ってやってるんだなーって感じでした。
これできるまでに、結構時間かかりましたし。。
それでもやってみてたくさん勉強になることが多かったので、またエンジニアとしてスキルアップできたかなって思います。

また、ここまで来るのにたくさんの記事を参考にさせていただいたのですが、参考になるものが多すぎてちょっと掲載が大変になってしまい、申し訳ないです。
「svg png js」でググって出てきた記事はほぼ読ませていただきました。
ありがとうございます。