
これはFIXER Advent Calendar 2025 23日目の記事です。
astro-notion-blogについて
GitHubリポジトリはこちらです:
https://github.com/otoyo/astro-notion-blog
otoyoさんによって開発された、AstroとNotionを使ってブログを構築できるオープンソースソフトウェア(OSS)です。
私自身も、このastro-notion-blogとCloudflare Pagesを利用して個人ブログを構築しております(肝心の記事投稿はまだゼロ)。
otoyoさんのブログでもastro-notion-blogに関する記事が公開されており、その中で「いいねボタン」の実装方法も紹介されています。
https://alpacat.com/posts/how-to-implement-like-button-to-astro-notion-blog
これはAstroでSSR(Server-Side Rendering)を有効にし、サーバー処理でNotionのデータベースにある「いいね数」を更新することで実現されています。
しかし、Cloudflare Pagesではastro-notion-blogのSSRを動かすことができないようです。以下はotoyoさんの記事からの引用です。
Cloudflare PagesもAstroのSSRに対応していますが、Node.jsランタイムでないためSSRを有効にしたastro-notion-blogをCloudflare Pagesで動かすことはできません
そこでAstroとNotionではなく、Cloudflare KVとPages Functionsを使用して「いいねボタン」を実装してみました。当時はCloudflareに不慣れで少し苦労したので、本記事ではその実装を紹介したいと思います。

参考にした記事はこちらです:
https://zenn.dev/736b/articles/a9585ef0364e7a
なお、一部のコード生成にはAIを使用しています。
使用する技術
- astro-notion-blog
- Cloudflare
- Pages(ブログのホスティング)
- Functions
- KV
- wrangler
- Pages(ブログのホスティング)
カスタムドメインを取得している場合を除き、無料で構築できます。ありがとうCloudflare。
なお前提条件は以下の通りです。
- astro-notion-blogのREADMEの内容を実施済み
- Cloudflareのアカウントを作成済み
処理のイメージ
「いいねボタン」がどのような処理を行うのか、まず概要を整理します。
otoyoさんの記事で紹介されている流れは、以下のようになります。
astro-notion-blog -> SSRでNotion APIをコール -> Notionデータベースのいいね数を更新
この流れをほぼ踏襲します。APIのコール先がPages Functionsとなり、更新先はNotionではなくCloudflare KVとなります。
使用する技術からも想像できると思いますが、今回実装するいいねボタンは以下のようなシンプルな流れになります。
astro-notion-blog -> Pages FunctionsからAPIをコール -> KVの値を更新
よって以下を実施していきます。
- Cloudflare KVの用意
- リポジトリに
functions/ディレクトリを作成し、KVへいいね数を更新するAPIを作成 - いいねボタンのUIを作成
実装
実装に入る前に、プロジェクトにwranglerをインストールする必要があります。こちらのインストール方法に従い、以下コマンドをプロジェクトで実行します。
npm i -D wrangler@latestwranglerを初めて実行した際はCloudflareのログインが求められます。
それでは早速実装していきましょう。
Cloudflare KVの用意
KVはCloudflareのKey-Valueストレージサービスで、無料でも利用できます。
Getting Startedを参考に、以下コマンドをプロジェクトで実行してKV namespaseを作成します。
npx wrangler kv namespace create <BINDING_NAME><BINDING_NAME> には適当な名前を入れます。ここでは BLOG_LIKES_COUNTER とします。
これで、いいね数を保持するストレージが作成できました。簡単ですね。
上記コマンドを実行すると、以下のような出力が表示されるはずです。
※ <BINDING_ID> は適当な値になっているはず
🌀 Creating namespace with title "BLOG_LIKES_COUNTER"
✨ Success!
To access your new KV Namespace in your Worker, add the following snippet to your configuration file:
{
"kv_namespaces": [
{
"binding": "BLOG_LIKES_COUNTER",
"id": "<BINDING_ID>"
}
]
}この内容をwranglerの設定ファイルに追加します。 プロジェクトのルートにwrangler.jsoncファイルを作成し、内容をコピペしていきます。
ついでに compatibility_date というプロパティが必要なので、追加して作成日を入力します。現時点のwrangler.jsoncは以下です。
{
"compatibility_date": "2025-12-23",
"kv_namespaces": [
{
"binding": "BLOG_LIKES_COUNTER",
"id": "<BINDING_ID>"
}
]
}(wranglerからpagesにデプロイする場合、 "pages_build_output_dir": "./dist" も必要になります)
CloudflareのダッシュボードからKVを確認すると、このように作成されたことが確認できます。

