😻

Astro ActionsとmicroCMSだけでいいね機能を作る

2024/12/14に公開

はじめに

この記事は microCMSでこんなことができた!あなたのユースケースを大募集 by microCMS Advent Calendar 2024 シリーズ2の14日目の記事です

今回はAstro 4.15 で追加されたActions APIを使ってmicroCMSでいいね機能をつけてみようと思います

作るにあたって、以下の記事を参考にさせていただきました
https://zenn.dev/k1350/articles/0f8466f2858f71

また、プロジェクトのベースを作るためにフルスタック生成AIサービスの bolt.new を使用しています

bolt.newについて、詳しくは別で記事を書いているので読んでみてください
https://zenn.dev/trpd/articles/58685060bffcda
https://zenn.dev/trpd/articles/cfbd217c38dd7c

完成品

先に、今回作成したコードを載せておきます

https://github.com/shibaTT/like-astro-actions

ブログを表示するための処理とかも入っています
ブログ表示部はすべてbolt.newに任せたので、エラーハンドリングをちゃんとやってるところとやってないところが混在しています
また処理の最適化などは行ってないので、あくまで動きの参考として見ていただけると

デモサイトはこちらです

https://like-actions.netlify.app/

構成

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キーの権限は以下のように設定しています
APIキーの権限画像

Actions部分

いいね追加処理

まずはいいね部分の処理から実装していきます
今回、あくまでいいね機能の実装がメインということで制限に関しては同一IPから送る場合5秒に1回しかいいねを送れないという制限のみ付けています

src/actions/like.ts
...省略

// 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の該当コンテンツに送信する、という処理を行っています

いいね数取得処理

次に、いいね数を取得する処理を追加します
これは、後ほどいいねボタンにいいね数を表示するために使います

src/actions/getLikes.ts
...省略
// 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 を作成していきます

src/actions/index.ts
import { like } from "./like"
import { getLikes } from "./getLikes"

export const server = {
    like,
    getLikes,
}

server 定数に先ほど定義した likegetLikes を設定します
これで「いいね追加処理」と「いいね数取得処理」がActionsとして登録されました

いいねボタン

記事詳細画面の日付横にいいねボタンを作成します
以下の画像の右にあるピンクのボタンですね

いいねボタン

コードの全体像はこちらでご確認ください
https://github.com/shibaTT/like-astro-actions/blob/master/src/components/LikeButton.astro

HTML部分

いいねボタン表示部分を記述します
後ほどの処理で使うため、 data-blog-id 属性に記事の blogId をセットします
この blogId は、src/pages/blog/[id].astro から渡された値です

src/components/LikeButton.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と通信していいね数を取得する関数を実行し、その後エレメントに対して取得したいいね数をセットする関数を実行します

src/components/LikeButton.astro
// 初期いいね数の取得と表示
private async initialize() {
    if (!this.blogId) return
    const likes = await this.getLikes()
    if (likes !== undefined) {
        this.updateLikeCount(likes)
    }
}

Actionsを経由していいね数取得

先ほど作ったActionsを呼び出していいね数を取得する処理を記述します
Actionsに渡す blogId はHTML部分で設定しています

src/components/LikeButton.astro
// 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側でいいね数をインクリメントするので、こちらでは現在のいいね数を渡します

src/components/LikeButton.astro
// 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
}

いいねボタンのエレメント更新

いいねボタンにあるいいね数表示部を更新する処理を記述します

src/components/LikeButton.astro
// いいね数の表示を更新
private updateLikeCount(likes: number) {
    if (this.likeCount) {
        this.likeCount.textContent = likes.toString()
    }
}

ボタンクリック時の処理

いいねボタンをクリックされたときの挙動を記述します
ボタンをクリックしたときにアニメーションをさせたいので、アニメーション用クラスを別で用意して付与しています

src/components/LikeButton.astro
// いいねボタンクリック時の処理
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で書きたい方などは注意が必要です
https://hiroppy.me/blog/astro-client-env/

ちなみにAstroのドキュメントではdata属性での渡し方を紹介しています
https://docs.astro.build/ja/guides/client-side-scripts/#フロントマター変数をスクリプトに渡す

動作確認

コマンドでローカルサーバーを立ち上げて動作確認してみます

npm run dev

いいねボタンを押すと……………

Image from Gyazo

ちゃんと数が増えましたね!
microCMSにAPIを送ってから表示を更新するので、ボタンを押してから0.5秒ほどラグがあります

microCMS側でも確認してみると、ちゃんと 2 になってますね

いいね数

ちなみに連打するとコンソール側にエラーが表示されます

エラー画像

注意点

外部からでも直接Actions(API)を叩けてしまうので、いいねの数は誰でも改ざんし放題になってます
対策するならば、Astroコンポーネントからいいね数を渡すのではなく、Actions側でいいね数を取得してインクリメントし更新するという処理にするのが良いかと思います
ただしその分処理に時間がかかってしまうので注意が必要です

さいごに

無事Astro Actionsを使っていいね数を増やすことができました
正直Astro IslandでReactとかを使ったほうが便利だと思いますが、Reactを使うほどでもないな~というプロジェクトだと外部ライブラリを増やすことなくAstroのみで済ませることができるので良いですね

完成品のコードはこちらに置いてあります
https://github.com/shibaTT/like-astro-actions

サイトのデモはこちらに
https://like-actions.netlify.app/

もっとActions活用したいな〜

Discussion