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

Vue.js + Firebase + OGP画像生成 本当に爆速で作れた!

自己紹介

プログラマーのAkasaと申します。
夢を実現するために頑張ってる人を応援するサービスを模索しています!
今回、Vue.js, Firebase (Hosting, Functions, Storage)でOGP画像生成形サービスを爆速で作りました!

作ったサービス

  • Wishlist Share
    • Amazonのほしい物リストを画像付きでツイートできるサービス。

開発環境

  • firebase : ^7.6.0
  • vue": ^2.6.6

参考にした記事

ページ

リンクを踏む側サンプル

リンクを作る側① リンクを作る側② リンクを踏む側

Functions設定

  • parseAmazon

    • ユーザーに入力してもらったほしい物リストのリンクから、該当のページをパースし商品画像などのデータを取得する。(省略)
  • buildHtml

    • SPAで動的にOGPを設置する為にはテクニックが必要で生のHTMLを返すFunctionsを用意する。
    • HeaderにTwitterBot用のOGPを設置する。
    • BodyにはTwitterでリンクをタップした人に見せたいページのhrefを設置し、そこに遷移させる。

アーキテクチャ

  • FirebaseCloudStorageの構成に合わせてツイートに載せるリンクが変わる。
  • 今回のディレクトリ構成は/amazon/TYPE/ID.pngなので URLにはTYPEとIDのみ入れ込み、残りはFirebaseCloudStorageのメタデータに入れておく。

Diagram.png

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とかがある)

コード

build-html.js
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を載せておきます。
設計に応じて書き換えてください。

firebase.json
{
  "hosting": {
    "public": "dist",
    "rewrites": [{
        "source": "/parseAmazon",
        "function": "parseAmazon"
      },
      {
        "source": "/*/*/*",
        "function": "buildHtml"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
}

router.js
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
    },
  ]
});

ツイート

便利なサービス

Card validator
ngrokが便利すぎる

Why not register and get more from Qiita?
  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
No 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
ユーザーは見つかりませんでした