Help us understand the problem. What is going on with this article?

Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう

はじめに

質問箱や、ボタンメーカー、診断メーカー等を始めとする 「OGP画像生成系」 を 2個以上作ってそれのベストプラクティスがわかってきたので、共有したいと思います。

宣伝

この技術を使ったサービスを実稼働2日ぐらいで作りました!

使い方は簡単です!
メッセージカードを書いて、Twitterにシェアするだけ。

#嵐ありがとう

Screenshot from 2019-01-31 23-22-53.png

Screenshot from 2019-01-31 23-23-31.png

OGP画像生成系サービスとは?

「ツイッターでつぶやけるボタン」を簡単に作成できるサービスをリリースしました【個人開発】

すごくいいサービスですよね!

こういう系のリンクを共有したときに、画像が生成されて共有されるサービスのことを指します。

どうすれば簡単に作れるか?

こういう画像って作るの面倒そうじゃないですか。

僕も昔はImageMagickで頑張って合成したりして作ってたのですが、もっと簡単な方法を思いつきました。

そうだSVGを使おう!

構成図

  1. IllustratorでSVGのデザインをする(デザイナーに丸投げ)
  2. Vue.jsでSVGの中をテンプレートでいい感じにする
  3. Canvasに書き出してPNGに変換する
  4. それをFirebaseのCloud Storageにアップロードする

Web 1920 – 2.png

メリット

  • OGPのデザインに無限の可能性が広がる
  • わざわざ画像生成用のサーバを用意しなくていい
  • ユーザがリアルタイムでプレビューできる
  • フロントで生成しているので、コストが安い
  • 絵文字が使える!(これはヤバい!

デメリット

  • 特に思いつかないです

準備

 npm install -g @vue/cli
 npm install -g firebase-tools
 vue create my-project # ここお好きなプロジェクト名
 cd my-project
 npm i
 npm i --save firebase
 firebase init

OGP生成のフロントのコード(雰囲気)

イラレ等で生成されたSVGをおもむろにぶちこんでください。
サンプルで適当にメッセージを入れて検索をかけるのがおすすめです。
サンプルメッセージを{{msg}}等Vueの変数に置換します。

<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">{{ msg }}</tspan>
      </text>
    </svg>
    <input v-model="msg" type="text">
    <button @click="create">create</button>
  </div>
</template>

<script>
import firebase from 'firebase'

// Webコンソールから取得したコンフィグをペースト
const config = {
    apiKey: "",
    authDomain: "hogefuga.firebaseapp.com",
    databaseURL: "https://hogefuga.firebaseio.com",
    projectId: "hogefuga",
    storageBucket: "hogefuga.appspot.com",
    messagingSenderId: "323003240989"
  };
firebase.initializeApp(config)
const db = firebase.firestore()

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


export default {
  name: 'hello',
  data () {
    return {
      msg: 'Welcome to Your Vue.js PWA',
      uuid: '' // 適当に採番する
    }
  },
  methods: {
    create() {
      // refでsvgCardをsvgに設定しているのでthis.$refs.svgCardで要素を取れます
      svg2imageData(this.$refs.svgCard, (data) => {
        const sRef = firebase.storage().ref()
        const fileRef = sRef.child(`${this.uuid}.png`)

        // Cloud Storageにアップロード
        fileRef.putString(data, 'data_url').then((snapshot) => {
          // Firestoreに保存しておく
          const card = db.collection('cards').doc(this.uuid)

          return card.set({
            message: this.description
          }, { merge: false })
        }).then(docRef => {
          console.log(docRef)
        }).catch(err => {
          console.error(err)
        })
      })
    }
  }
}
</script>

本当はもうちょっとちゃんといろいろした方がいいですけど、サンプルなので良しとします。

OGP表示側(CloudFunction)コード

https://<domain>/s/:id

というURLにアクセスしたときに、OGPのメタタグが出るようにFirestoreから取得し、Cloud Storageから画像を取得するやつです。

const functions = require('firebase-functions')
const express = require('express')
const app = express()
const admin = require('firebase-admin')

admin.initializeApp(functions.config().firebase)

const db = admin.firestore()

let projectId, keyFilename, bucketName

// Firebaseのproject ID
projectId = '<FILL ME>'
keyFilename = 'privateKey.json'

// OGPが保存されてるCloudStorageのバケット
bucketName = '<FILL ME>'

async function generateSignedUrl (bucketName, filename) {
  // [START storage_generate_signed_url]
  // Imports the Google Cloud client library
  const { Storage } = require('@google-cloud/storage')

  // Creates a client
  const storage = new Storage({
    projectId,
    keyFilename
  })

  /**
   * TODO(developer): Uncomment the following lines before running the sample.
   */
  // const bucketName = 'Name of a bucket, e.g. my-bucket';
  // const filename = 'File to access, e.g. file.txt';

  // These options will allow temporary read access to the file
  const options = {
    action: 'read',
    expires: Date.now() + 1000 * 60 * 60 * 24 * 30 // 1month
  }

  // Get a signed URL for the file
  const [url] = await storage
    .bucket(bucketName)
    .file(filename)
    .getSignedUrl(options)

  console.log(`The signed url for ${filename} is ${url}.`)
  // [END storage_generate_signed_url]
  return url
}

const url = 'https://qiita.com/'
const site_name = 'Qiita'
const title = 'Qiita'
const meta_description = 'プログラミング情報共有サイトです。'
const meta_keywords = ['プログラミング']
const og_description = 'プログラミング情報共有サイトです。'
const og_image_width = 1200
const og_image_height = 630
const fb_appid = ''
const tw_description = 'プログラミング情報共有サイトです。'
const tw_site = ''
const tw_creator = ''

const genHtml = (url) => `
<!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=${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=${url}>
    <meta name="twitter:site" content=${tw_site}>
    <meta name="twitter:creator" content=${tw_creator}>
  </head>
  <body>
    <script>
      // クローラーにはメタタグを解釈させて、人間は任意のページに飛ばす
      location.href = '/share';
    </script>
  </body>
</html>
`

app.get('/:id', async (req, res) => {
  const doc = await db.collection('cards').doc(req.params.id).get()
  if (!doc.exists) {
    console.log(`${req.params.id} not exist`)
    res.status(404).send('404 Not Exist')
  } else {
    const url = await generateSignedUrl(bucketName, `${req.params.id}.png`)
    const html = genHtml(url)
    res.set('cache-control', 'public, max-age=3600');
    res.send(html)
  }
})
exports.s = functions.https.onRequest(app)

あと/s/:idでCloudFunctionにアクセスできるように設定を書きます。

// firebase.json
{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        // 大事
        "source": "/s/**", "function": "s"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

デプロイ

firebase deploy

簡単ですね!VueもFirebaseも素晴らしいです。

クレジット

まとめ

Vue.jsとFirebaseを使えば2日あれば、OGP画像生成系サービスを作れるようになります。

1度作ってしまうとだいたいコピペで量産できるので、コードが資産になります。

皆さんもぜひこの組み合わせでサービスを作ってみてください!

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away