【動的OGP生成】Firebase×Nuxt.jsでクリスマスカードアプリを作った

この記事は[Firebas Advent Calendar 2020](https://qiita.com/advent-calendar/2020/firebase) 23日目の記事です。

今回はこれから来るクリスマスに向けてクリスマスカードを簡単にwebで作って配布できるアプリを作ったのでそれについて書きます。（作ったとは言いつつ、他の人が利用可能なようにデプロイできませんでした.....）
メインはOGPになるので、**Nuxt.js×firebaseでの動的OGP生成**について書きます。

完成物はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/22daba10-00a6-2e61-a79c-ebb1ab9f5efe.png)


# アプリの雛形を作る

さて、まずはアプリの雛形を作って、必要なものを諸々インストールしておきます。


```
//Nuxt.jsアプリの雛形作成。cssなどはお好みで
$ npx create-nuxt-app <app name>
//firebaseをインストール
$ npm install --save firebase
//Firebase Functions環境設定
$ npm install -g firebase-tools
```

これで、firebaseにコマンドラインからログインできるようになりました。

```
$ firebase login
```

ここまでできれば諸々の設定は終わり。

あとは好みですが、私は```.env```に環境変数を格納して使いたいので以下もインストール

```
$ npm install @nuxtjs/dotenv
```

nuxt.config.jsに忘れないうちに以下追記

```javascript:nuxt.config.js

modules: ["@nuxtjs/dotenv"]
```

これでprocess.env.XXX_KEYの形で環境変数を呼び出せるようになります。

# svgファイルを動的に変更してjpgに変換する

適当にイラストレーターなどで好きなデザインでsvgファイルを作ります。

仕組みとしてはsvgファイルの中に文字を表示する部分を作っておいて、データをバインドさせます。


```vuejs:index.vue
<template>
  <div>
    <svg>
      <text>
        {{ text }}
      </text>
    </svg>
  </div>
  <div>
    <input
      v-model="text"
      type="text"
    />
    <button @click="create">つくる</button>
  </div>
</template>

```

このように``v-model="text"``をinputタグに付与して、inputに入力した値と{{ text }}で表示させる値をバインドさせます。

そしてcreateメソッドでfirebaseのstorageに画像をポストしていくんですが、その際にsvgをjpg（またはpng）に変換する必要があります。