これでKVの用意は完了です。
KVはKeyとValueのペアでデータを格納できるため、 {slugの値: いいね数の値} という組み合わせで登録するようにAPIを作成していきます。
functions/ にAPIを作成
Pages Functionsはディレクトリ直下に functions/ ディレクトリを作成し、その中に処理を書いたファイルを置くことでその機能が動作します。
たとえばGet startedに沿って以下のようなファイルを作成すると、 /helloworld にアクセスして Hello, world! というメッセージを得ることができます。
.
├── functions
│ └── helloworld.js
└── そのほかAstroプロジェクトTypeScriptの準備
さて、astro-notion-blogはTypescriptプロジェクトのため、まずはこちらの導入に従って準備を行います。
- プロジェクトのルートに
functionsディレクトリを作成します。 コマンドを実行します。これによりtypes.d.tsファイルが作成されます。
npx wrangler types --path='./functions/types.d.ts'functionsディレクトリ内にtsconfig.jsonファイルを作成します。
{ "compilerOptions": { "target": "esnext", "module": "esnext", "lib": ["esnext"], "types": ["./types.d.ts"] } }プロジェクトルートのtsconfig.jsonの
excludeに"functions/**/*"を追加します。以下の内容になるはずです。{ "extends": "astro/tsconfigs/strict", "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist", "functions/**/*"] }
これでTypeScriptの準備は終わりです。次は、kvにいいね数を更新するAPIを作成します。
APIエンドポイントの実装
ここでotoyoさんの記事から、エンドポイントの詳細を確認してみましょう。こちらの部分です。
- APIエンドポイントは
POST /api/likes.jsonとする POST /api/likes.json?slug=aaaのようにリクエストする- POSTのレスポンスで最新の「いいね数」を返す
レスポンスの形式は以下
{"likes": 1}
今回作成するエンドポイントもこれらの仕様に沿って実装します。APIエンドポイントが POST /api/likes とする点以外は、ほぼ同様の実装になります。
functions/api ディレクトリにlikes.tsファイルを作成します。コード全体は以下の通りです。なお BLOG_LIKES_COUNTER には先ほど作成したKVの名前を当てはめてください。
TypeScriptexport type Env = {
BLOG_LIKES_COUNTER: { // 🌱KVの名前を入力
get: (key: string) => Promise<string | null>
put: (key: string, value: string) => Promise<void>
}
}
/** CORS のための共通レスポンスヘッダ */
const CORS_HEADERS: Readonly<Record<string, string>> = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
}
/**
* エントリポイント: HTTP リクエストを受け取りメソッドに応じて処理する
*/
export const onRequest = async (context: {
request: Request
env: Env
}): Promise<Response> => {
if (context.request.method === 'OPTIONS') {
return new Response(null, { headers: CORS_HEADERS, status: 204 })
}
const { request, env } = context
const { searchParams } = new URL(request.url)
const slug = searchParams.get('slug')
if (!slug) { // slug は必須
return json({ error: 'slug クエリパラメータが必要です' }, 400)
}
try {
switch (request.method) {
case 'GET':
return await handleGetLikes(slug, env)
case 'POST':
return await handlePostLikes(slug, env)
default:
return json({ error: '許可されていないメソッドです' }, 405)
}
} catch (error) {
console.error('リクエスト処理中にエラーが発生しました:', error)
return json({ error: 'サーバ内部でエラーが発生しました' }, 500)
}
}
/**
* GET: 現在のいいね数を取得して返却
* KV に値がなければ 0 として扱う
*/
async function handleGetLikes(slug: string, env: Env): Promise<Response> {
const likes = await getLikes(env, slug)
return json({ likes }, 200)
}
/**
* POST: いいね数を 1 増加して返却
*/
async function handlePostLikes(slug: string, env: Env): Promise<Response> {
const current = await getLikes(env, slug)
const next = current + 1
await setLikes(env, slug, next)
return json({ likes: next }, 200)
}
function getLikeKey(slug: string): string {
return `likes:${slug}`
}
/**
* KV からいいね数を取得
*/
async function getLikes(env: Env, slug: string): Promise<number> {
const raw = await env.BLOG_LIKES_COUNTER.get(getLikeKey(slug)) // 🌱KVの名前を入力
if (raw == null) return 0
const parsed = parseInt(raw, 10)
return Number.isNaN(parsed) ? 0 : parsed
}
/**
* KV にいいね数を保存
*/
async function setLikes(env: Env, slug: string, value: number): Promise<void> {
await env.BLOG_LIKES_COUNTER.put(getLikeKey(slug), String(value)) // 🌱KVの名前を入力
}
/**
* 共通の JSON レスポンス生成
*/
function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
})
}
これでAPIエンドポイントの作成ができました。
さらに参考記事にあるように、CORS設定をしておくと良いと思います:
いいねボタンコンポーネントの作成
src/components ディレクトリにLikeButton.astroファイルを作成します。 アイコンはお好きなものにしてください。
TypeScript---
interface Props { slug: string }
const { slug } = Astro.props;
---
<div class="like-container">
<button id="like-button" class="like-button" data-slug={slug}>
<span class="like-icon">👍</span>
<span id="like-count">読み込み中...</span>
</button>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
const likeButton = document.getElementById('like-button') as HTMLButtonElement | null;
const likeCount = document.getElementById('like-count');
if (!likeButton || !likeCount) return;
const slug = likeButton.dataset.slug;
if (!slug) return;
// 共通の API 呼び出し
const callLikeApi = async (method: 'GET' | 'POST') => {
try {
const res = await fetch(`/api/likes?slug=${slug}`, {
method,
headers: method === 'POST' ? { 'Content-Type': 'application/json' } : undefined,
});
if (!res.ok) throw new Error(`APIエラー: ${res.status}`);
const data = await res.json();
return Number(data?.likes) || 0;
} catch (err) {
console.error(method === 'GET' ? 'いいね数の取得に失敗しました:' : 'いいねの更新に失敗しました:', err);
return method === 'GET' ? 0 : null;
}
};
// 初期化
const initial = await callLikeApi('GET');
likeCount.textContent = String(initial ?? 0);
// クリック処理
likeButton.addEventListener('click', async () => {
likeButton.disabled = true;
const newCount = await callLikeApi('POST');
if (newCount !== null) likeCount.textContent = String(newCount);
likeButton.disabled = false;
});
});
</script>
<style>
.like-container {
display: flex;
justify-content: center;
margin: 1rem 0;
}
.like-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid #ddd;
border-radius: 2rem;
background-color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.like-button:hover { background-color: #f5f5f5; transform: translateY(-2px); }
.like-button:active { transform: translateY(0); }
.like-icon { font-size: 1.2rem; }
</style>ボタンコンポーネントができたら、 src/pages/posts/[slug].astro ファイルを開いて作成したコンポーネントを追加します。
フロントマター部分に以下を追加します。
TypeScriptimport LikeButton from '../../components/LikeButton.astro'また、 <Layout> 内のお好きな箇所に以下を追加します。
HTML<LikeButton slug={slug} />これで、コード部分の設定が完了しました。
ここらで動作確認をしてみましょう。以下のコマンドを実行して、ローカルで起動できます。
npm run build
npx wrangler pages dev ./distkvもローカルで起動します。適当な記事のいいねボタンを押した後、 .wrangler/state/v3/kv/[BINDING_ID] ディレクトリを見るとsqliteのファイルが追加されていると思います。
kvをバインディングする
最後に、本番環境の設定を行います。ドキュメントを参考に、PagesからKVのバインディングを行います。
Cloudflareのダッシュボードからastro-notion-blogのPagesにアクセスして設定を開き、「バインディング」の項目へ移動します。

バインディングの右側にある「+追加」から「KV名前空間」を選び、作成したKVの名前を入力して保存します。

これでKVのバインディングの設定は完了です。
これまでの変更をGitHubにpushして、変更を反映させるとともにデプロイさせましょう。KVの設定が反映されるのは設定移行のデプロイからだそうなので、先にpushした場合はもう一度デプロイを走らせる必要があります。
完成
適当な記事を作成し、いいねボタンを押してみましょう。

いいねが出来ました👍
Cloudflareダッシュボードから作成したKVを見ると、このように値が登録されていると思います🙌

おわりに
Cloudflare KVとPages Functionsを利用していいねボタンを実装することができました。記事はまだないのに、いいねボタンにこだわってしまいました......でも、実現できて本当に嬉しいかったです。
Notionデータベースと紐付けられない点が惜しいところですが、自分はそこまでこだわりがないのでこのまま運用していきたいと思っております。
この記事が参考になれば幸いです。





![Microsoft Power BI [実践] 入門 ―― BI初心者でもすぐできる! リアルタイム分析・可視化の手引きとリファレンス](/assets/img/banner-power-bi.c9bd875.png)
![Microsoft Power Apps ローコード開発[実践]入門――ノンプログラマーにやさしいアプリ開発の手引きとリファレンス](/assets/img/banner-powerplatform-2.213ebee.png)
![Microsoft PowerPlatformローコード開発[活用]入門 ――現場で使える業務アプリのレシピ集](/assets/img/banner-powerplatform-1.a01c0c2.png)


