Site cover image

ふつうのITエンジニアの独り言

本業はAndroidとiPhoneのアプリ開発のエンジニアです。将来はフリーランスで海の近くで妻とのんびり暮らすことを夢見て、幅広くIT技術に触れていきたいと思います。このブログはその備忘録と私のポートフォリオとして活動記録を記すものです。

astro-notion-blogをカスタマイズ(その3) ~いいねボタンの追加~

目次


はじめに


公式の実装方法の確認

 今回のは、”いいね”ボタンをastro-notion-blogに追加したいと思います。まだまだ人の目に触れる機会の少ない私の記事(備忘録)ですが、誰かの目に止まったときに”いいね”を頂けると、ものすごく頑張れる気がします。

 そういうわけで、ブログに不可欠な”いいね”ボタンを実現するために、まずはastro-notion-blogの公式の人の記事に目を通してみます。

 

 ここで紹介されている方法を整理すると、以下のことが書かれていました。

  • notionデータベースに”いいね”数を保存するカラムを追加する。
  • Notion APIを利用して”いいね”数を取得、更新する。
  • 記事に表示される”いいね数”は、SSR(サーバーサイドレンダリング)で最新値を取得して表示する。
  • SSRはCloudflareでは動作しないため、SSRが使えるホスティングサービスを使う。
  • いいねボタンを押したとき、クライアント側のJavaScriptからリクエストし、最新の”いいね数”をレスポンスで受け取って表示を更新する。

公式の実装の課題

 公式を真似して実装しようとNotionデータベースにカラムを追加したところで課題が見つかり、さっそくつまづく事になりました。課題というのは…

  • 私のブログでは、最終更新日時を記事に表示するようにカスタマイズしている。
  • 最終更新日時はNotionデータベースのページに付与されている日時を利用している。
  • Notionデータベースの”いいね”数を更新すると、ページの最終更新日時がアップデートされる。
  • “いいね”して貰うたびに、最終更新日時が変わる(泣)

というものでした。

 それに、ようやく使い方が分かってきたCloudflareからVercelに乗り換えることにも抵抗があるため、Cloudflareの機能を使って”いいね”ボタンを実現することを考えてみたいと思います。

Cloudflareでの実現方法


 結論から先に書くと、Cloudflare WorkersとCloudflare KVを使うことで実現することができました。Cloudflare WorkersとCloudflare KVについて、簡単に説明しておきます。

Cloudflare Workersについて

 Cloudflare Workersは、軽量で高速なサーバーレス環境を提供するプラットフォームです。JavaScript/TypeScriptなどでコードを書けてCloudflareのサーバ上で実行することができるので、astro-notion-blogをCloudfrareでデプロイしている人にとっては使い勝手がよいと思います。またAPIの実行はCDNによって、ユーザーに近い場所で処理が行われるため、低遅延での応答が期待できます。

 無料枠で出来ることで特筆したいのは、以下の2つです。

  • リクエスト数は、最大100,000リクエスト/日
  • CPU実行時間は、各リクエストにつき 最大10ミリ秒

リクエスト数については、10万ということで多すぎるくらいですが、リクエストの処理時間が10ミリ秒と短すぎます。それでも、”いいね”の数を更新することと、最新値を取得する程度であれば問題なく実行出来ると思います。

 ただ、今回は次で説明する、静的コンテンツに”いいね”値を埋め込むため、すべての”いいね”値を10ミリ秒で取得する必要があります。これを実現するための方法も合わせて遺しておきます。

Cloudflare KVについて

 Cloudflare KV(Key-Value)は、その名前の通りキーバリューストアで、Workersから高速な読み書きが可能です。こちらも無料枠では制限があります。

  • 1日あたり最大100,000件の読み込み操作
  • 1日あたり最大1,000件の書き込み/削除/列挙操作
  • 最大1Gストレージ

 容量と読み込み件数に関しては、”いいね”ボタンを実現するに当たって十分すぎる枠が用意されています。書き込みに関しては1000件…と少なく感じますが、いいねが1000回も押される人気サイトでもない限りは問題なさそうです。

システム概要

最新”いいね”数の埋め込みフロー

 今回、CloudflareのWorkersとKVを使って”いいね”ボタンを実現するにあたって、常に最新の値を表示するのではなく、ビルド時点での最新値を使って静的コンテンツを生成する方法を採用しました。これによって、SSRを使うこと無く、かつ高速表示の性能も落とさずに実現することができます。

 一方で、読者はリアルタイムに”いいね”の値を知ることができないのですが、よっぽどの人気サイトでもない限りは、”いいね”の数が劇的に増えることもないので、私のようなサイトでは何の問題もないと思います。

 ビルドは、定期的に実行されるようになっているので、1日に一回は”いいね”数を更新を更新することができます。デプロイまでの流れは以下のようになっています。②~⑤のステップは、記事の件数だけ繰り返されます。

