😻

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

に公開

はじめに

この記事は 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