爆速ではなかったけど、”Vue.jsとFirebaseでOGP画像生成系のサービス"を作ってみた。

#はじめに
[Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう - Qiita](https://qiita.com/serinuntius/items/3017fb6ef51cd47352f6)
[「Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう」を実際に作ってみる - Qiita](https://qiita.com/taishikato/items/6cbcdc8fe5103084c737#_reference-db09ddce118b061753dd)
[Nuxt.js + FirebaseでOGPの仕組みを完全に理解した　〜俳句をSVGで描画するサービスをリリースした話〜](https://qiita.com/mitsudaman/items/1956b94dc8faf8fb8c59#_reference-908543055456b4de457e)

これらの記事を見て、実際に、FirebaseとVue.jsを使って、OGP画像生成系のサービスを作ってみました。
#作ったサービス
WhatISaw
[https://whatisaw.firebaseapp.com/](https://whatisaw.firebaseapp.com/)
![スクリーンショット 2019-09-14 13.15.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/367226/3d9dd92b-d6e3-ef97-2261-368fc417817c.png)


ふと見て気になったヒトとかモノとかコトを投稿できるサイトです。
Twitterで共有するとこんな感じに表示されます。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">新宿で... <a href="https://t.co/0qQURy4JT7">https://t.co/0qQURy4JT7</a> <a href="https://twitter.com/hashtag/whatisaw?src=hash&amp;ref_src=twsrc%5Etfw">#whatisaw</a> <a href="https://twitter.com/hashtag/%E8%A6%8B%E3%81%BE%E3%81%97%E3%81%9F?src=hash&amp;ref_src=twsrc%5Etfw">#見ました</a></p>&mdash; 締切は10年後 (@dl10yr) <a href="https://twitter.com/dl10yr/status/1171439480617652224?ref_src=twsrc%5Etfw">2019年9月10日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#Who I am
* 今年から本格的にプログラミングをやり始めた初心者
* WEBサイト・サービス作ってみたい
* 最初は、LaravelとVueを勉強していた。
* Laravelむずい
* Firebaseすごくね？これならやりきれそう。しかも無料。
* 作ってみる！

#環境構築
基本的にはこの記事の通りにやりました。
[Vue.js+Firebase+Dockerで環境構築をして、初期画面をデプロイするまで](https://qiita.com/azul915/items/975082747e971025ca40)
ただし、Cloud Functionを使うので、3つ変更した点があります。
###1, DockerFileの変更
上の記事では、nodeのバージョンが"8.11.3-alpine"なのですが、Cloud Functionがdeployできませんでした。

```
[info] >  (node:1220) UnhandledPromiseRejectionWarning: Error: @grpc/grpc-js only works on Node ^8.13.0 || >=10.10.0
```
"Nodeのバージョンが古すぎる"みたいなエラーを吐くので、8.13.0より新しい"8.16.1-alpine"を使いました。

```DockerFile:DockerFile
FROM node:8.16.1-alpine
  
WORKDIR /app

RUN apk update && \
    npm install -g npm \
    npm install -g @vue/cli \
    npm install -g firebase-tools \
    && rm -rf /var/lib/apt/lists/*

CMD ["/bin/ash"]
```
###2, docker-compose.ymlの変更
デプロイする前にローカル環境でテストするために```firebase serve```で、cloud functionsの動作を確認しようとしたら、エラーを吐きました。

```
 (node:131) UnhandledPromiseRejectionWarning: Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
```
よく分からないけど、認証が必要らしい。
下の記事を参考に、docker-compose.ymlを変更し、jsonファイルをプロジェクトフォルダの直下（app/project_name/）に置きました。
あと、5000番のポートも開けておきます。
[[Firebase] Cloud Functionsで消耗したくない人のために、開発環境のベストプラクティスをまとめていったらDockerに行き着いた話（随時更新）](https://qiita.com/pannpers/items/244a7e3c18d8c8422e4f)

```docker:docker-compose.yml
version: '3'
services:
    vue:
        build: .
        volumes:
            - ./app:/app
        ports:
            - '8080:8080'
            - '5000:5000'
        stdin_open: true
        tty: true
        environment:
          - GOOGLE_APPLICATION_CREDENTIALS=firebase-adminsdk.json

```
###3,firebase-initの設定
[さっきの記事](https://qiita.com/azul915/items/975082747e971025ca40)では、Cloud Functionsは使っていないので、流れが少し違います。
```firebase init```で、設定を色々聞かれますが、ここで

```
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. 
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◉ Functions: Configure and deploy Cloud Functions
 ◉ Hosting: Configure and deploy Firebase Hosting sites
❯◯ Storage: Deploy Cloud Storage security rules
```
Hostingに加えて、Functionsも選択します。

#画像生成→Cloud Fire storageにアップロード
```vuejs:Create.vue
<template>
  <div class="view-wrapper-content">
    <svg ref="svgCard" viewBox="0 0 200 100" class="svgarea">
      <rect x="0" y="0" width="200" height="100" fill="#fff" stroke="#000000" stroke-width="10" />
      <text font-family="sans-serif" font-weight="bold" dominant-baseline="middle">
        <tspan font-size="10px" x="15" y="15">{{text1}}</tspan>
        <tspan font-size="8px" x="15" y="28">で、</tspan>
        <tspan font-size="10px" x="15" y="43">{{text3}}</tspan>
        <tspan font-size="10px" x="15" y="58">{{text4}}</tspan>
        <tspan font-size="10px" x="15" y="73">{{text5}}</tspan>
        <tspan font-size="8px" x="15" y="88">を見た。</tspan>
      </text>
      <image
        xmlns="http://www.w3.org/2000/svg"
        id="image0"
        width="50"
        height="50"
        x="155"
        y="45"
        href="data:image/png;base64,iVBORw0KGgoAAA....."
      />
    </svg>
    <form action @submit.prevent="create" class="form-horizonal form-inline">
      <label style=" margin:10px 10px 30px 10px">投稿者:</label>
      <input
        v-model="user_name"
        type="text"
        style="width: 60%; margin:10px 10px 30px 10px"
        class="form-control"
        maxlength="15"
        minlength="1"
        required
      />
      <input
        v-model="text1"
        type="text"
        style="width:100%; margin-bottom:10px"
        placeholder="どこ？（17字以内）"
        class="form-control"
        maxlength="17"
        minlength="1"
        required
      />
      <h6 style="text-align:left;">で、</h6>
      <textarea
        v-model="text2"
        type="text"
        style="width:100%; margin-bottom:10px"
        placeholder="何？（100字以内）"
        class="form-control"
        maxlength="100"
        minlength="1"
        required
        rows="4"
      />
      <h6 style="text-align:left;">を</h6>
      <div class="submit-button">
        <button class="btn btn-dark" type="submit">見た。</button>
      </div>
    </form>
  </div>
</template>

<script>
import firebase from "firebase";
const db = firebase.firestore();

// svg->png
const svg2imageData = (svgElement, successCallback, errorCallback) => {
  const canvas = document.createElement("canvas");
  canvas.width = 1200;
  canvas.height = 630;
  const context = canvas.getContext("2d");
  const image = new Image();
  const svgData = new XMLSerializer().serializeToString(svgElement);
  image.src =
    "data:image/svg+xml;charset=utf-8;base64," +
    btoa(unescape(encodeURIComponent(svgData)));
  image.onload = () => {
    context.drawImage(image, 0, 0, 1200, 630);
    successCallback(canvas.toDataURL());
  };
  image.onerror = e => {
    errorCallback(e);
  };
};
function getUniqueStr(myStrong) {
  var strong = 1000;
  if (myStrong) strong = myStrong;
  return (
    new Date().getTime().toString(16) +
    Math.floor(strong * Math.random()).toString(16)
  );
}
firebase
  .auth()
  .signInAnonymously()
  .then(() => {
  });
export default {
  components: {},
  data() {
    return {
      text1: "",
      text2: "",
      user_name: "名無しさん"
    };
  },
  computed: {
    text3: function() {
      return this.text2.substring(0, 15);
    },
    text4: function() {
      if (this.text2.length <= 15) {
        return "";
      }
      if (this.text2.length >= 16) {
        return this.text2.substring(15, 30);
      }
    },
    text5: function() {
      if (this.text2.length <= 30) {
        return "";
      }
      if (this.text2.length >= 31 && this.text2.length <= 43) {
        return this.text2.substring(30, 43);
      }
      if (this.text2.length >= 44) {
        return this.text2.substring(30, 43) + "...";
      }
    }
  },
  methods: {
    async create() {
     
      svg2imageData(this.$refs.svgCard, async data => {
        const sRef = firebase.storage().ref();
        var uuid = getUniqueStr();
        const fileRef = sRef.child(`${uuid}.png`);
       
        await fileRef.putString(data, "data_url");
        const url = await fileRef.getDownloadURL();

        db.collection("cards").add({
          where: this.text1,
          what: this.text2,
          imageurl: url,
          created_at: new Date(),
          user_uid: this.$store.getters.user.uid,
          user_name: this.user_name,
          post_id: uuid
        });
        this.text1 = "";
        this.text2 = "";
        this.$router.push({ path: `/post/${uuid}` });
      });
    }
  }
};
</script>
```

###iPhone（スマホ）のsafariでSVGの表示が崩れる
iPhoneのsafariから、SVGを生成するとテキストの表示が崩れました。
![スクリーンショット 2019-09-14 13.04.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/367226/fcd2c4ff-b46d-d6b9-ca75-77fa78eeadf6.png)


そこで、

```vue
      <text font-family="sans-serif" font-weight="bold" dominant-baseline="middle">
        <tspan font-size="10px" x="15" y="15">{{text1}}</tspan>
        <tspan font-size="8px" x="15" y="28">で、</tspan>
        <tspan font-size="10px" x="15" y="43">{{text3}}</tspan>
        <tspan font-size="10px" x="15" y="58">{{text4}}</tspan>
        <tspan font-size="10px" x="15" y="73">{{text5}}</tspan>
        <tspan font-size="8px" x="15" y="88">を見た。</tspan>
      </text>
```
font-familyの指定と、dominant-baseline="middle"を追加しています。

###SVGで改行処理
SVGで文字を改行することはできないので、フォームで入力された内容を一行ごとに分割するコードを書きました。

```vue
      <textarea
        v-model="text2"
        type="text"
        style="width:100%; margin-bottom:10px"
        placeholder="何？（100字以内）"
        class="form-control"
        maxlength="100"
        minlength="1"
        required
        rows="4"
      />
```
このフォームで入力された内容は"text2"に入れられ、

```vue
  computed: {
    text3: function() {
      return this.text2.substring(0, 15);
    },
    text4: function() {
      if (this.text2.length <= 15) {
        return "";
      }
      if (this.text2.length >= 16) {
        return this.text2.substring(15, 30);
      }
    },
    text5: function() {
      if (this.text2.length <= 30) {
        return "";
      }
      if (this.text2.length >= 31 && this.text2.length <= 43) {
        return this.text2.substring(30, 43);
      }
      if (this.text2.length >= 44) {
        return this.text2.substring(30, 43) + "...";
      }
    }
```
ここでtext3、text4、text5に分割されます。

#Cloud function（OGP表示側コード）
```javascript:index.js
const functions = require('firebase-functions');
const express = require('express');
const app = express();
const admin = require('firebase-admin');

admin.initializeApp({});
const db = admin.firestore();

const url = 'https://whatisaw.firebaseapp.com/post/';
const site_name = 'WhatISaw';
const title = 'WhatISaw';
const meta_description = '見たモノ・ヒト・コトを記録する。';
const meta_keywords = ['WhatISaw'];
const og_description = '見たモノ・ヒト・コトを記録する。';
const og_image_width = 1200;
const og_image_height = 630;
const fb_appid = '';
const tw_description = '見たモノ・ヒト・コトを記録する。';
const tw_site = 'https://whatisaw.firebaseapp.com/post/';
const tw_creator = '';

const genHtml = (image_url, post_id) => `
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>${title}</title>
    <meta name="description" content="${meta_description}">
    <meta name="keywords" content="${meta_keywords.join(',')}">
    <meta property="og:locale" content="ja_JP">
    <meta property="og:type" content="website">
    <meta property="og:url" content="${url}">
    <meta property="og:title" content="${title}">
    <meta property="og:site_name" content="${site_name}">
    <meta property="og:description" content="${og_description}">
    <meta property="og:image" content="${image_url}">
    <meta property="og:image:width" content="${og_image_width}">
    <meta property="og:image:height" content="${og_image_height}">
    <meta property="fb:app_id" content="${fb_appid}">
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="${title}">
    <meta name="twitter:description" content="${tw_description}">
    <meta name="twitter:image" content="${image_url}">
    <meta name="twitter:site" content="${tw_site}${post_id}">
    <meta name="twitter:creator" content="${tw_creator}">
  </head>
  <body>
    <script>
      location.href = '${tw_site}${post_id}';
    </script>
  </body>
</html>
`;

app.get('/s/:id', (req, res) => {
    db.collection("cards")
        .where("post_id", "==", req.params.id)
        .get()
        .then(querySnapshot => {
            querySnapshot.forEach(doc => {
                const html = genHtml(doc.data().imageurl, req.params.id);
                res.send(html);
            });
        });
});
exports.s = functions.https.onRequest(app);
```