Route360

Firebase + React + Gatsbyで、(レビュー機能付き)コメントシステムを作る

目次

Gatsby.jsサイト用にGoogle Firebaseを使ってコメントシステムを作った際の、備忘録です。

Firebaseとは、アプリ開発用のプラットフォームで、ログインシステムやカートシステム等のデータを格納したり読み書きができる機能を提供しています。

Firebaseの機能の1つである「Realtime Database」は、json形式でデータを読み書きできる上、javascriptのHTTPリクエスト等でデータにアクセスできるため、コメントシステムにも使いやすいかと思います。

私がFirebaseのRealtime Databaseでコメントシステムを作ったのはGatsbyサイト用でしたが、表示部分を工夫すればNext.js等でも使えるはずです。

また、Firebaseを利用したコメントシステムでは入力項目が自由に設定できるため、製品にレビュー(星評価)機能をつけたいECサイト用にもいいかと思います。

※私自身はFirebaseに明るいとはとても言えないレベルのため、間違いや改善点がありましたら何なりとご指摘ください🙇‍♀️

動作環境:

  • Node v18.12.1
  • React v18.2.0
  • Gatsby v5.7.0 / v4.25.0

Realtime Databaseでデータベースを作る

Firebaseにはすでに登録済みという前提で進めます。

プロジェクトの作成

まずは新しいプロジェクトを作成。

Firebaseで新しいプロジェクトを作成

© Google Firebase

プロジェクト名は自由です。今回は「comments」としました。

Firebaseで新しいプロジェクトを作成

© Google Firebase

今回はGoogle Analyticsは使わないので、アナリティクスとの連携はオフにしています。

Firebaseで新しいプロジェクトを作成

© Google Firebase

「プロジェクトの作成」ボタンをクリックすると、10秒もかからずプロジェクトが作成されます。

Realtime Databaseの作成

プロジェクトの中に、Realtime Databaseというデータベースを作成します。

FirebaseでRealtime Databaseを作成

© Google Firebase

セキュリティルールは「ロックモードで開始」でかまいません。

FirebaseでRealtime Databaseのルール編集画面

© Google Firebase

Realtime Databaseに接続する準備

Realtime Databaseでは、データベースのURLの末尾にjsonファイル名を指定するだけで、データベースの中身にアクセスができます。

https://[yourproject].firebasedatabase.app/comments.json

一方、セキュリティルールの初期設定(readとwriteともfalse)のままでは管理者でも何もできないため、「シークレット」キーを認証キーとして使います。

シークレットキーを確認

トップの⚙️アイコンから、「プロジェクトの設定」→「サービスアカウント」タブ「Database Secrets」で、シークレットを確認します。

FirebaseでRealtime DatabaseのDatabase Secrets

© Google Firebase

このシークレットが認証AUTHとなり、fetch()内のHTTPリクエスト用URLの末尾に追加することで、Firebase のREST APIへアクセスができるようになります。

fetch(`https://[yourproject].firebasedatabase.app/comments.json?auth=[secret]`)

ただ、このシークレットキーはGoogle Firebase上ではレガシー扱いとなっており、Firebase Admin SDKを使用してアクセストークンを取得することが推奨されています。

アクセストークンはRealtime Databaseの「秘密鍵」を生成した上で、Google APIクライアントライブラリを利用して発行できます。(Node.js版を使うのが楽でしょうか。)

一方、この方法で発行できるアクセストークンは短時間で無効になってしまうため、ビルドの度にアクセストークン取得用ファイルを実行する必要があり、難易度が上がります。できる方はチャレンジしてみてください。

参考 REST リクエストの認証 | Firebase Realtime Database

投稿フォームのコンポーネントの作成

以下のコードは、説明のためにフォームバリデーションなどを省略するなど、かなり簡略化しています。実際の場合では実装してください。

また、今回はGatsby.jsでの実装のため、環境変数はGATSBY_FIREBASE_TOKENとしています(※)。必要に応じて変更してください。

※クライアント側で実行する場合の環境変数は、Gatsbyなら「GATSBY*」、Next.jsなら「NEXT*」等と接頭辞を付与する必要があります。

/src/components/commentForm.js
import React, { useState } from "react"

