
この記事は、Cloudflare D1で個人開発向けCMSを作りたい人に向けて書いています。特に、記事をDBに入れるだけでなく、下書き、予約投稿、公開済み、noindex、レビュー待ちをどう分けるかで迷っている人のための実装メモです。
このサイトでは、記事本文だけでなく公開制御もD1に寄せています。単なるD1紹介ではなく、実際のmigrationとWorkerコードをもとに、公開される記事をどう絞っているかを整理します。

解決したかった問題
最初に困るのは、管理画面では見たい記事と、読者に公開してよい記事が一致しないことです。下書き、予約中、公開済み、削除済み、検索に出したくない記事、事実確認が残る記事を同じ一覧に混ぜると、RSSやsitemapに余計なURLが入りやすくなります。
そのため、このCMSでは記事を削除して整理するのではなく、DBの列で状態を分けます。審査中に公開しない記事を隠す場合も、データを消すのではなく公開条件から外します。
postsテーブルに持たせた状態
初期のpostsテーブルには、タイトル、slug、本文HTML、ステータス、予約日時、公開日時がありました。その後、AdSense審査用モードへ切り替えるために、抜粋、サムネイル、noindex、content_type、review_statusを追加しています。
ALTER TABLE posts ADD COLUMN review_status TEXT NOT NULL DEFAULT reviewed;
status: draft / scheduled / published / private / deleted
content_type: technical / case_study / worklog / legacy
review_status: reviewed / draft_review / fact_check_requiredここで重要なのは、status と review_status を分けたことです。記事としては公開済みでも、本文に確認待ちが残っているなら fact_check_required にできます。noindexとは別の、編集上の安全弁です。
Worker側の公開条件
公開一覧、検索、RSS、sitemapでは同じ条件を使います。実装上は src/index.ts の publicPublishedPostConditions に寄せています。
function publicPublishedPostConditions(env) {
const contentTypes = publicContentTypes(env);
return [
"status = published",
"deleted_at IS NULL",
"COALESCE(noindex, 0) = 0",
"COALESCE(review_status, reviewed) = reviewed",
"COALESCE(content_type, legacy) IN (...)"
];
}記事一覧だけで条件を変えると、記事詳細では見えるのにsitemapへ出ない、RSSには出るのに検索では出ない、というズレが起きます。ここは最初から共通化しておく方が後で楽でした。
予約投稿の流れ
- 管理画面で記事を作り、ステータスを
scheduledにする scheduled_atに投稿予定時刻を入れる- Cloudflare Cronが定期的にWorkerを起動する
publishDuePostsが予定時刻を過ぎた記事をpublishedに変えるpublished_atを入れ、一覧やRSSの並び順に使う
この方式にしておくと、管理画面から手動公開しても、予約公開しても、最終的に公開条件は同じになります。
失敗しやすい判断
一番危ないのは、公開したくない記事を noindex だけで管理することです。noindexは検索エンジン向けの指示であって、サイト内一覧やRSSから消す条件ではありません。このCMSでは、noindex記事をRSSとsitemapからも除外するようにしています。
もうひとつは、AdSense審査中の記事タイプを blog のような広い分類で出すことです。今回は technical、case_study、worklog だけを公開対象にし、審査対象外の記事タイプは外しました。
同じ構成を作る手順
- postsテーブルに
status、noindex、content_type、review_statusを持たせる - 公開記事を取得するSQL条件を1か所にまとめる
- RSSとsitemapも同じ条件を使う
- 管理画面からcontent_typeとreview_statusを変更できるようにする
- 未確認記事は削除せず
fact_check_requiredで止める
D1は、こうした小さなCMSの状態管理に向いています。ただし、画像、動画、大量のアクセスログまで全部D1に寄せる必要はありません。このサイトでは、記事本文と公開制御をD1、画像はWorkers Assetsという役割分担にしています。