FirebaseとReactでブログを実装!

記事
IT・テクノロジー

FirebaseとReactでブログを実装!

FirebaseでWebホスティングをすれば、無料でWebサイトの運営をスタートできます。
Firebaseの利用事例としてブログサイトの運用を取り上げてみました。 この記事ではシンプルなブログの運用例としてフロントエンドのフレームワークのReactを使った事例を何回かに分けて紹介していきます。

利用するFirebaseの機能は?

この事例で利用するFirebaseの機能は
* Firebase ストレージ
* Firebase Cloud Firestore (データベース)
* Firebase ホスティング
の3つの機能です。

Firebaseストレージで記事を保存

ブログの記事はFirebaseのストレージに保存します。こうすることで、Fireabaseのホスティングに毎回デプロイすることを避けることができます。
一番シンプルな方法は、毎回記事のHTMLのファイルを作成して、その都度その記事をWebサイトに追加していく方法ですが、毎回Webサイトのデプロイを行わなければならず、運用上はちょっと面倒な仕組みです。そこで記事自体はFirebaseのストレージに保存しておいて、プログラムでFirebaseストレージのファイルを読み込んで表示するという仕組みを作れば、サイトの運営がシンプルになります。

Firebase Cloud Filrestore(データベース)に記事情報を保存

Firebaseのストレージだけでも、ブログサイトの仕組みは作れますが、将来的なサイトの拡張を考えると、各記事の情報をデータベースで保存しておくとサイトの仕組み作りに役立ちます。
例えば、ファイル名ではなく各記事にタイトルをつけて表示できるようにしたり、記事内容の概要等なども表示するためには、単純にFirebaseのストレージに入れてファイル名だけで表示するよりは利用者がより使いやすく見やすいサイトにすることが可能です。また記事の投稿された日時や更新時間なども管理が可能です。

仕組みはReactのアプリで作る

記事のリストの表示や、指定された記事を表示するための仕組みはReactで作ります。 フロントエンドのフレームワークの代表格であるReactを使う以外にも、Vueなども利用可能です。 この記事では、Reactで作ることを前提に書いています。
その他のオプションとしては、NEXT/NUXTといったサーバー側での描画(レンダリング)を利用する方法もありますが、初心者でも作りやすいフロントエンドでの仕組みづくりをこの記事では紹介します。

Firebaseのホスティングを利用して無料で立ち上げる!

インターネットで、ブログを運営するためにはWebホスティングが必要です。今回はすべてを「Firebae」を利用して実現します。Firebaseのホスティングを利用してホスティングするのはReactのアプリです。 ブログの記事は、Webホストを使わずにFirebaseのストレージを利用しているので、いったんReactのアプリがインターネット上で稼働してしまえば、ブログ記事の投稿は完全に独立して行うことが可能になります。

ブログをReactアプリで実現

実施例として、趣味の登山サイトを実際に紹介している方法でReactアプリを作って稼働させています。
Reactで実装するポイントとして、
* カテゴリごとにフォルダを分ける。
* 各ページは、個別にURLが指定できる
を実装のコンセプトにしています。各ページはReact(Javascript)で動的に作っています。しかし、ページ単位でサーチエンジンに認識してもらえるように、記事ごとにページ指定できるような構成にしています。 実現には「React Router(react-router-dom)」を利用して実現しています。

React Routerのインストールと使い方

npm を使って、React Routerのモジュールをインストールしておきます。

npm install react-router-dom

これで、あとは、React Routerをアプリケーションのトップの記述で設定すればReact Routerを使ってページ指定できるようになります。実装例では「App.tsx」で設定を行っています。
React Routerのモジュールをインポートして
import { BrowserRouter, Switch, Route } from "react-router-dom";

以下のようなJSXを「render()」メソッドに書きます。
render() {
    return (
      <div className="App">
        <BrowserRouter>
          <Switch>
            <Route path="/" exact>
              <Home />
            </Route>
            <Route path={window.location.pathname}>
              <Article path={window.location.pathname}/>
            </Route>
          </Switch>
        </BrowserRouter>
      </div>
    );
}
「Home」が最初に表示されるページで、記事のリストやサイトの説明を表示するページに設定してあります。 「Article」が実際に記事を表示するページです。 このページでは、URLで指定されたページを表示するような記述ですが、表示は上から順番に評価されます。最初に「/」でドメインのトップフォルダが指定された場合は「Home」モジュールが表示されます。 それ以外のURLが指定された場合は全て「Article」のモジュールを表示します。その際、Articleのモジュールには指定されたパス名を「window.location.pathname」で取得して渡します。