graph LR
	Timer(定期タイマー)
	Pages(Cloudflare Pages)
	Workers(Cloudflare Workers)
	KV(Cloudflare KV)

	Timer --> |①定期ビルド|Pages
	Pages --> |②いいね数要求|Workers --> |⑤いいね数|Pages
	Workers --> |③いいね数取得|KV --> |④いいね数|Workers
”いいね”ボタンクリック時フロー

 次に”いいね”ボタンを押したときの動作ですが、こちらはクライアント側のJavaScriptでWorkersのAPIを呼び出すことで、”いいね”数を更新するようにしました。また、更新したあとAPIは最新値を返却しますが、ブラウザ側では返却を待たずに”いいね”数を+1します。

 理由は、APIが返す値は最新値ですが、ブラウザに表示されている”いいね”数は最後にビルドされたときの最新値だからです。ビルド以降で”いいね”が10回押されていたとすると、APIが返す値は表示値から+1されたものに加えて、その前に押された+10された合計+11の数値が返ってくることになります。そうすると、クリックと同時に”いいね”数が+11されることになり、押した側は違和感を覚えることになります。

graph LR
	Workers(Cloudflare Workers)
	KV(Cloudflare KV)
	Client(ブラウザ JS)
	User(読者)
  
  User --> |①いいねクリック|Client
  Client --> |②更新API|Workers --> |③更新|KV
  Workers --> |④最新値|Client
  
  

実現方法の詳細


Cloudflare KVの作成

 いいねのカウントを保存するためのKVを新規作成します。作成方法は、左側のメニューからWorkers KV > Create Instanceで簡単に作成することができます。作成後に表示されるIDはWorkersからアクセスする際に必要になりますが、いつでもIDは参照できます。

Cloudflare Workersの作成

Cloudflare Workersのローカル環境構築

 私の場合はローカル環境で、VS Codeを使って開発を行っているので、下記のリンクの手順に従って開発環境を構築しました。ここでの説明は省略します。

KVバインディング

 Cloudflare WorkersからKVにアクセス出来るようにするために、バインディングの設定を行います。Cloudflare Workersのプロジェクトに自動で作成されているwrangler.jsoncを開いて、”kv_namespaces”を追加します。

{
	"$schema": "node_modules/wrangler/config-schema.json",
	"name": "/*任意の名前を設定*/", //デプロイ後にCloudflare上に登録される名前になります。
	"main": "src/index.ts",
	"compatibility_date": "2025-09-21",
	"observability": {
		"enabled": true
	},
	"kv_namespaces": [
		{
		"binding": "MY_KV", // Cloudflare Workersで使用する変数名
		"id": "/*KVのID*/" // 作成したKVのIDを書き込む
		}
	]
}
wrangler.jsonicを編集して、kv_namespacesとそのパラメータを設定する。
Cloudflare WorkersのAPI実装

 Cloudflare Workersのindex.htmlに以下のコードを書きました。用意した機能はたったの2つです。本当はすべての値を取得する機能も作ったのですが、KVでは一回のリクエストで一つの値しか取得/書き込みができません。そのため、すべての値を取得するにはすべてのKeyをfor文で回して取得する必要があります。そうすると、結果を返すまでに動作時間の10ミリ秒制限を経過する可能性があるため使うことがありませんでした。なお、10ミリ秒はスクリプトの実行時間で、KVのアクセスにかかる時間は含まれないらしいので、そうそう超えることはないかもしれませんが、念の為に使うのを辞めました。

  1. KVの指定したslugの“いいね”カウントを増やす機能
  2. KVの指定したslugの”いいね”カウントを取得する機能
const corsHeaders = {
  "Access-Control-Allow-Origin": "*", // 必要に応じてオリジンを限定
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type",
};

interface KVListResult {
  keys: { name: string }[];
  list_complete: boolean;
  cursor?: string;
}

// URLから値を取得
function getMethodName(url : string){
  return url.split("/").pop()?.split("?").shift();
}

export default {
  async fetch(request : Request, env : Env) {
    const { searchParams } = new URL(request.url);

    // 保存: /like?key=slug
    if (request.method === "GET" && getMethodName(request.url) == "like") {
      const key = searchParams.get("key");
      if (!key) {
        return new Response("key required.", { status: 400, headers: corsHeaders });
      }
      let value = await env.MY_KV.get(key);
	  if( value == null ){
		value = "1";
	  }else{
		value = (parseInt(value, 10) + 1).toString();
	  }

      await env.MY_KV.put(key, value);
      return new Response(`${value}`, { headers: corsHeaders });
    }

    // 取得: /get?key=slug
    if (request.method === "GET" && getMethodName(request.url)  == "get") {
      const key = searchParams.get("key");
      if (!key) {
        return new Response("key required", { status: 400, headers: corsHeaders });
      }
      let value = await env.MY_KV.get(key);
      if( value == null ){
        value = "0";
      }
      return new Response(value ?? "Not found", { headers: corsHeaders });
    }

    return new Response("Not found..", { status: 404, headers: corsHeaders });
  },
};
index.htmlのコード例。likeとgetで”いいね”のカウント数を更新と取得を行う。
ローカルサーバでの動作確認

 動作確認のため、ローカルサーバを立ち上げます。このとき、KV環境もローカルに用意されるみたいです。ローカルサーバが立ち上がると、http://localhost:8787 でアクセスすることができます。