その部分に関して[こちらの記事](https://blog.asial.co.jp/1615)を参考にしました。

```javascript

// svgをjpegに変換
const svg2imageData = (svgElement, successCallback, errorCallback) => {
  const canvas = document.createElement("canvas");
  canvas.width = 1200;
  canvas.height = 630;
  const ctx = canvas.getContext("2d");
  const image = new Image();
  image.onload = () => {
    ctx.drawImage(image, 0, 0, 1200, 630);
    successCallback(canvas.toDataURL());
  };
  image.onerror = e => {
    errorCallback(e);
  };
  const svgData = new XMLSerializer().serializeToString(svgElement);
  image.src = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(svgData)));
};
```

これをスクリプト内に書くことで、``svg2imageData``に引数で値を渡すだけでjpgファイルが出力されるようになります。

# firebaseのルールの設定

さて、firebaseにデータをpostするにあたって諸々ルールを書いていきます。


※以下のルールのまま本番にデプロイはしないでください！
現状は練習用なのでこのまますすめます。
そのうち認証に関しても書きます。

```
//Storageのルール
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write;
    }
  }
}

//Cloud Firestoreのルール
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}
```

必要に応じて権限などでルールを縛ってください。

# 必要なfirebase情報の記載と初期化
firebaseでプロジェクト内部に入って、設定→全般から確認するとapiKeyなど諸々確認することができますが、Storageへのファイルのアップのためには以下だけで大丈夫です。

```
    apiKey: "hogehoge"
    authDomain: "hoge.firebaseapp.com"
    storageBucket: "hoge.appspot.com"
```

私は上記に加えて、生成した画像をurlに変換したものをfirestoreにアップしておきたかったので、以下も合わせて書きました。

```
    apiKey: "hogehoge",
    authDomain: "hoge.firebaseapp.com",
    projectId: "hoge",
    storageBucket: "hoge.appspot.com",
    messagingSenderId: "11111111",
```

これらをconfigの中においてfirebaseを初期化します。

```vuejs:index.vue

<script>
import firebase from "firebase";
var config = {
  apiKey: process.env.API_KRY,
  authDomain: process.env.AUTH_DOMAIN,
  projectId: process.env.PROJECT_ID,
  storageBucket: process.env.STRAGE_BUCKET,
  messagingSenderId: process.env.MESSAGING_SENDER_ID
};
let db;
if (!firebase.apps.length) {
  const firebaseApp = firebase.initializeApp(config);
  db = firebaseApp.firestore();
}

//省略
</script>
```

db = firebaseApp.firestore(); に関しては関数内で書いたほうが良い気がします。
グローバル関数で書くのも気持ち悪いので...

# Storageにpostする処理を書く

それではついにpost部分の処理を書きます。

```vuejs:index.vue
<script>
export default {
  data() {
    return {
      text: "クリスマスカード",
      description: "大切な人にメッセージカードを贈ろう",
      url: ""
    };
  },
  methods: {
    async create() {
      svg2imageData(this.$refs.svgArea, async data => {
        const storageRef = firebase.storage().ref();
        const imagesRef = storageRef.child(`${this.text}.jpg`);
        await imagesRef.putString(data, "data_url");
        let url = this.url;
        url = await imagesRef.getDownloadURL();
        const card = db.collection("cards").doc(this.text);
        await card.set({
          url,
          message: this.text
        });
      });
    }
  }
};
</script>
```

詳細に関しては[公式](https://firebase.google.com/docs/storage/web/create-reference?hl=ja)の通りなのですが、``const storageRef = firebase.storage().ref();``の部分でこれから操作を行う場所を参照しています。
そしてその中で``const imagesRef = storageRef.child(`${this.text}.jpg`);``と書くことで、参照したfirebaseのstorageに子要素として``${this.text}.jpg``という名前で画像を保存することができます。便利！

例えば今回``ほげ``と入力してボタンをクリックした場合、``ほげ.jpg``という名前でstorageに保存されることになります。

本当はこれはidなどをパラメータで付与したほうが良いと思いますが、あいにくあまり良くわかりませんでした...わかる方コメントなどいただけると嬉しいです。

ともかく、これでstorageに保存ができるようになりました。

私はstorageへの保存に加えて画像URLを生成して（``url = await imagesRef.getDownloadURL();``）firestoreにpostしていますが、これはなくても動きます。

個人的にこれから設定を行うOGPのために使うimage-urlがfirestoreで保存していたほうがgetしやすいかと思って書きましたが、その点はもしかしたらもう少しいい方法があるかもしれません。

## OGPのためにメタタグを動的に変更する

それでは画像の生成とstorageに保存までできたので、次はメタタグの設定をしていきます。
functions/index.jsにメタタグ情報を記載します。

```javascript:index.js

const functions = require("firebase-functions");

exports.christmas = functions.https.onRequest((req, res) => {
  if (req.params[0] !== undefined) {
    const [, , param] = req.path.split("/");
    const filename = param + ".jpg";
    const storage = new Storage({});
    const bucketName = process.env.STRAGE_BUCKET;
    const path = encodeURIComponent(filename);
    //上記の情報から画像URLを組み立てて以下のIMAGEのところに入れる
    const IMAGE = "";
    const TITLE = "クリスマスカード";
    const DESCRIPTION = "大切な人にクリスマスカードを贈ろう";
    res.set("Cache-Control", "public, max-age=600, s-maxage=600");

    res.status(200).send(`<!doctype html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <title>Christmas</title>
        <meta property="og:title" content="${TITLE}">
        <meta property="og:image" content="${IMAGE}">
        <meta property="og:description" content="${DESCRIPTION}">
        <meta property="og:url" content="${SITEURL}">
        <meta property="og:type" content="website">
        <meta property="og:site_name" content="${TITLE}">
        <meta name="twitter:site" content="クリスマスカード">
        <meta name="twitter:card" content="summary_large_image">
        <meta name="twitter:title" content="${TITLE}">
        <meta name="twitter:image" content="${IMAGE}">
        <meta name="twitter:description" content="${DESCRIPTION}">
      </head>
      <body>
        <script>location.href = "new-christmas-card.firebaseapp.com"; </script>
      </body>
    </html>`);
  }
});


```


firebase.jsonの``rewrite``の部分を変更します。

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

これで/christmas/~にアクセスがあった場合には``index.js`` 内の``christmas``を参照して値を動的に変更できます。

ここまでできればあとはデプロイすると動的OGPが確認できるはずです。

# デプロイ前の設定


まずはプロジェクトディレクトリを初期化します。

```
$ npm run generate
```
これでアプリの静的バージョンがdist内にビルドされます。
この作業をすることで、先程firebase.jsonに記載のあったindex.htmlが自動で生成されます。


```
$ firebase init
```

色々質問されるので、使うものを選択します。
今回はFunctionsとHostingを使うので以下。
![スクリーンショット 2020-12-21 23.44.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670446/b8836e49-d305-fae4-a186-28f30d4d1d5b.png)

```
? Please select an option: Use an existing project
//projectを作ってない場合は[don't setup a default project]でOKです。
? Select a default Firebase project for this directory: <app-name> (app-name)
i  Using project <app-name> (app-name)

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? Yes
✔  Wrote functions/package.json
✔  Wrote functions/.eslintrc.json
✔  Wrote functions/index.js
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes
```

以上の設定は諸々好みで大丈夫です。

```
? What do you want to use as your public directory? dist
//publicがデフォルトですが、今回は先程npm run generateで生成したdistを参照します。
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
? Set up automatic builds and deploys with GitHub? No
? File dist/index.html already exists. Overwrite? No
```

あとはデプロイするだけ！！

```
$ firebase deploy
```

今回はじめて知ったんですが、firebase deployって課金ユーザーのみが使えるんですね。
sparkプランだったのでビクビクしながらblazeプランに変更しました。

デプロイできたら、アプリを起動して文字を入力してボタンをクリックします。
そして/christmas/${入力した文字名}のURLで[Card Validator](https://cards-dev.twitter.com/validator)などで確認してみると、OGPが確認できるはずです！

# その他やったほうが良いこと

+ 完了画面へのルーティング設定
+ それぞれの投稿にidをつける（さすがにtextそのままは...）

現在、ボタンを押しても遷移しません。そのため、ユーザーがOGPを作成できたのかどうかわかりにくい状態です。なので完了画面などを作って、クリックされたらその画面まで擬似的に飛ばすという処理が必要かとおもいます。
そして今回途中でも書きましたが、本来idを付与しないといけないところをtextベタ打ちのURLになっています。
問題なのは、同じ投稿があった際に、過去のデータが上書きされてしまう（同じ入力なら問題ないのかもしれませんが気持ち悪いですね）のと、URLが長々しくなってかっこ悪いです。
なのでこのへんも修正したいと思いつつ現在対応を探している状態です。

完全な状態での作ってみた記事でなくて恐縮ですが、少しでも参考になれば幸いです。

firebaseは学習の敷居は低いですが、なかなか詳しいところまで理解しようとすると難しいですね。
もっと使ってなれていきたいです。

# 参考

以下を参考にしました。

[SNS映えするWebアプリを...！FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だった](https://qiita.com/yuneco/items/5e526464939082862f5d)
[「Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう」を実際に作ってみる](https://qiita.com/kanye__east/items/6cbcdc8fe5103084c737)
[Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう](https://qiita.com/serinuntius/items/3017fb6ef51cd47352f6)