Cloudflare Pages + KVで実現する爆速CMS:実装記録と課題
どーも、ちょりんだです。
このブログのCMS機能開発が順調に進んでいます。Cloudflare Pages + KVという組み合わせで、従来のCMSとは異なるアプローチを試みています。
今回は、現在の開発状況、技術的な課題、そして今後の改善計画について詳しく解説します。
はじめに:なぜCloudflare Pages + KVを選んだか
従来のCMSはMySQLやPostgreSQLなどのリレーショナルデータベースを使用しますが、Cloudflare KVには以下の利点があります:
- 爆速なパフォーマンス: エッジロケーションにデータが分散
- スケーラビリティ: 自動的なスケーリング、運用不要
- コスト効率: 従量課金制、小規模サイトならほぼ無料
- グローバル配信: 世界中のユーザーに高速アクセス
ただし、KVには制限もあります。その制限をどう乗り越えるかが開発の鍵でした。
現在のアーキテクチャ
全体像
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Admin Panel │ │ Cloudflare │ │ Astro Site │
│ (Astro) │───▶│ Functions │───▶│ (Static) │
│ │ │ (API) │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Cloudflare KV │
│ (Key-Value) │
└─────────────────┘
データフロー
- 書き込み: Admin Panel → Functions → KV
- 読み込み: KV → Functions → Astro Site
- キャッシュ: エッジロケーションでの自動キャッシュ
実装の詳細
1. KVストレージの設計
KVは単純なKey-Valueストアなので、CMSのデータ構造を工夫する必要がありました。
// 記事データの構造
const postStructure = {
id: "post-123456",
slug: "my-blog-post",
data: {
title: "記事タイトル",
description: "記事説明",
pubDate: "2026-03-21",
updatedDate: "2026-03-21",
tags: ["技術", "Astro"],
status: "published"
},
body: "記事本文...",
metadata: {
author: "admin",
version: 1,
lastModified: "2026-03-21T10:00:00Z"
}
};
Key設計の戦略:
- 記事:
post:{id} - 設定:
config:{key} - インデックス:
index:{type}:{value}
2. APIエンドポイントの実装
Cloudflare FunctionsでRESTful APIを構築:
// src/pages/api/posts.js
export async function GET({ request }) {
try {
const postsStore = await getPostStore({ locals: globalThis });
const allKeys = await postsStore.list();
const allPosts = [];
for (const key of allKeys.keys) {
if (key.name.startsWith('post:')) {
const postJson = await postsStore.get(key.name);
if (postJson) {
const post = JSON.parse(postJson);
if (post.data.status === 'published') {
allPosts.push(post);
}
}
}
}
return new Response(JSON.stringify({
success: true,
data: allPosts
}), {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({
success: false,
error: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
3. 管理画面の構築
Astroで管理画面を実装:
---
// src/pages/admin/index.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
// 認証チェック
const authToken = Astro.cookies.get('auth_token')?.value;
if (!authToken) {
return Astro.redirect('/admin/login');
}
// 記事データの取得
const response = await fetch(`${import.meta.env.SITE}/api/posts`);
const result = await response.json();
const posts = result.success ? result.data : [];
---
<BaseLayout title="CMS管理画面">
<!-- 管理画面UI -->
</BaseLayout>
技術的な課題と解決策
1. KVの制限への対応
課題: KVには1つの値あたり25MB、1つのNamespaceあたり1GBの制限
解決策:
- 大きな記事は分割して保存
- 画像やメディアは別ストレージ(R2)に保存
- 古い記事はアーカイブ機能で移動
// 大きな記事の分割保存
async function saveLargePost(post) {
const chunkSize = 1000000; // 1MB
const chunks = [];
for (let i = 0; i < post.body.length; i += chunkSize) {
const chunk = post.body.slice(i, i + chunkSize);
chunks.push(chunk);
await postsStore.put(`post:${post.id}:chunk:${i}`, chunk);
}
// メタデータのみメインキーに保存
await postsStore.put(`post:${post.id}`, {
...post,
body: null,
chunks: chunks.length
});
}
2. 検索機能の実装
課題: KVはクエリ機能がないため、全文検索が難しい
解決策:
- インデックス用のKeyを別途作成
- タイトルとタグで部分検索を実装
- Pagefindのような静的検索ツールを併用
// 検索インデックスの作成
async function createSearchIndex(post) {
const keywords = [
...post.data.title.split(' '),
...post.data.tags,
// 本文からキーワード抽出(簡易版)
...post.body.split(' ').filter(word => word.length > 3)
];
for (const keyword of keywords) {
const indexKey = `search:${keyword.toLowerCase()}`;
const existing = await postsStore.get(indexKey);
const posts = existing ? JSON.parse(existing) : [];
if (!posts.includes(post.id)) {
posts.push(post.id);
await postsStore.put(indexKey, JSON.stringify(posts));
}
}
}
3. リアルタイム更新の課題
課題: KVの propagation delay(最大60秒)
解決策:
- 即時更新が必要なデータはDurable Objectsを使用
- UI側で楽観的更新を実装
- WebSocketでリアルタイム通知
// 楽観的更新の実装
async function updatePost(postId, updates) {
// まずUIを更新
updateUIOptimistically(updates);
try {
// KVに保存
await postsStore.put(`post:${postId}`, updates);
// 成功したら確定
confirmUpdate();
} catch (error) {
// 失敗したらロールバック
rollbackUpdate();
showError(error);
}
}
4. バージョン管理の実装
課題: KVは単純な上書きしかできない
解決策:
- バージョン番号で別Keyに保存
- 差分データで容量を節約
- 定期的な古いバージョンの削除
// バージョン管理の実装
async function savePostVersion(post, version) {
const versionKey = `post:${post.id}:version:${version}`;
await postsStore.put(versionKey, {
...post,
metadata: {
...post.metadata,
version,
savedAt: new Date().toISOString()
}
});
// 最新バージョンへの参照
await postsStore.put(`post:${post.id}:latest`, version);
}
パフォーマンスの最適化
1. キャッシュ戦略
// マルチレベルキャッシュ
const cacheStrategy = {
// エッジキャッシュ(KV)
edge: {
ttl: 3600, // 1時間
staleWhileRevalidate: 86400 // 1日
},
// ブラウザキャッシュ
browser: {
html: 300, // 5分
api: 60, // 1分
assets: 86400 // 1日
}
};
2. データの圧縮
// JSONデータの圧縮
import { gzip, ungzip } from 'pako';
async function compressData(data) {
const jsonString = JSON.stringify(data);
const compressed = gzip(jsonString);
return compressed;
}
async function decompressData(compressed) {
const decompressed = ungzip(compressed);
return JSON.parse(decompressed);
}
3. バッチ処理
// 複数のKV操作をバッチ化
async function batchOperations(operations) {
const promises = operations.map(op => {
switch (op.type) {
case 'put':
return postsStore.put(op.key, op.value);
case 'get':
return postsStore.get(op.key);
case 'delete':
return postsStore.delete(op.key);
}
});
return Promise.all(promises);
}
開発体験の改善
1. ローカル開発環境
// ローカルKVシミュレーター
class LocalKVSimulator {
constructor() {
this.store = new Map();
}
async get(key) {
return this.store.get(key) || null;
}
async put(key, value) {
this.store.set(key, value);
}
async list() {
const keys = Array.from(this.store.keys())
.filter(key => key.startsWith('post:'))
.map(name => ({ name }));
return { keys };
}
}
2. デバッグツール
// KV操作のログ
function createKVLogger(kv) {
return new Proxy(kv, {
get(target, prop) {
if (typeof target[prop] === 'function') {
return function(...args) {
console.log(`KV.${prop}:`, args);
return target[prop].apply(target, args);
};
}
return target[prop];
}
});
}
3. テスト環境
// KVのテストユーティリティ
class KVTestHelper {
constructor() {
this.testData = new Map();
}
async setupTestData(posts) {
for (const post of posts) {
this.testData.set(`post:${post.id}`, JSON.stringify(post));
}
}
async cleanup() {
this.testData.clear();
}
}
今後の改善計画
短期的改善(1ヶ月)
-
検索機能の強化
- 全文検索エンジンの導入
- ファセット検索の実装
- 検索結果のランキング
-
UI/UXの改善
- リッチテキストエディタの導入
- ドラッグ&ドロップでの画像アップロード
- プレビュー機能のリアルタイム化
-
パフォーマンス最適化
- データの事前読み込み
- インクリメンタル更新
- オフライン対応
中期的改善(3ヶ月)
-
機能拡張
- マルチユーザー対応
- 権限管理システム
- コメント機能
-
データ管理
- 自動バックアップ機能
- データエクスポート/インポート
- バージョン管理の強化
-
インテグレーション
- 外部API連携
- Webhook対応
- サードパーティーツール連携
長期的改善(6ヶ月)
-
スケーラビリティ
- マルチリージョン対応
- 負荷分散
- パフォーマンス監視
-
高度な機能
- A/Bテスト機能
- パーソナライズ機能
- 分析ダッシュボード
まとめ
Cloudflare Pages + KVでのCMS開発は、従来のアプローチとは異なる課題がありますが、適切な設計と実装で十分に実用可能です。
現在の成果:
- ✅ 基本的なCRUD機能の実装
- ✅ 管理画面の構築
- ✅ 認証システムの導入
- ✅ パフォーマンスの最適化
- ✅ 開発環境の整備
残された課題:
- 🔄 検索機能の強化
- 🔄 リアルタイム更新の改善
- 🔄 大規模データへの対応
- 🔄 高度なCMS機能
このCMSは、小規模〜中規模のブログやサイトに最適なソリューションになる可能性を秘めています。KVの制限を工夫で乗り越えながら、より良いCMSを目指して開発を続けていきます。
関連記事: CMS開発進捗:Cloudflare KVでのデータ永続化に挑戦中
タグ: #CMS #Cloudflare #Astro #KV