% npm start

 試しに、以下のコマンドをシェルで実行すると、1が返ってきます。test-slugの”いいね”数が1に更新されたということです。もう一度実行すると、2が返ってきます。

% curl -L "http://localhost:8787/like?key=test-slug"

 次は、getを試してみます。すると、現在の最新値である2が返ってくると思います。

% curl -L "http://localhost:8787/get?key=test-slug"
Cloudflare Workersのデプロイ

 最後に作成したCloudflare Workersをコマンドでデプロイします。デプロイすると、”https://xxxx.workers.dev”のようなURLが表示されるので、このURLを使ってAPIを呼ぶことになります。

% npm run deploy

astro-notion-blogの修正

ビルド時に”いいね”最新値を取得実装

 astro-notion-blogがビルドされる時に、最新の”いいね”値を取得して静的コンテンツに埋め込むために、私はclient.tsのgetAllPosts()に処理を追加しました。

  for(const post of postsCache){
    const slug = post["Slug"];

    // slugを指定して、いいね数を取得
    const res = await fetch(`${PUBLIC_LIKE_COUNT_URL}/get?key=${slug}`);
    const likeCount =  await res.json() ;
    post["LikeCount"] = likeCount;
  }
client.tsのgetAllPosts()に”いいね”数をslug毎に取得してpostに追加する処理を追加。

 URLはCloudflare Pagesの定数設定でPUBLIC_LIKE_COUNT_URLを追加して、URLを設定しました。PUBLIC_LIKE_COUNT_URLを使うためには、server-constants.tsにも宣言する必要があります。このように定数化することでローカルサーバとデプロイ時のURLを自動で切り替えることができます。

export const PUBLIC_LIKE_COUNT_URL = import.meta.env.PUBLIC_LIKE_COUNT_URL || process.env.PUBLIC_LIKE_COUNT_URL || 'http://localhost:8787'
server-constants.tsに、PUBLIC_LIKE_COUNT_URLを追加。

 Postには、”いいね”数を保存できるようにLikeCountを追加しました。

xport interface Post {
  PageId: string
  Title: string
  Icon: FileObject | Emoji | null
  Cover: FileObject | null
  Slug: string
  Date: string
  Update: string
  Tags: SelectProperty[]
  Excerpt: string
  FeaturedImage: FileObject | null
  Rank: number
  LikeCount: number
}

 これでビルド時にすべての記事の”いいね”数を取得できるので、あとは好きなところに”いいね”ボタンと”いいね”数を表示するだけです。

”いいね”ボタンのコンポーネント作成

 いいねボタンについては、自由に好きなデザインのものを作ればよいです。私はこのようなボタンを記事の最後に表示するようにしました。ボタンを押すと、少し色がついて現在値に(+1)と表示されるようになっています。

ボタンを押す前
ボタンを押した後
”いいね”ボタンを押したときの動作実装

 ボタンを押したとき、クライアント側のJavaScriptでCloudflare WorkersのAPIを実行してKVに保存されている値を更新します。返ってきた値には最新値が含まれていますが、今回は使用していません。

function sendRequest(slug){
    const apiUrl = `${PUBLIC_LIKE_COUNT_URL}/like?key=${slug}`;

    fetch(apiUrl)
    .then(response => response.json())
    .then(data => {
        console.log('レスポンス:', data);
    })
    .catch(error => {
        console.error('エラー:', error);
    });
}

まとめ


 ”いいね”ボタンを、Cloudflareの機能だけを使って実現することができました。今回紹介していませんが、”いいね”ボタンが連打してもリクエストが連続で送られないようにする処理であったり、再読み込みしても同じ記事の”いいね”を押せないように工夫しています。

 実は、Cloudflareのリクエスト回数が10万回/日まで使えるので、ページを表示した時にクライアント側から最新値を取得して表示を更新するという方法で実現しても問題はなかったと思います。ただ、この方法ではF5連打で何度もリクエストが送信される懸念や、検索エンジンのクローラーに影響があるため、CEO対策をしたい場合には不向きであることもあり、静的コンテンツのビルド時に埋め込む方法を選びました。

 ”いいね”ボタンを追加したので、良かったら押してください。励みになります。