Lambda+API Gateway+CloudFrontとVueでOGP画像の自動生成をする

Lambda+API Gateway+CloudFrontとVueを使ってフロントエンドのみでOGP画像の自動生成をしてみたので備忘録。

###構成
まずVueでSVGを返すページを用意しておく。
Lambda側は`chrome-aws-lambda`でスクリーンショットを撮って、base64で返すようにする。

よくあるLambda@Edgeを使ったダイナミックレンダリングを行いつつ、Edgeで返すMetaタグの`og:image`や`twitter:image`のURLへのアクセスがあったら、用意しておいたSVGページをLambdaでスクリーンショット撮ってAPI Gateway経由でpngにして返す、というちょっと面倒くさい構成。

バックエンド側でLambdaを起動させてスクリーンショット撮ってS3に保存とかでもよかったのだけど、今回はあくまでもアクセスがあったらOGP画像を返すようにしたかったので、こんな感じの構成にした。

###VueでSVG生成
VueでSVGを生成するのはこちらの記事を参考にさせていただいた。
[Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう](https://qiita.com/serinuntius/items/3017fb6ef51cd47352f6)

```vuejs
<template>
  <div class="hello">
    <svg ref="svgCard">
      <text transform="translate(103.29 347.281)" fill="#e51f4e" font-size="29" font-family="HiraginoSans-W5, Hiragino Sans" letter-spacing="-0.002em">
        <tspan x="0" y="26">{{ data.content }}</tspan>
      </text>
    </svg>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  name: 'Svg',
  data () {
    return {
      data: {}
    }
  },
  beforeMount () {
      this.fetchData(response => {
        this.data = response
      })
  },
  methods: {
    ...mapActions([
        'fetchData',
    ])
  },
}
</script>
```

svgタグの中にvueのデータを埋め込めるので、APIから持ってきたデータを表示できるようにする。
注意が必要なのが、svgタグではテキストを自動で折り返してくれないので、途中で切って配列にして`v-for`で回すとかしないといけない。

あとはこのページをrouterに登録する。

```
import Svg from '@/views/Svg.vue'

Vue.use(VueRouter)

const routes = [
  ...
  {
    path: '/path/to/svg',
    name: 'Svg',
    component: Svg
  }
]
```

###Lambda+API Gateway
次にスクリーンショットを撮るLambdaを作る。ほんとはserverless　frameworkで作りたかったのだけど、serverlessで作ると何故かchromeが動いてくれなかったのと、API Gateway側の設定がイマイチ把握しきれなかったので、今回はコンソールでポチポチした。

#####Lambda

```javascript

const chromeLambda = require("chrome-aws-lambda");

const defaultViewport = {
  width: 1200,
  height: 630
};

exports.handler = async event => {

  const browser = await chromeLambda.puppeteer.launch({
    args: chromeLambda.args,
    executablePath: await chromeLambda.executablePath,
    defaultViewport 
  });
  
  const sleep = (time) => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve();
      }, time);
    });
  }

  const page = await browser.newPage();

  const url = "https://" + process.env.DOMAIN + "/path/to/svg";
  await page.goto(url, { waitUntil: "networkidle0" });
  sleep(1000)

  const buffer = await page.screenshot({ encoding: "base64", type: "png" });

  return {
    "statusCode": 200,
    "headers": {"Content-Type": "image/png"},
    "isBase64Encoded": true,
    "body": buffer
  };
  
};
```

Lambda側のソースコードはこんな感じ。`chrome-aws-lambda`はLambda Layerを使わせてもらった。
https://github.com/shelfio/chrome-aws-lambda-layer#available-regions

#####API Gateway
API Gateway側は適当なリソースを作って、GETメソッドを用意する。

- `/path/to/svg - GET - 統合リクエスト`で上で作ったLambdaに繋いで、`Lambda プロキシ統合の使用`にチェックを入れる
- HTTP リクエストヘッダーに`Accept`を追加する
- メソッドレスポンスのコンテンツタイプに`image/png`を追加する
- APIの設定で、バイナリメディアタイプに`image/png`を追加する

APIキーや使用量プランは必要に応じて設定して、ステージにデプロイする。ここでは仮に`prod`ステージにデプロイしたと仮定して進める。

これでcurlコマンドでAPI Gatewayを叩くと画像が返ってくるようになる。

```bash

curl -H "Accept: image/png" --output test.png https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/path/to/svg
```

#####CloudFront
この状態でブラウザでアクセスすると、Acceptヘッダーがリクエストに含まれないのでjsonが返ってきてしまう。CloudFrontを経由させることで、Acceptヘッダーを付けつつ、一度アクセスのあった画像はキャッシュしてもらえる。

まずDistributionsの作成してOriginを追加する。

```
Origin Domain Name: xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com
Origin Path: /prod
Origin Custom Headers: Accept: image/png
```

Behavior側はOrigin or Origin Groupで先ほど追加したOriginを選択する。

CloudFrontのデプロイが完了したら、CloudFrontのURL経由でブラウザで画像が表示されるようになる。

###Lambda@EdgeでOGPタグ
Lambda@Edgeを使ったOGPの生成はこのあたりを参考に。
[Lambda@EdgeでSPAのOGPを動的に設定する](https://qiita.com/kurimoto/items/6212372ead6522161a60)
[SSRをやめる。OGP対応はLambda@Edgeでダイナミックレンダリングする。](https://qiita.com/geerpm/items/78e2b85dca3cb698e98d)

今回は1個目の記事のような感じで、botからのアクセスだった場合はバックエンドのAPIにアクセスしてタイトルとかを整えつつ、上で用意した画像のURLを含んだOGPタグを生成して返すようにした。

botの種類はこのあたり。

```javascript

const crawlers = [
  "Googlebot",
  "facebookexternalhit",
  "Twitterbot",
  "bingbot",
  "msnbot",
  "Slackbot",
  "Discordbot"
];
```

###まとめ
とまあ、こんな具合でOGP用の画像を自動生成できた。あとは必要に応じてコールドスタート対策もしておきたいところ。
[Serverless Frameworkで行うLambdaのコールドスタート対策](https://qiita.com/keitakn/items/fac324cd44c4374d82e6)

いつもながら先人の知恵や知識に感謝。

###参考URL
https://scottbartell.com/2019/03/25/automating-og-images-with-aws-lambda/
https://codissimo.sinumo.tech/2019/12/27/serverless-puppeteer-with-aws-lambda-layers-and-node-js/
https://qiita.com/junara/items/5563ad7ee133ce736ed0
https://qiita.com/kodai-saito/items/9051d2b30a29c7d64f7d