Articleモジュールではパス名を元に表示する記事情報を取得

Articleのモジュールでは、渡されたパス名を元に表示するデータを受け取る処理をします。そのうえで、指定された記事を表示します。
記事のパスの構成は
* 最初のフォルダー名がカテゴリを示す
* 次のフォルダーがファイル名を示す
という風に決めて起きます。
「/outdoor/hr_gps_data」と指定された場合は、「outdoor」が記事のカテゴリになります。「hr_gps_data」がファイル名で実際のファイル名は拡張子がついたものになります。例えば、「hr_gps_data.html」のようになります。
最初に、記事はカテゴリ名と同じフォルダ内にファイルが保存されるような構造をFirebaseストレージに作ります。つまり、Firebaseストレージ内のファイルのパスは「"outdoor" + "/" + "hr_gps_data" + ".html"」(outdoor/hr_gps_data.html)ということになります。
Articleモジュールでは最初の処理で、この「カテゴリ(フォルダ名)」と「ファイル名」を取り出す処理をします。
const path = this.props.path.split("/");
const folder = path[1];
const file = path[2];
Blog.getPost(folder, file).then((post: TYPE.StorageFile | undefined) => {
    this.setState({
        post: post,
    });
});
「Blog.getPost(folder, file)」で投稿の中身を取得します。このメソッドは記事のファイルが存在しない場合は「undefined」を返すように実装します。
React Routerの設定で「"/"」以外は全てこの「Article」のモジュールにルーティングがされるため、存在しないページを指定した場合のエラーはこのモジュールでやる必要があるからです。「記事」がみつからずに、「undefined」が帰ってきた場合は、ページが存在しないというエラーの処理を行います。(404エラーと呼ばれる処理を行います)
取得した投稿はHTML形式のデータにして、「dangerouslySetInnerHTML」を使って表示させることができます。
render() {
    if (this.state.post) {
      return (
        <div className="article">
            <section>
                <article
                    dangerouslySetInnerHTML={{
                        __html: this.state.post.contents
                            ? this.state.post.contents
                            : "",
}}
                ></article>
            </section>
          </div>
      );
    } else {
      return (<div></div>);
    }
}
記事データの取得はFirebaseから!
記事の取得はまず、記事の詳細情報をFirebaseのデータベース(Cloud Firestore)で取得します。 その情報を元に、Firebaseストレージからダウンロード用のURLを取得します(getURL())。
static getPost(
    folderName: string,
    fileName: string
  ): Promise<TYPE.StorageFile | undefined> {
    return new Promise((resolve) => {
      Blog.getFolder(folderName).then((folder: TYPE.FbFolder | undefined) => {
        if (folder) {
          firebase
            .firestore()
            .collection(CONSTANT.FB_COLLECTION_DATA)
            .doc(folder.id)
            .collection(folderName)
            .where("file", "==", fileName + ".html")
            .get()
            .then((querySnapshot: firebase.firestore.QuerySnapshot) => {
              if (querySnapshot.size === 1) {
                const doc = querySnapshot.docs[0];
                const post: TYPE.StorageFile = {
                  id: doc.id,
                  folder: doc.data().folder,
                  file: doc.data().file,
                  title: doc.data().title,
                  timestamp: doc.data().timestamp,
                  url: doc.data().url,
                  contents: doc.data().contents,
                };
                Blog.getURL(folderName, post.file)
                  .then((url) => {
                    post.url = url;
                    if (url) {
                      fetch(url)
                        .then((response) => {
                          return response.text();
                        })
                        .then((text) => {
                          let contents: string = text;
                          post.contents = text;
                          resolve(post);
                        })
                        .catch((err) => {
                          resolve(undefined);
                        });
                    } else {
                      resolve(undefined);
                    }
                  })
                  .catch((error: any) => {
                    resolve(undefined);
                  });
                // A document exists
              }
            })
            .catch((error: any) => {
              resolve(undefined);
            });
        } else {
          resolve(undefined);
        }
      });
    });
  }
  static getFolder(name: string): Promise<TYPE.FbFolder | undefined> {
    return new Promise((resolve) => {
      firebase
        .firestore()
        .collection(CONSTANT.FB_COLLECTION_FOLDER)
        .where("name", "==", name)
        .get()
        .then((querySnapshot: firebase.firestore.QuerySnapshot) => {
          if (querySnapshot.size === 1) {
            // Exists
            const doc = querySnapshot.docs[0];
            const folder: TYPE.FbFolder = {
              id: doc.id,
              name: doc.data().name,
              display: doc.data().display,
            };
            resolve(folder);
          } else {
            // Error: It does not exists or moultiple entries are found
            resolve(undefined);
          }
        })
        .catch((error: any) => {
          if (CONSTANT.DEBUG) {
            console.log("lib/blog.ts:getFolder():");
            console.log(error);
          }
          resolve(undefined);
        });
    });
  }
  static getURL(
    folderName: string,
    fileName: string
  ): Promise<string | undefined> {
    return new Promise((resolve) => [
      firebase
        .storage()
        .ref()
        .child(folderName + "/" + fileName)
        .getDownloadURL()
        .then((url: string) => {
          resolve(url);
        })
        .catch((error: any) => {
          resolve(undefined);
        }),
    ]);
  }
