Nextjs と Firebase でブログ用CMS作る
前から作ろうと思っていたブログ用のCMSを作ったので、まとめてみます。
構成
フロントエンド: Nextjs
データベース: Firestore
ストレージ:Cloud Storage
ユーザー認証:Firebase Authentication
状態管理:Recoil
,swr
スタイル:styled-components
遷移アニメーション:framer-motion
なんでNextjs?
一番触れていたフレームワークであったことがかなり大きかったです。その中でも個人的に便利だったのはファイルベースのルーティングでした。ブログ投稿を動的にルーティングしてくれるDynamic Routing
や、モック用のAPIやサーバサイドで定義したい処理を設定できるAPI Route
がデフォルトでサポートされており、公式ドキュメントに沿ってスムーズに実装ができました。
なんでFirebase?
ブログ用のCMSを作るにあたってサーバー側に必要な機能は、以下の内容があればとりあえず足りるかなと思っていました。
投稿保存用のDB
ユーザー認証機能
画像アップロード機能
上の機能を実現するにあたって、自分でバックエンドを構築・運用するのは正直面倒だなあと思い、今回は全てFirebase
におまかせすることにしました。
投稿保存用のDB => Firestore
ユーザー認証機能 => Firebase Authentication
画像アップロード機能 => Cloud Storage
Firebase
上のそれぞれの機能は、共通の認証情報でアクセスすることができるため、管理する情報が少なくて良いこともメリットでした。
機能1:予約投稿・リアルタイム編集
ブログを公開するタイミング時に、わざわざ手作業で公開設定するのが面倒なので予約投稿できるようにしました。できるようにしましたと言っても、公開時間
と公開設定
を投稿のデータとして保存しているだけですが。。
import { StorageObject } from 'components/storage/types/storge-object'
import { Timestamp } from 'firebase/firestore'
// releaseが今より前 かつ publishがtrueの時のみ公開
export type Post = {
id: string
title?: string
slug?: string
publish: boolean
release: Timestamp
markdown?: string
thumbnail?: StorageObject
}
投稿を取得する際は、CMS経由でないと取得できないようにします。これを実現するために、Firebase Authentication
の認証情報とFirestore
のセキュリティルールを使用します。具体的な方法としては、Firebase Authentication
で管理しているユーザーにカスタムトークンとして権限情報を追加設定します。この権限情報をもとにFirestore
のCRUD権限を制御することで、CMSにログインしているかつ特定の権限を持つユーザーのみが投稿にアクセスできるようになります。
allow read,write: if request.auth.token.writer;
allow read,write: if request.auth.token.manager;
allow read,write: if request.auth.token.owner;
このままだとクライアントから投稿の取得ができないので、クライアントからもCMS経由で投稿を取得できるようにします。具体的には、Nextjs
にてクライアント取得用のAPI Route
を定義し、そのエンドポイントにアクセスする形式で実装しました。CMSとクライアント取得用エンドポイントは別の場所においた方が安全な気がしているため、ここは要検討です。。
クライアントから取得する際は、 CMSにログインできない前提があるため、先ほどのセキュリティルールに引っ掛かって投稿を取得できません。これを回避するために、Firebase Admin SDK
を利用して投稿を取得できるようにします。Firebase Admin SDK
を利用すれば、セキュリティルールを無視して投稿を取得できるため、CMSへのログインをしなくても問題なく取得できます。
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'GET') {
if (admin.apps.length === 0) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
privateKey: (process.env.FIREBASE_PRIVATE_KEY as string).replace(/\\n/g, '\n'),
clientEmail: process.env.FIREBASE_CLIENT_EMAIL
})
})
}
const query = req.query
const slug = query.slug as string
const db = admin.firestore()
const now = new Date()
const docPost = await db.collection('post')
.where('slug', '==', slug)
.where('publish', '==', true)
.where('release', '<', now)
.get()
if (docPost.empty) {
return res.status(404).json({ message: 'Post is not found' })
}
const post = docPost.docs[0].data() as Post
return res.status(200).json({
id: post.id,
title: post.title,
slug: post.slug,
release: post.release.toDate(),
markdown: post.markdown,
thumbnail: post.thumbnail
})
}
}
リアルタイム編集については特別な機能はなく、スイッチで切り替えることでマークダウンのプレビューが見れます。useState
を利用して切り替えています。また、マークダウンのパーサーは正規表現やアルゴリズムの勉強も兼ねて自分で作ってみました。独自のコンポーネントをマークダウン記法でかけたら便利そうと思ったので、そのうち実装してみたいです。忘れないうちに備忘録をいつか書きたいと思います。
機能2:画像アップロード機能
Zenn
の画像アップロード機能がめちゃ便利だったので、再現したいという思いから作ってみました。機能としては、CloudStorageに画像をアップロード
>アップロード先をもとにマークダウンを作成&挿入
という2つの機能を目指しましたが、個人的に悩んだのはユーザーがどこに画像用のマークダウンを組み込みたいのかという点でした。というのも、ユーザーが画像をアップロードする際はテキストエリアからフォーカスが離れるため、ユーザーがどこに画像用マークダウンを挿入するのか、意識が離れるのかもと意味があるか分からない深読みをしていたからです。少し考えて、以下のように落ち着きました。
1. テキストエリアに1度でもフォーカスした場合...最後にフォーカスした行の次の行に挿入する
2. テキストエリアに1度もフォーカスしていない場合...末尾(1文字以上ある行に限る)の次の行に挿入する
無意識に快適ゆえ、いざ自分で作るとなると色々迷いますね。。勉強になりました。
機能3:ユーザー認証
誰彼構わず投稿をいじれたら色々まずいので、Firebase Authentication
を使用して認証機能をつけます。基本的に公式のドキュメント通りに実装をして、追加でカスタムトークンを設定します。カスタムトークンを設定することで、ユーザー別の権限を付与したり、いろんな属性をつけることができます。
const setUserClaim = async (uid: string, customClaims: UserClaim) => {
try {
await admin.auth().setCustomUserClaims(uid, customClaims)
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.log(error)
}
}
}
作ってみて
以上の実装で基本的なブログ用のCMSとしての機能は果たすことができるようになりました。作り始める時に、本当にシンプルに作る! と目標立てたので、本当に他の機能はないです。笑
Firebase
は初めて使用したのですが、とてもシンプルにいろいろなサービスを触ることができました。他に作りたいものが出てきた時は、また使用してみたいと思います。
今後はオリジナルのコンポーネントをマークダウンに加えたり、投稿の他にオブジェクトを管理できるようにしたり、作りたい機能を足していきたいと思います。
参考文献
Discussion
無知で申し訳ないのですが、api routeのapiをエンドポイントにするとどのようなurlになるのでしょうか?
また、その場合セキュリティなどどのようになっているのでしょうか?
質問ありがとうございます!
Nextjs
では、プロジェクト内のpages/api
というディレクトリ配下にファイルを配置すると、その配置場所を API のエンドポイントとして認識するようになります。例えば、
pages/api
配下にblog.ts
というファイルを配置すると、/api/blog
というエンドポイントが作成されます。そして、/api/blog
宛にリクエストを送ると、先ほど配置したblog.ts
の内容を実行するようになります。セキュリティについては、
Firebase
におけるセキュリティルールという機能を使用することで制限をしています。セキュリティルールは、この条件を満たせばデータを取って良し! という許可形式で書くことができます。私の場合は、上の記事のようにCMSを利用している特定のユーザーのみデータが取得・更新できるように設定をしていました。CMSのユーザー以外がブログ記事を取得する際は、上記のセキュリティルールに引っ掛かって取得ができないので、
Firebase Admin SDK
を利用します。Firebase Admin SDK
は、特権環境として強い権限を持って操作することができるため、先ほど紹介したセキュリティルールもすっ飛ばしてデータの取得・更新を行うことができます。そのため扱うには注意する必要がありますが、今回は投稿の取得のみを API Route に定義することでその機能を限定しています。
こうすることで投稿取得・更新の可能な範囲は以下の通りとなります。
上の記事で話していた
という話は、「クライアントから投稿を取得する時に、取得先つまりCMSのドメインがバレるのであまり宜しくないよね・・」という話になります。(分かりづらくて申し訳ないです。。)ドメインがバレればログイン画面に辿り着いてログインされる可能性もあるため、できるなら別のサーバーを立ててそこにアクセスする方が余計な心配をしなくて良いということです。