【JAMstack】Next.js × Firebase で JAMstack なブログサイトを作る④［管理アプリ］

<!-- ======================================================= -->
# 概要
<!-- ======================================================= -->
本記事は、[【JAMstack】Next.js × Firebase で JAMstack なブログサイトを作る③［Hooks］](https://qiita.com/nemutas/items/2e97e69f637e912e7f9b)の続きとなっています。

本記事では、**Firebase Storage**へ記事の投稿・削除を行うアプリを作成します。
JAMstackブログサービスの基幹部分は前回までで完成しましたが、記事の投稿・削除はFirebase Storage上でしかできないので、ブログの管理者用のアプリを作成します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/d3d7d85d-6214-db7a-6c03-ad7a052f6981.png" width="500px">

<!-- ======================================================= -->
# プロジェクトの作成
<!-- ======================================================= -->
### Firebase Hosting
**Firebase Hosting**のプロジェクトを作成しておきます。
※ Firebase自体のプロジェクトは、[【JAMstack】Next.js × Firebase で JAMstack なブログサイトを作る②［ブログサイト］](https://qiita.com/nemutas/items/bb312d26cf3349ce0230)で作成したものを使います。

### アプリケーション
アプリケーションは、RCA（create-react-app）で作成します。
作業フォルダを作成して、そこでコマンドプロンプトで以下を実行します。

```
npx create-react-app . --template typescript
```

以下を実行して動作確認をします。

```
npm run start
```

### Hosting テスト
アプリケーションを作成したら、Hostingができるか確認しておきます。
firebase-toolsをインストールしていない場合は、インストールします。

```
npm i -g firebase-tools
```
プロジェクトで、コマンドプロンプトを使用して以下を実行します。

```
firebase init
```
初期設定は、以下のようにします。

- Hosting（上）を選択します。
- 既存プロジェクトとして、Firebaseのプロジェクトを選択します。
- デプロイするフォルダ名を**build**にします。
- React（SPA）なので、single-page-appをYesにします。
- GitHubと連携してリビルドする設定はNoにしておきます。（お好み）

![無題6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/977dde2e-d424-d374-58bf-b3a38390f38e.png)

初期設定ができたら、以下を実行してHostingします。

```
npm run deploy
```
成功すればURLが表示されるので、アクセスしてReactの初期ページが表示されればOKです。

<!-- ======================================================= -->
# アプリケーションの作成
<!-- ======================================================= -->
### 機能
メインの機能として、以下のことをできるようにします。

- Firebase Storageにブログ記事を投稿する
- Firebase Storage内のブログ記事を削除する
- 削除するために、Firebase Storage内のブログ記事を表示する

オプションの機能として、以下も実装します。

- **Firebase Authentication**を使用して認証機能を実装する
- ブログ記事で画像を扱えるようにするために、画像投稿機能を実装する
- 画像投稿機能を実装する上で、削除、一覧の表示機能も実装する

### 使用するライブラリ

UIは、**Material-UI**を使用します。

https://material-ui.com/

状態管理は、**Recoil**を使用します。（使うのは認証部分のみ）

https://recoiljs.org/

スタイリングは、Material-UIのコンポーネントはMaterial-UIで用意されているStyle方法、通常のコンポーネントについては**emotion/css**で行います。

https://emotion.sh/docs/@emotion/css

### パッケージ インストール

```
npm i @material-ui/core @material-ui/icons
npm i recoil @types/recoil
npm i @emotion/css
npm i gray-matter
```
gray-matterは、取得した記事をテキストデータからオブジェクトデータに変換するために使用します。

### Firebase Storage のアクセス設定
#### Rules
Firebase Storageの設定で、Rulesを変更します。
![無題9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/43baae13-f80b-a843-18e0-7e5d8814e498.png)

#### CORS
アプリケーション内で記事や画像を取得する場合、Storageに**CORS**を設定する必要があります。
設定方法は以下を確認してください。

https://firebase.google.com/docs/storage/web/download-files?hl=ja#cors_configuration

https://qiita.com/niusounds/items/383a780d46ee8551e98c

#### 外部ユーザーの Storage アクセス権限
画像をブログ記事に埋め込んで表示するためには、Storageを外部に公開する必要があります。
特殊なメンバー**allUsers**を追加して、Storageの読み取り専用の権限を与えます。

https://cloud.google.com/storage/docs/access-control/making-data-public?hl=ja

### コーディングについて
コーディング部分は、長くなるので割愛します。成果物のGitHubリンクを参照してください。

### 環境変数の設定
RCA（create-react-app）で作成されたプロジェクトでは、特になにかをインストールしなくても環境変数を扱えます。また、環境変数の接頭辞は**REACT_APP_**にする必要があります。
root直下に`.env.local`を作成して、Firebaseの**SDK の設定と構成**で取得できる値を以下のように設定します。

```.js:.env.local
REACT_APP_FIREBASE_APIKEY="apiKey"
REACT_APP_FIREBASE_DOMAIN="authDomain"
REACT_APP_FIREBASE_DATABASE="https://PROJECT_ID.firebaseio.com"
REACT_APP_FIREBASE_PROJECT_ID="projectId"
REACT_APP_FIREBASE_STORAGE_BUCKET="storageBucket"
REACT_APP_FIREBASE_SENDER_ID="messagingSenderId"
REACT_APP_FIREBASE_APP_ID="appId"
```
以下のファイルを作成して、firebaseの初期化設定を行う処理を記述します。

```.ts:src/firebase/firebase.ts
import 'firebase/auth';
import 'firebase/storage';
import firebase from 'firebase/app';

const firebaseConfig = {
	apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
	authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
	databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
	projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
	storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
	messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
	appId: process.env.REACT_APP_FIREBASE_APP_ID
};

if (!firebase.apps.length) firebase.initializeApp(firebaseConfig);

export const auth = firebase.auth();
export const storage = firebase.storage();
export const provider = new firebase.auth.GoogleAuthProvider();
```

認証では、メール＆パスワードの他にGoogleアカウント認証もできるようにしました。

### SingIn 状態の監視
Firebase Authenticationには、ユーザーのSingnIn状態を監視する関数があります。
これを状態管理ライブラリ（Recoil）と組み合わせることで、SignIn機能をスムーズに実装できます。

https://firebase.google.com/docs/auth/web/manage-users?hl=ja#get_the_currently_signed-in_user

```.ts:src/firebase/authFunctions.ts
/**
 * SignInの状態を監視する
 */
export const useAuth = () => {
	const [signInUser, setSignInUser] = useRecoilState(signInUserState);
	const resetStatus = useResetRecoilState(signInUserState);

	useEffect(() => {
		const unSub = auth.onAuthStateChanged(authUser => {
			if (authUser) {
				setSignInUser({
					uid: authUser.uid,
					email: authUser.email!
				});
			} else {
				resetStatus();
			}
		});
		return () => unSub();
	}, [setSignInUser, resetStatus]);

	return signInUser;
};
```

### 記事の取得
記事は、getDownloadURLを使用してurlを取得して、fetchすることで取得できます。
それをgray-matterを使用して解析して、必要な情報を抜き出します。

https://firebase.google.com/docs/storage/web/download-files?hl=ja

```.ts:src/firebase/storageFunctions.ts
const getArticleContents = async (article: any) => {
	const url = await article.getDownloadURL();
	const res = await fetch(url);
	const data = await res.text();
	const matterResult = matter(data);
	return {
		path: article.fullPath as string,
		...(matterResult.data as { date: string; title: string })
	};
};
```
※ 引数の型anyは、本来`firebase.storage.Reference`ですが、正常に設定できなかったため泣く泣くanyにしています。

### 画像の取得
Firestore Storageを外部公開（読み取りのみ）しているので、画像はそれを利用してurlを取得し、imgタグに割り当てます。

```.ts:src/firebase/storageFunctions.ts
const urlPrefix = `https://storage.googleapis.com/${process.env.REACT_APP_FIREBASE_STORAGE_BUCKET}/`;

/**
 * 画像データ配列を取得する
 * @returns 日付ソートした画像データ配列
 */
export const getImages = async () => {
	try {
		const result: ImageType[] = [];
		const images = await storage.ref('images').list();
		await Promise.all(
			images.items.map(async item => {
				const meta = await item.getMetadata();

				if ((meta.contentType as string).startsWith('image')) {
					result.push({
						name: meta.name,
						date: new Date(meta.updated).toLocaleString(),
						url: urlPrefix + (meta.fullPath as string)
					});
				}
			})
		);

		return result.sort((a: ImageType, b: ImageType) => {
			const a_PathMilli = new Date(a.date);
			const b_PathMilli = new Date(b.date);
			return a_PathMilli < b_PathMilli ? 1 : -1;
		});
	} catch (error) {
		console.log({ error });
	}
};
```

### その他の処理
ファイルのアップロードや削除は、ドキュメントを読めば特に詰まることなく実装できると思います。

https://firebase.google.com/docs/storage/web/upload-files?hl=ja

https://firebase.google.com/docs/storage/web/delete-files?hl=ja

### 動作確認
管理アプリからブログ記事の投稿、削除を行うと、VercelのDashboardで自動的にリビルド・デプロイされ、ブログサイトが更新されることを確認します。
また、アップロードした画像リンクをブログ記事に埋め込むと、画像が表示されることを確認します。

最後にFirebase Hostingにデプロイして終了です。

<!-- ======================================================= -->
# 成果物
<!-- ======================================================= -->

https://github.com/nemutas/blog-manager-frontend

SignIn画面

![無題7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/03e53dcd-011c-0804-995e-420f91d5de82.png)

管理画面

![無題8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/06eefce4-68d7-77f9-4a0f-41e07e5ed646.png)

<!-- ======================================================= -->
# まとめ
<!-- ======================================================= -->
ようやくJAMstackなブログサイトが完成しました。
Firebaseを使えば簡単にできるかなーって始めましたが、思ったより時間と労力がかかりました。調査7割、実装3割くらいの労力でした。
でも、調べて解決する分だけ、できることの幅が広がると思えば報われます。😌