Firebaseのデータベースアクセスには利用者の読み込み権限が必要になります。 Firebase Cloud Firestoreのセキュリティルールの例です。全ての利用者にデータの「outdoor」のコレクションと、「folders」の読み込みを許可しています。
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
    match /data/{dataId}/outdoor/{outdoorPostId} {
     allow read;
    }
    match /folders/{folderId} {
     allow read;
    }
  }
}
Firebaseのストレージでは、URLを取得するために読み込み権限が必要です。
Firebaseストレージのセキュリティルールの例です。「outdoor」フォルダの読み込みをすべての利用者に許可しています。
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow write,read: if false;
    }
    match /outdoor/ {
        allow read;
    }
  }
}

CORS(Cross Origin Resource Sharing)の設定

Firebaseストレージのファイルの中身は、「fetch()」を使って読み込んでいます。しかし、この操作はアクセスするドメインが同じでない場合Webブラウザでブロックされてしまいます。したがって、Firebaseのストレージ側でCORSの設定を行って、異なるドメインでも読めるようにします。
FirebaseストレージのCORSの設定のやり方は別の記事で解説しています。

記事のアップロードは?

単純に記事をFirebaseストレージにアップロードするだけの場合、Firebaseコンソールから行うことができます。しかし、各記事のタイトルなどの情報をFirebaseのデータベースで管理する場合は、Firebaseコンソールでデータベースのデータの入力もできますが不便です。
できれば、ファイルをアップロードするための管理用のアプリを作った方がスムーズに運用が可能です。
管理用のアプリを作らない場合、シンプルにFirebaseのストレージからファイル名のリストやフォルダの情報を取得して表示するようにReactのアプリを作る事も可能です。ただし、投稿する記事の数が増えると、Firebaseのストレージからファイルやフォルダのリストを取得するのに時間がかかります。Cloud Firestoreから情報を取得した方が、高速に結果が得られるので利用者にとっては便利な実装になります。

まとめ

以上がFirebaseとReactを使ってブログサイトを実装する概要になります。 実装自体は、Firebaseの機能を駆使すると駆使すると意外にシンプルなコードでブログのサイトを作ることができます。実際に運用している例が、「tomonorihirai.com」です。ReactとFirebaseで実装されています。 Firebaseのホスティングで使用しているサイズはわずか1.5MBです。現時点では記事の数は15本ほどしかありませんので、使用しているストレージのサイズは画像データを含めても153.4KBだけです。
ストレージは5GBまで、ホスティングは10GBまで無料で利用できますので容量的には無料枠は当分超えることはなさそうです。あとは、アクセス数や転送回数・容量といった要因で利用料金が決まりますが、当面の運用は無料で問題なさそうです。
さらに詳しい実装のやり方やソースコードを別途作成しています。近いうちにお届けする予定です。
サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す