自己紹介
プログラマーのAkasaと申します。
夢を実現するために頑張ってる人を応援するサービスを模索しています!
今回、Vue.js, Firebase (Hosting, Functions, Storage)でOGP画像生成形サービスを爆速で作りました!
(サービスは現在停止しています)
作ったサービス
-
Wishlist Share
- Amazonのほしい物リストを画像付きでツイートできるサービス。
開発環境
- firebase : ^7.6.0
- vue": ^2.6.6
参考にした記事
-
Nuxt.js + FirebaseでOGPの仕組みを完全に理解した 〜俳句をSVGで描画するサービスをリリースした話〜
-
SNS映えするWebアプリを...!FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だった
ページ
リンクを作る側① | リンクを作る側② | リンクを踏む側 |
---|---|---|
Functions設定
-
parseAmazon
- ユーザーに入力してもらったほしい物リストのリンクから、該当のページをパースし商品画像などのデータを取得する。(省略)
-
buildHtml
- SPAで動的にOGPを設置する為にはテクニックが必要で生のHTMLを返すFunctionsを用意する。
- HeaderにTwitterBot用のOGPを設置する。
- BodyにはTwitterでリンクをタップした人に見せたいページのhrefを設置し、そこに遷移させる。
アーキテクチャ
- FirebaseCloudStorageの構成に合わせてツイートに載せるリンクが変わる。
- 今回のディレクトリ構成は/amazon/TYPE/ID.pngなので
URLにはTYPEとIDのみ入れ込み、残りはFirebaseCloudStorageのメタデータに入れておく。
ex)
-
URL
-
TYPE : 1 (まだ背景画像を1つしか用意してないので1のみ)
-
ID : O5Z11NOGY2KH (リストのID)
-
METADATA
-
AVATAR_URL :
https://images-fe.ssl-images-amazon.com/images~~~~
-
LIST_ NAME : "今年中に読みたい技術書"
-
LIST_DESCRIPTION : "Wishlist Share開発者Akasaのほしい物リストです!継続は力なり!読むぞー!"
-
DOMAIN : jp (amazonのドメイン、他にもco.ukとかがある)
コード
const functions = require('firebase-functions');
const util = require('util');
const SITEURL = 'https://wishlist-share.com';
const OGP_W = 1200;
const OGP_H = 630;
const SITE_NAME = 'Wishlist Share';
const { admin } = require('./firebase/firebase-admin');
const { generateRandomStrings } = require('./utils/randomStrings');
const func = functions.https.onRequest(async (req, res) => {
// ex) req.path = /amazon/1/2H6Q88D0SBG0E
const [,, type, id] = req.path.split('/');
const pageUrl = SITEURL + req.url;
let html;
let listName;
let desc;
let link;
// ex) https://wishlist-share.com/invited/amazon/1/2H6Q88D0SBG0E
link = `https://wishlist-share.com/invited/amazon/${type}/${id}`;
const path = `amazon/${type}/${id}.png`; // ex) /amazon/1/39QHNEX56SK42.png
const bucket = admin.storage().bucket('amaz0n-wish-list.appspot.com');
const file = bucket.file(path);
const exists = await file.exists();
if (!exists[0]) {
res.status(404).end();
return;
} else {
const getMetadataAsync = util.promisify(file.getMetadata).bind(file);
const metadata = await getMetadataAsync().catch(e => {
console.log(e);
throw e;
});
listName = metadata.metadata.listName;
desc = metadata.metadata.desc;
}
const randomStrings = generateRandomStrings(5); // For Web crawlers fetching every time.
const imageUrl = "https://firebasestorage.googleapis.com/~~~~";
html = createHtml(
/* title */ listName,
/* desc */ desc,
/* imageUrl */ imageUrl,
/* link */ link,
/* pageUrl */ pageUrl
);
res.set('Cache-Control', 'public, max-age=3600, s-maxage=3600'); // 1 hour
res.status(200).send(html);
return;
});
const createHtml = (title, desc, imageUrl, link, pageUrl) => {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Wishlist Share</title>
<meta property="og:title" content="${title}" />
<meta property="og:image" content="${imageUrl}" />
<meta property="og:image:width" content="${OGP_W}" />
<meta property="og:image:height" content="${OGP_H}" />
<meta property="og:description" content="${desc}" />
<meta property="og:url" content="${pageUrl}" />
<meta property="og:type" content="article" />
<meta property="og:site_name" content="${SITE_NAME}" />
<meta name="twitter:site" content="${SITEURL}" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${title}" />
<meta name="twitter:image" content="${imageUrl}" />
<meta name="twitter:description" content="${desc}" />
</head>
<body>
<script>
location.href = '${link}';
</script>
</body>
</html>
`;
};
module.exports = func;
躓きやすいポイント
firebase.jsonとrouter.jsを載せておきます。
設計に応じて書き換えてください。
{
"hosting": {
"public": "dist",
"rewrites": [{
"source": "/parseAmazon",
"function": "parseAmazon"
},
{
"source": "/*/*/*",
"function": "buildHtml"
},
{
"source": "**",
"destination": "/index.html"
}
]
}
import Vue from 'vue';
import Router from 'vue-router';
import Main from './views/Main.vue';
import Invited from './views/Invited.vue';
Vue.use(Router);
export default new Router({
mode: 'history',
linkExactActiveClass: 'active',
routes: [
{
// リンクを作る側
path: '/',
name: 'main',
component: Main
},
{
// リンクを踏む側
path: '/invited/amazon/:type/:id',
name: 'invited',
component: Invited
},
]
});