vue.js
Firebase
個人開発
Firestore

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(
}).catch(err => {
console.error(err)
})
})
}
}
}
</script>

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


OGP表示側(CloudFunction)コード

https:///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('/s/: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度作ってしまうとだいたいコピペで量産できるので、コードが資産になります。

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