「firebaseの機能をふんだんに使って、クイズサイトをリリースした話」


はじめに

この記事の対象読者
・ firebaseを使いたいと思っている人
・ サイトをリリースしたいと考えている人

新卒2年目のエンジニアです!
会社でフロントエンドをできるようにしてほしいというリクエストがあり、
フロントエンドの勉強がてらにReactとfirebaseでサービスを作りました。

また、エンジニアなら一度は自分でサービスを運営することに憧れるのでは?と思います。
そんな憧れが自分にもあり、一度サービスをリリースしてみようと思い、今回作成しました。

どんなサービス?


みんなのスタディ
ちょっとしたクイズを作って、誰かに共有したいなあと思うことがあり、
クイズの投稿サイトを作成しました!

類似サービスはもちろんあったのですが、自分で色々使いやすいようカスタマイズできたらいいな
と思い、作りました。

プログラマーのみなさんには、下記クイズオススメなのでぜひ一度触って試してみてください。
Rubyクイズ | みんなのスタディ

システム概要/機能一覧

システム設計図

DNSはRoute53で設定し、firebase hostingを見に行くようにしています。
アカウント管理はfirebase Auth
クイズデータはfirestoreで管理しています。
cloud functionを使ってBotアクセスのときだけOGP設定を返すようにしています。

クイズ投稿機能

こだわりポイント

ログイン不要でクイズの作成ができます。
URLを簡単にシェアでき、限定公開の有無を指定できるので友達同士でも使えます!

クイズ解答機能

こだわりポイント

クイズを解くのがストレスにならないようにとにかくしたかった。
そのため、正解/不正解がすぐわかるようにする。画面スクロールをさせない。
というところを意識しました!
正解/不正解をすぐ反応させるために、firestoreから取得するデータを
クイズを解く前に全て取得しています!

マイページ


ログインして作ったクイズはここから編集できるようになっています。
また、いいねしたクイズを登録しておけます。

こだわりポイント

クイズを作った人のモチベーションになるのは、どれだけいいねされたか、どれだけ閲覧されたか
だと思うので、それがひと目でわかるようにしました!
今後も、モチベーションアップのための新規機能追加を予定しています!!

firebaseを駆使したポイント

Firestore

クイズのデータを格納。
Firestoreはクイズの読み書き当たりの課金のため、都度データを読み出していると
すぐに課金額が増えてしまう。
そのため、ちょっとした工夫が必要。

今回は、クイズのデータを一つのドキュメントにまとめることでデータを読み込む回数を減らしている。

Authentication

Firebaseの個人的に最も便利だと思う点。
ユーザーのログイン機能を簡単に実現できる。色々なサービスと連携しており、
ちょっとした操作で複数サービスとの連携がすぐにできる。

下記プラグインを使用することで、簡単に作成できます!
firebase/firebaseui-web-react: React Wrapper for firebaseUI Web

Functions

OGPを表示するために使用。
SSRを最初は考えていたが、OGPを表示するためにSSRをわざわざ行うのはオーバーな気がしたので、
Functionsを用いました。

具体的には下記のコードで、botとbot以外で分けてOGPを表示させるようにしています。

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const fs = require("fs");
const xss = require("xss");
admin.initializeApp(functions.config().firebase);

const isBot = userAgent => {
  return userAgent.includes("googlebot") ||
    userAgent.includes("yahoou") ||
    userAgent.includes("bingbot") ||
    userAgent.includes("baiduspider") ||
    userAgent.includes("yandex") ||
    userAgent.includes("yeti") ||
    userAgent.includes("yodaobot") ||
    userAgent.includes("gigabot") ||
    userAgent.includes("ia_archiver") ||
    userAgent.includes("facebookexternalhit") ||
    userAgent.includes("twitterbot") ||
    userAgent.includes("developers.google.com")
    ? true
    : false;
};

const getReplaceHtml = (HTML, title, description, uri) => {
  const html = HTML.replace(
    /<title>.*<\/title>/g,
    "<title>" + title + "</title>"
  )
    .replace(
      /<\s*meta name="description" content="[^>]*>/g,
      '<meta name="description" content="' + description + '" />'
    )
    .replace(
      /<\s*meta property="og:title" content="[^>]*>/g,
      '<meta property="og:title" content="' + title + '" />'
    )
    .replace(
      /<\s*meta property="og:url" content="[^>]*>/g,
      '<meta property="og:url" content="' + uri + '" />'
    )
    .replace(
      /<\s*meta property="og:description" content="[^>]*>/g,
      '<meta property="og:description" content="' + description + '" />'
    )
    .replace(
      /<\s*meta property="og:image" content="[^>]*>/g,
      '<meta property="og:image" content="~~.png" />'
    );

  return html;
};

exports.returnHtmlWithOGP = functions.https.onRequest((req, res) => {
  // Access URL '/answer/{quizId}'
  const domain = "https://min-study.com/";
  const userAgent = req.headers["user-agent"].toLowerCase();
  const path = req.path.split("/");
  const id = path ? path[path.length - 1] : null;
  const indexHTML = fs.readFileSync("./index.html").toString();

  if (isBot(userAgent) && id !== null) {
    const firestore = admin.firestore();
    const docRef = firestore.collection("quiz").doc(id);
    docRef
      .get()
      .then(snapshot => {
        const user = snapshot.data();
        const title = xss(user.title);
        const description = xss(user.description);
        const uri = xss(domain + "answer/" + id);
        const replacedHTML = getReplaceHtml(indexHTML, title, description, uri);
        res.status(200).send(replacedHTML);
        return true;
      })
      .catch(err => {
        console.log(err);
        res.status(404).send(indexHTML);
        throw new Error(err);
      });
  } else {
    res.status(200).send(indexHTML);
    return true;
  }
});

最後に

興味を持っていただいた方は、ぜひ一度使ってみてほしいです!
使った上で感想やこうしたほうがいいのでは?という意見を待ってます!

サービスをリリースすることで、学べることはかなりあるので、
迷っている方は一度は自分でサービスを立ち上げてみることをオススメします!