const CommentForm = (props) => {
  const [enteredRating, setEnteredRating] = useState("")
  const [enteredComment, setEnteredComment] = useState("")
  const [enteredName, setEnteredName] = useState("")
  const [enteredEmail, setEnteredEmail] = useState("")

  const submitHandler = (event) => {
    event.preventDefault()
    const dburl = `https://[yourproject].firebasedatabase.app/comments.json?auth=${process.env.GATSBY_FIREBASE_TOKEN}`

    const response = await fetch(dbUrl, {
      method: "POST",
      body: JSON.stringify({
        slug: props.slug, // コメントが投稿されたページのスラッグ
        name: enteredName,
        email: enteredEmail,
        rating: Number(enteredRating),
        comment: `<p>${enteredComment
          .replaceAll("\n\n", "</p><p>")
          .replaceAll("\n", "<br />")}</p>`,
        date: new Date().toISOString(),
        approved: false,
      }),
    })

    if (!response.ok) {
      return
    }

    setEnteredRating("")
    setEnteredComment("")
    setEnteredName("")
    setEnteredEmail("")
  }

  return (
    <form id="commentform" onSubmit={submitHandler}>
      <label htmlFor="rating">
        評価
        <fieldset id="rating">
          <input
            type="radio"
            id="p-rating_5"
            name="rating"
            defaultValue={5}
            checked={enteredRating === "5"}
            required
          />
          <label htmlFor="p-rating_5">5</label>
          <input
            type="radio"
            id="p-rating_4"
            name="rating"
            defaultValue={4}
            checked={enteredRating === "4"}
          />
          <label htmlFor="p-rating_4">4</label>
          <input
            type="radio"
            id="p-rating_3"
            name="rating"
            defaultValue={3}
            checked={enteredRating === "3"}
          />
          <label htmlFor="p-rating_3">3</label>
          <input
            type="radio"
            id="p-rating_2"
            name="rating"
            defaultValue={2}
            checked={enteredRating === "2"}
          />
          <label htmlFor="p-rating_2">2</label>
          <input
            type="radio"
            id="p-rating_1"
            name="rating"
            defaultValue={1}
            checked={enteredRating === "1"}
          />
          <label htmlFor="p-rating_1">1</label>
        </fieldset>
      </label>
      <label htmlFor="comment">
        コメント
        <textarea
          id="comment"
          name="comment"
          maxLength={1000}
          required
          value={enteredComment}
        />
      </label>
      <label htmlFor="author">
        お名前
        <input
          id="author"
          name="name"
          type="text"
          required
          value={enteredName}
        />
      </label>
      <label htmlFor="email">
        メールアドレス
        <input
          id="email"
          name="email"
          type="email"
          required
          value={enteredEmail}
        />
      </label>
      <button type="submit">送信する</button>
    </form>
  )
}

export default CommentForm

上記コードのポイント

  1. フォームの入力値を取得するために、ReactフックのuseStateを利用
  2. コメントが投稿されたページの判断のために、propsからslugを取得しておく
  3. 送信ボタンを押した時に、submitHandler関数を発火させる。その中で、HTTPリクエストのPOSTを使って、取得した入力値をbody要素としてRealtime Databaseに送信
  4. approvedキー(初期値false)をbody内で同時に送信し、Firebase上でtrueにすることでコメント承認
  5. 送信後は入力値をリセット(空欄に戻す)

フォーム自体は、Reactの基本であるuseStateとサブミットハンドラを使ったものです。

実際の場面ではこれに加えて、入力値のバリデーション(検証)、reCaptchaチャレンジ、送信後のモーダル表示(「コメントが送信されました」等)などを実装することになると思います。

このフォームコンポーネントを個別の投稿ページや製品ページ等に設置。フォームコンポーネントにslugを渡すことで、どのページに投稿があったかを記録します。

/src/templates/singlePage.js
import React from 'react'
import CommentForm from '../components/commentForm'
...

const SinglePage = () => {
  ...
  return (
    ...
    <CommentForm slug={`ページのslug`} />
    ...
  )
}

export default SinglePage

投稿内容の読み取り

今回はフロントエンドのフレームワークとして、Gatsbyを使用します。

Gatsby.jsではgatsby-node.js内にコードを追加することによって、外部APIからGraphQLを生成することが可能です。

そのため、テンプレート内でのHTTPリクエストはせず、gatsby developまたはgatsby buildの際にクエリを生成するコードを追加します。

コメント内容をGraphQLに追加する

