Vue.js + Firebase + OGP画像生成 本当に爆速で作れた！

### 自己紹介
プログラマーのAkasaと申します。
夢を実現するために頑張ってる人を応援するサービスを模索しています！
今回、Vue.js, Firebase (Hosting, Functions, Storage)でOGP画像生成形サービスを爆速で作りました！
（サービスは現在停止しています）

### 作ったサービス
 - [Wishlist Share](https://wishlist-share.com)
   - Amazonのほしい物リストを画像付きでツイートできるサービス。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167460/3b3c7d66-8e75-7a99-072e-87831445184c.png" width="500">

### 開発環境
- firebase : ^7.6.0
- vue": ^2.6.6

### 参考にした記事
 - [Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう](https://qiita.com/serinuntius/items/3017fb6ef51cd47352f6)

 - [Nuxt.js + FirebaseでOGPの仕組みを完全に理解した　〜俳句をSVGで描画するサービスをリリースした話〜](https://qiita.com/mitsudaman/items/1956b94dc8faf8fb8c59)
 - [SNS映えするWebアプリを...！FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だった](https://qiita.com/yuneco/items/5e526464939082862f5d)


### ページ
[リンクを踏む側サンプル](https://wishlist-share.com/invited/amazon/1/O5Z11NOGY2KH)

|リンクを作る側①|リンクを作る側②|リンクを踏む側|
|:---:|:---:|:---:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167460/ac9e6191-fe98-7054-688f-4d3443ad6dd2.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167460/cc569d59-2827-fda7-e5bb-6aac6ea5a929.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167460/62ba65e6-1fd9-94f3-f563-9dc6e16fc1cc.png)|


### Functions設定

- parseAmazon
  - ユーザーに入力してもらったほしい物リストのリンクから、該当のページをパースし商品画像などのデータを取得する。(省略)
 
- buildHtml
  - SPAで動的にOGPを設置する為にはテクニックが必要で生のHTMLを返すFunctionsを用意する。
  - HeaderにTwitterBot用のOGPを設置する。
  - BodyにはTwitterでリンクをタップした人に見せたいページのhrefを設置し、そこに遷移させる。


### アーキテクチャ 

 - FirebaseCloudStorageの構成に合わせてツイートに載せるリンクが変わる。
 - 今回のディレクトリ構成は/amazon/TYPE/ID.pngなので
URLにはTYPEとIDのみ入れ込み、残りはFirebaseCloudStorageのメタデータに入れておく。

<img width="1102" alt="Diagram.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167460/9d8b8dfb-047e-3263-bde4-f31dcd6024d3.png">


ex)

 - URL
  - TYPE : 1 (まだ背景画像を１つしか用意してないので1のみ)
  - ID : O5Z11NOGY2KH (リストのID)

 - METADATA
  - AVATAR_URL : `https://images-fe.ssl-images-amazon.com/images~~~~`
  - LIST_ NAME : "今年中に読みたい技術書"
  - LIST_DESCRIPTION : "Wishlist Share開発者Akasaのほしい物リストです！継続は力なり！読むぞー！"
  -  DOMAIN : jp (amazonのドメイン、他にもco.ukとかがある)

### コード

```javascript:build-html.js
const functions = require('firebase-functions');
const util = require('util');
const SITEURL = 'https://wishlist-share.com';
const OGP_W = 1200;
const OGP_H = 630;
const SITE_NAME = 'Wishlist Share';
const { admin } = require('./firebase/firebase-admin');
const { generateRandomStrings } = require('./utils/randomStrings');

const func = functions.https.onRequest(async (req, res) => {
  // ex) req.path = /amazon/1/2H6Q88D0SBG0E
  const [,, type, id] = req.path.split('/');

  const pageUrl = SITEURL + req.url;
  let html;
  let listName;
  let desc;
  let link;

  // ex) https://wishlist-share.com/invited/amazon/1/2H6Q88D0SBG0E
  link = `https://wishlist-share.com/invited/amazon/${type}/${id}`;

  const path = `amazon/${type}/${id}.png`; // ex) /amazon/1/39QHNEX56SK42.png

  const bucket = admin.storage().bucket('amaz0n-wish-list.appspot.com');
  const file = bucket.file(path);
  const exists = await file.exists();

  if (!exists[0]) {
    res.status(404).end();
    return;
  } else {
    const getMetadataAsync = util.promisify(file.getMetadata).bind(file);
    const metadata = await getMetadataAsync().catch(e => {
      console.log(e);
      throw e;
    });

    listName = metadata.metadata.listName;
    desc = metadata.metadata.desc;
  }
  const randomStrings = generateRandomStrings(5); // For Web crawlers fetching every time.
  const imageUrl = "https://firebasestorage.googleapis.com/~~~~";
  html = createHtml(
    /* title    */ listName,
    /* desc     */ desc,
    /* imageUrl */ imageUrl,
    /* link     */ link,
    /* pageUrl  */ pageUrl
  );

  res.set('Cache-Control', 'public, max-age=3600, s-maxage=3600'); // 1 hour
  res.status(200).send(html);
  return;
});


const createHtml = (title, desc, imageUrl, link, pageUrl) => {
  return `<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Wishlist Share</title>
    <meta property="og:title" content="${title}" />
    <meta property="og:image" content="${imageUrl}" />
    <meta property="og:image:width" content="${OGP_W}" />
    <meta property="og:image:height" content="${OGP_H}" />
    <meta property="og:description" content="${desc}" />
    <meta property="og:url" content="${pageUrl}" />
    <meta property="og:type" content="article" />
    <meta property="og:site_name" content="${SITE_NAME}" />
    <meta name="twitter:site" content="${SITEURL}" />
    <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="${desc}" />
  </head>
  <body>
   <script>
    location.href = '${link}';
    </script>
  </body>
</html>
`;
};

module.exports = func;
```

### 躓きやすいポイント
firebase.jsonとrouter.jsを載せておきます。
設計に応じて書き換えてください。

```json:firebase.json
{
  "hosting": {
    "public": "dist",
    "rewrites": [{
        "source": "/parseAmazon",
        "function": "parseAmazon"
      },
      {
        "source": "/*/*/*",
        "function": "buildHtml"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
}

```

```javascript:router.js
import Vue from 'vue';
import Router from 'vue-router';
import Main from './views/Main.vue';
import Invited from './views/Invited.vue';

Vue.use(Router);

export default new Router({
  mode: 'history',
  linkExactActiveClass: 'active',
  routes: [
    {
      // リンクを作る側
      path: '/',
      name: 'main',
      component: Main
    },
    {
      // リンクを踏む側
      path: '/invited/amazon/:type/:id',
      name: 'invited',
      component: Invited
    },
  ]
});
```


### 便利なサービス
[Card validator](https://cards-dev.twitter.com/validator)
[ngrokが便利すぎる](https://qiita.com/mininobu/items/b45dbc70faedf30f484e)