Astro ActionsとmicroCMSだけでいいね機能を作る
はじめに
この記事は microCMSでこんなことができた!あなたのユースケースを大募集 by microCMS Advent Calendar 2024 シリーズ2の14日目の記事です
今回はAstro 4.15 で追加されたActions APIを使ってmicroCMSでいいね機能をつけてみようと思います
作るにあたって、以下の記事を参考にさせていただきました
また、プロジェクトのベースを作るためにフルスタック生成AIサービスの bolt.new を使用しています
bolt.newについて、詳しくは別で記事を書いているので読んでみてください
完成品
先に、今回作成したコードを載せておきます
ブログを表示するための処理とかも入っています
ブログ表示部はすべてbolt.newに任せたので、エラーハンドリングをちゃんとやってるところとやってないところが混在しています
また処理の最適化などは行ってないので、あくまで動きの参考として見ていただけると
デモサイトはこちらです
構成
AstroとmicroCMSのSDK を使っています
スタイリングにはtailwindを採用しました
それ以外のライブラリなどは極力使わないようにしています
- Astro 4.15.3
- microcms-js-sdk 2.7.0
- tailwindcss 3.4.1
実装
前提
今回、microCMS側で作成したAPIのエンドポイント名は blogs
で、APIスキーマに関しては以下の通りです
フィールドID | 名前 | 種類 |
---|---|---|
title | タイトル | テキストフィールド |
content | 本文 | リッチエディタ |
eyecatch | アイキャッチ | 画像 |
category | カテゴリ | コンテンツ参照(カテゴリ) |
likes | いいね数 | 数字 |
また、APIキーの権限は以下のように設定しています
Actions部分
いいね追加処理
まずはいいね部分の処理から実装していきます
今回、あくまでいいね機能の実装がメインということで制限に関しては同一IPから送る場合5秒に1回しかいいねを送れないという制限のみ付けています
...省略
// microCMSのクライアントを作成(環境変数から認証情報を取得)
const client = createClient({
serviceDomain: import.meta.env.MICROCMS_SERVICE_DOMAIN,
apiKey: import.meta.env.MICROCMS_API_KEY,
})
// クライアントIPアドレスのキャッシュマップ(連続いいねを防ぐ)
const ipCache = new Map<string, number>()
// いいねアクションを定義
export const like = defineAction({
// 入力データのスキーマ検証
input: z.object({
blogId: z.string(),
likes: z.number(),
}),
// アクションのハンドラー関数
handler: async (input, ctx): Promise<LikeResponse> => {
try {
// 入力データの分割代入
const { blogId, likes } = input
const { request } = ctx
// 新しいlikes数
const newLikes = likes + 1
// クライアントのIPアドレスを取得(プロキシ環境に対応)
const clientIp = request.headers.get("x-forwarded-for") || "unknown"
const now = Date.now()
// 直近のいいね時間を確認
const lastLike = ipCache.get(clientIp)
// 5秒以内の連続いいねを制限(LIKE_COOLDOWNは別ファイルで定義)
if (lastLike && now - lastLike < LIKE_COOLDOWN) {
return { success: false, error: `${LIKE_COOLDOWN / 1000}秒間待ってからいいねしてください` }
}
// microCMSのブログコンテンツを更新(いいね数をインクリメント)
await client.update({
endpoint: "blogs",
contentId: blogId,
content: {
likes: newLikes,
},
})
// IPアドレスとタイムスタンプをキャッシュに保存
ipCache.set(clientIp, now)
// 成功レスポンスを返却
return { success: true, likes: newLikes }
} catch (error) {
// エラーハンドリング
console.error("Like error:", error)
return { success: false, error: "いいねの更新に失敗しました" }
}
},
})
コメントアウトを見ていただけると、やってることは大体わかるかなと思います
大体はIPでの制限に関する記述なので、いいねを付ける機能として大事なのは以下の部分だけです
// microCMSのブログコンテンツを更新(いいね数をインクリメント)
await client.update({
endpoint: "blogs",
contentId: blogId,
content: {
likes: likes + 1,
},
})
いいねボタンから渡されたいいね数に1追加してmicroCMSの該当コンテンツに送信する、という処理を行っています
いいね数取得処理
次に、いいね数を取得する処理を追加します
これは、後ほどいいねボタンにいいね数を表示するために使います
...省略
// microCMSのクライアントを作成(環境変数から認証情報を取得)
const client = createClient({
serviceDomain: import.meta.env.MICROCMS_SERVICE_DOMAIN,
apiKey: import.meta.env.MICROCMS_API_KEY,
})
export const getLikes = defineAction({
input: z.object({
blogId: z.string(),
}),
handler: async (input): Promise<GetLikesResponse> => {
const { blogId } = input
try {
const response = await client.get<Blog & MicroCMSDate & MicroCMSContentId>({
endpoint: "blogs",
contentId: blogId,
})
return { success: true, likes: response.likes }
} catch (error) {
console.error("getLikes error:", error)
return { success: false, error: "いいね数の取得に失敗しました" }
}
},
})
ここで大事な処理は以下の部分です
const response = await client.get<Blog & MicroCMSDate & MicroCMSContentId>({
endpoint: "blogs",
contentId: blogId,
})
return { success: true, likes: response.likes }
ここでmicroCMSと通信して、該当記事の詳細情報を取得しています
ただし、いいね数以外の情報はいらないので response.likes
でいいね数のみを返しています
Actionsとして定義
このままではActionsとして定義されていないので、 src/actions/
に index.ts
を作成していきます
import { like } from "./like"
import { getLikes } from "./getLikes"
export const server = {
like,
getLikes,
}
server
定数に先ほど定義した like
と getLikes
を設定します
これで「いいね追加処理」と「いいね数取得処理」がActionsとして登録されました
いいねボタン
記事詳細画面の日付横にいいねボタンを作成します
以下の画像の右にあるピンクのボタンですね
コードの全体像はこちらでご確認ください
HTML部分
いいねボタン表示部分を記述します
後ほどの処理で使うため、 data-blog-id
属性に記事の blogId
をセットします
この blogId
は、src/pages/blog/[id].astro
から渡された値です
<div class="like-button-wrapper flex items-center justify-center" data-blog-id={blogId}>
<button
class="like-button flex items-center justify-center bg-pink-100 text-pink-500 px-4 py-2 rounded-full hover:bg-pink-200 transition-colors"
>
<span class="inline-block mr-1">♥</span>
<span class="inline-block like-count w-5 h-5 -mt-1.5 transition-all"></span>
</button>
</div>
JS部分
初期化
まずは初期化をします
いいねボタンの隣にいいね数を表示するので、パーツ読み込み時にまず一度microCMSと通信していいね数を取得する関数を実行し、その後エレメントに対して取得したいいね数をセットする関数を実行します
// 初期いいね数の取得と表示
private async initialize() {
if (!this.blogId) return
const likes = await this.getLikes()
if (likes !== undefined) {
this.updateLikeCount(likes)
}
}
Actionsを経由していいね数取得
先ほど作ったActionsを呼び出していいね数を取得する処理を記述します
Actionsに渡す blogId
はHTML部分で設定しています
// APIからいいね数を取得
private async getLikes(): Promise<number | undefined> {
if (!this.blogId) return
const { data, error } = await actions.getLikes({ blogId: this.blogId })
if (error || (data && !data.success)) {
console.error("いいね数の取得に失敗しました")
return
}
return data.likes
}
Actionsを経由していいね数を更新
こちらも先ほど作ったActionsを呼び出して、いいね数を更新する処理を記述します
Actions側でいいね数をインクリメントするので、こちらでは現在のいいね数を渡します
// Actionsでいいね数を更新
private async setLikes(currentLikes: number): Promise<number | undefined> {
if (!this.blogId) return
const { data, error } = await actions.like({
blogId: this.blogId,
likes: currentLikes,
})
if (error || !data.success) {
console.error("いいねの更新に失敗しました")
return
}
return data.likes
}
いいねボタンのエレメント更新
いいねボタンにあるいいね数表示部を更新する処理を記述します
// いいね数の表示を更新
private updateLikeCount(likes: number) {
if (this.likeCount) {
this.likeCount.textContent = likes.toString()
}
}
ボタンクリック時の処理
いいねボタンをクリックされたときの挙動を記述します
ボタンをクリックしたときにアニメーションをさせたいので、アニメーション用クラスを別で用意して付与しています
// いいねボタンクリック時の処理
private handleClick = async () => {
if (!this.likeCount || !this.blogId) return
try {
// ローディングアニメーションの開始
this.likeCount.classList.add(...classes)
// 現在のいいね数を取得
const currentLikes = await this.getLikes()
if (currentLikes === undefined) return
// いいね数を更新
const newLikes = await this.setLikes(currentLikes)
if (newLikes === undefined) return
// 表示を更新
this.updateLikeCount(newLikes)
} finally {
// ローディングアニメーションの終了
this.likeCount.classList.remove(...classes)
}
}
補足
今回はAstro側の値を渡すためにdiv要素にdata属性を使っていますが、scriptタグに define:vars
を指定するという方法もあります
ただし、素のJSとして扱われるためTypeScriptで書きたい方などは注意が必要です
ちなみにAstroのドキュメントではdata属性での渡し方を紹介しています
動作確認
コマンドでローカルサーバーを立ち上げて動作確認してみます
npm run dev
いいねボタンを押すと……………
ちゃんと数が増えましたね!
microCMSにAPIを送ってから表示を更新するので、ボタンを押してから0.5秒ほどラグがあります
microCMS側でも確認してみると、ちゃんと 2
になってますね
ちなみに連打するとコンソール側にエラーが表示されます
注意点
外部からでも直接Actions(API)を叩けてしまうので、いいねの数は誰でも改ざんし放題になってます
対策するならば、Astroコンポーネントからいいね数を渡すのではなく、Actions側でいいね数を取得してインクリメントし更新するという処理にするのが良いかと思います
ただしその分処理に時間がかかってしまうので注意が必要です
さいごに
無事Astro Actionsを使っていいね数を増やすことができました
正直Astro IslandでReactとかを使ったほうが便利だと思いますが、Reactを使うほどでもないな~というプロジェクトだと外部ライブラリを増やすことなくAstroのみで済ませることができるので良いですね
完成品のコードはこちらに置いてあります
サイトのデモはこちらに
もっとActions活用したいな〜
Discussion