gatsby-node.js
require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`,
})

const fetch = require("node-fetch")

exports.sourceNodes = async ({
  actions: { createNode },
  createContentDigest,
  createNodeId,
}) => {
  const response = await fetch(
    `https://[yourproject].firebasedatabase.app/comments.json?auth=${process.env.FIREBASE_TOKEN}`
  )
  const data = await response.json()

  Object.entries(data).map(([key, value]) => {
    value.approved && //approved === trueの場合
      createNode({
        id: key,
        date: value.date,
        name: value.name,
        comment: value.comment,
        rating: value.rating,
        slug: value.slug,
        parent: null,
        children: [],
        internal: {
          type: "Comments",
          contentDigest: createContentDigest(value),
        },
      })
  })
}

上記内容を追記した上でgatsby developを実行すると、GraphQLでコメント情報が取得できるようになります。

Gatsby上のGraphQL画面

尚、今回は「投稿者のe-mailを表示しない」という前提で、表示用としてはe-mailをGraphQLに反映させていません。不必要なデータをGraphQLとして生成しなければ、ビルド時間を節約することができます。

コメントの日付フォーマットオプションを有効にする

Gatsby v4では日付のフォーマットオプション(表示形式編集・今から何日前の表示等)がデフォルトで有効になっていますが、Gatsby v5では追加したスキーマの日付フォーマットのオプションがデフォルトで無効になっているようです。

日付フォーマットのオプションを有効にしたい場合は、gatsby-node.jsに以下を追記します。

gatsby-node.js
exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
  type Comments implements Node {
    date: Date @dateformat
  }
  `
  createTypes(typeDefs)
}

これで日付フォーマットのオプションが有効になります。

Gatsby上のGraphQL画面

コメントを投稿に紐付けする

さらに、コメントを各投稿と紐付けすれば、記事一覧ページでもコメント数や星の数を表示することが可能になります。投稿とコメントの共通項はスラッグなので、slugが一致するそれぞれを結びつけます。

異なるクエリを紐付けるにはいくつか方法がありますが、今回はcreateResolversを使いました。

※以下はサイトコンテンツがMarkdownで管理されている場合の例です。ヘッドレスCMSからデータを引っ張ってきている場合は、MarkdownRemarkやfilterの部分をよしなに変更してください。

gatsby-node.js
exports.createResolvers = ({ createResolvers }) => {
  const resolvers = {
    MarkdownRemark: {
      comments: {
        type: ["Comments"],
        resolve: async (source, args, context, info) => {
          const { comments } = await context.nodeModel.findAll({
            query: {
              filter: {
                slug: { eq: source.frontmatter.slug },
              },
            },
            type: "Comments",
          })
          return comments
        },
      },
    },
  }
  createResolvers(resolvers)
}

gatsby-node.jsに上記コードを追加した上でgatsby developを実行すると、投稿にコメントが紐付けられているのが確認できます。

Gatsby.jsのGraphQL

既存のコメントシステムからの引っ越し

Realtime Databaseはjsonデータのインポートも可能なので、既存のコメントをjsonデータに変換すればコメントシステムをFirebaseに引っ越すこともできます。

たとえば、今までのコメントを以下のようにまとめます。

comments.json
{
  "comments": {
    "0": {
      "date": "2022-08-29T00:00:00:000Z",
      "email": "[email protected]",
      "comments": "<p>とても美味しかったです。</p>",
      "name": "山田太郎",
      "rating": 4,
      "slug": "honey00",
      "approved": true
    },
    "1": {
      "date": "2023-01-30T00:00:00:000Z",
      "email": "[email protected]",
      "comments": "<p>素敵な商品でした。</p>",
      "name": "佐藤花子",
      "rating": 5,
      "slug": "flower01",
      "approved": true
    }
  }
}

Realtime Databaseのトップページ(「データ」タブ)の右の3点マークから、jsonデータがインポートできます。

Realtime Databaseのインポート画面

© Google Firebase

まとめ(その他課題など)

Disqusなど既存のコメントシステムで満足できない場合の代替手段として、Firebaseを使った方法を紹介しました。

今回のコードは動作部分の説明だけですので、コメントシステムとしては入力値のバリデーションも含めて、他にもやることは色々あります。

  • 投稿者IPの取得
  • reCaptcha等によるボット対策
  • e-mailの有効性確認
  • コメントが投稿された場合は通知が来るようにする
  • コメントにいいねボタンをつける
  • 返信システムをつける

また、コメント承認がFirebase上で手入力が必要なのもやや不便ではあります。クライアントによっては、コメント用のUIページを別途用意する・・・など、追加の作業が必要になりますね。

ちなみに、レビューと星評価付きのコメントシステムとしては、筆者はYotpoを利用したことがあります。無料枠でもある程度使えるので、高機能を求めるならそちらや類似サービスを使った方が早いですね(当たり前)。