サーバーレスで動的な OGP を設定する

# サーバーレスで動的な OGP を設定する

SNS に自分の作ったページやサービスを配信する際に OGP を設定すると、リッチなコンテンツとして共有することができ、
CTR や CVR などに寄与するかもしれません。

OGP は html の meta タグとして設定しますが、要件によってはページごとに別のコンテンツとして設定したい場合が多々あります。
その場合 meta タグを動的に生成しなくてはならなくなって実装が大変です。

今回は Cloud Functions を用いて比較的簡単に動的な OGP を設定してみます。

## 構成

![スクリーンショット 2020-05-23 12.01.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119099/2d7ff7c0-525e-d547-6bd6-d577d258200b.png)

### 前提
- `/XXX` は共有したいコンテンツで、XXX の部分は可変だと仮定
- Twitter などに共有する際は `/ogp/XXX` というリンクを生成してそれをツイートする想定

### 流れ
1. `/ogp/XXX` というリンクにアクセス（ここでいうリンクとはツイートにはられたリンクのこと）
2. firebase hosting を使って `/ogp` から始まるパスはすべて Cloud Functions に受け流す
3. Cloud Functions 側で bot かユーザーを UA から判断
4. firestore や storage からデータを取得し bot やユーザーに対して適切なコンテンツを作る
5. それぞれにあったコンテンツを返す

## firebase hosting
以下のようにリダイレクトの設定を書きます。

```json:firebase.json
 ...
    "rewrites": [
      {
        "source": "/ogp/**", "function": "ogp"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  ...
```

## Cloud Functions

firebase をバックエンドに用いていたため Cloud Functions を使用しました。
`isBot` で Twitter bot かそれ以外かを判定します。
あとは firestore からもととなるデータを引っ張ってきて meta タグに設定します。

`isBot = false` の場合はアクセスされた url から `/ogp` の部分だけ取り除いた url にリダイレクトして元のコンテンツを返します。

```ts:index.ts
export const ogp = functions.region('us-central1').https.onRequest(async (req, res) => {
  const userAgent = req?.headers['user-agent']?.toLowerCase()
  const path = req.path
  const params = path.match(/^\/ogp\/contents\/(.+?)$/)
  const contentId = params?.pop()
  const doc = await db.collection('contents').doc(contentId).get()
  const name = doc.data()?.name
  const description = `${name} の説明`
  const imageUrl = doc.data()?.imageUrl

  const isBot = userAgent !== null && userAgent?.includes('twitterbot')

  if (isBot) {
    // Twitter bot の場合
    // ogp を設定した html を返す
    const html = createHtml(name, description, imageUrl, contentId)
    res.status(200).send(html)
  } else {
    // Twitter bot 以外（ユーザー）の場合
    // ogp を抜いたパスにリダイレクトしてあげる
    const url = `${DOMAIN}/contents/${contentId}`
    res.redirect(url)
  }
})

/**
 * ogp 作成用関数
 */
const createHtml = (
  title: string,
  description: string,
  imageUrl: string,
  contentId: string | undefined) => {
  return `<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>OGP Test</title>
    <meta property="og:title" content="${title}" />
    <meta property="og:image" content="${imageUrl}" />
    <meta property="og:image:width" content="1200" />
    <meta property="og:image:height" content="630" />
    <meta property="og:description"${description}" />
    <meta property="og:url" content="${DOMAIN}/ogp/contents/${contentId}" />
    <meta property="og:type" content="article" />
    <meta property="og:site_name" content="test" />
    <meta name="twitter:site" content="${DOMAIN}" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="${title}" />
    <meta name="twitter:image" content="${imageUrl}" />
    <meta name="twitter:description" content="${description}" />
  </head>
  <body>For Twitter Bot</body>
</html>
`
}
```

## テスト
開発中にツイートしてしまうとフォロワーさんに迷惑になっちゃうかもなので、 Twitter Card Validator を使ってテストします。
`/ogp` から始まる URL を貼り付けて設定したタイトルや説明、画像などが表示されたら成功です。

https://cards-dev.twitter.com/validator

## 参考
https://shuheitagawa.com/set-ogp-tags-with-cloudfunctions/