はじめに
質問箱や、ボタンメーカー、診断メーカー等を始めとする 「OGP画像生成系」 を 2個以上作ってそれのベストプラクティスがわかってきたので、共有したいと思います。
宣伝
この技術を使ったサービスを実稼働2日ぐらいで作りました!
使い方は簡単です!
メッセージカードを書いて、Twitterにシェアするだけ。
OGP画像生成系サービスとは?
(。・ρ・)オシテミテ
— Kizuna AI@hello,2019‼︎ (@aichan_nel) 2019年1月29日
キズナアイな気分のときに押すボタンを作りました!みんな押してね!#キズナアイな気分のときに押すボタン#みんなのボタンメーカー
https://t.co/2mzJUE48zj
「ツイッターでつぶやけるボタン」を簡単に作成できるサービスをリリースしました【個人開発】
すごくいいサービスですよね!
- 診断メーカー
【ブチ切れた時の沙希】
— 沙希@オリジン済み (@A1to839alo) 2019年1月31日
これヤバいなwwwhttps://t.co/uJfHj1fzLi#あなたがブチ切れたらhttps://t.co/8Je16QvMX3
こういう系のリンクを共有したときに、画像が生成されて共有されるサービスのことを指します。
どうすれば簡単に作れるか?
こういう画像って作るの面倒そうじゃないですか。
僕も昔はImageMagickで頑張って合成したりして作ってたのですが、もっと簡単な方法を思いつきました。
そうだSVGを使おう!
構成図
- IllustratorでSVGのデザインをする(デザイナーに丸投げ)
- Vue.jsでSVGの中をテンプレートでいい感じにする
- Canvasに書き出してPNGに変換する
- それをFirebaseのCloud Storageにアップロードする
メリット
- 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も素晴らしいです。
クレジット
- フロントエンド、バックエンド@_serinuntius
- デザイン@skinnybrian_tw
- 企画, 立案@okamu_ro
まとめ
Vue.jsとFirebaseを使えば2日あれば、OGP画像生成系サービスを作れるようになります。
1度作ってしまうとだいたいコピペで量産できるので、コードが資産になります。
皆さんもぜひこの組み合わせでサービスを作ってみてください!