HAKUYUCHIAI導入・業務自動化の支援

記事の正本を Git に置く — 公開=マージにすると何が楽になるか

悠 Yu NiiboriAIエンジニア運営者について

AIに記事を書かせるメディアで CMS を持たず、記事の唯一の正本を Git のファイルに置いた。公開=PRのマージにすると、履歴・レビュー・差し戻しがコードと同じ機構に乗る。実際に運用しているシステムの実装と、失ったもの・まだ繋がっていない部分を公開する。

目次
  1. 記事をコードとして扱うとは
  2. なぜ CMS を持たなかったのか
  3. 実装 — ビルド時にバンドルへ埋め込む
  4. 検証は CI に置く — スキーマは「厳格に」定義する
  5. fail-closed と fail-open を場所で使い分ける
  6. 失ったもの(正直に)
  7. データベースを完全に捨てたわけではない
  8. FAQ
  9. まとめ

記事をコードとして扱うとは

記事をコードとして扱う(articles as code)とは、記事の唯一の正本を Git リポジトリのファイルに置き、公開という行為を Pull Request のマージに一致させる運用です。

管理画面から本文を保存してデータベースに入れる、という経路を作りません。記事は content/articles/<slug>.md というただのファイルで、frontmatter(YAML)にタイトル・分類・出典を持ちます。編集は差分、レビューは PR レビュー、公開はマージ、取り消しは revert になります。

このメディアはそう作ってあります。以下は理想論ではなく実在するコードの話で、まだ繋がっていない一手についても隠さずに書きます。

なぜ CMS を持たなかったのか

もともとの設計では、本文をオブジェクトストレージに保存し、版管理テーブルと承認テーブルを自前で持つつもりでした。やめた理由は3つあります。

1. 承認の機構がすでにありました。 このシステムは開発自体を AI に回させています。実装・レビュー・取り込みを別々のAIが PR を介して行う仕組みが先にあり、人手ゼロで一周することが確認できていました。記事をそこに乗せれば新しい機構は要りません。

2. 履歴が無料で手に入ります。 全版・差分・誰がいつ何をなぜ変えたか・任意の時点への復元は、git がすでに持っています。自前の版管理テーブルは、それを劣化させて作り直す作業になります。

3. ブロッカーが2つ消えました。 オブジェクトストレージの有効化と API キー発行が、公開開始の前提ではなくなりました。そもそも記事本文は数十KB のテキストで、blob ストアを持ち込む必然性は薄いところです。

実装 — ビルド時にバンドルへ埋め込む

公開サイトは Cloudflare Workers 上で動きます。記事の配信経路はこうなっています。

  1. content/articles/*.md が正本
  2. ビルド時に、ビルドスクリプトが全ファイルを読んで RAW_ARTICLES(ファイル名 → 生文字列の Record)という生成物にする
  3. 公開サイトの Worker がその文字列を実行時に解釈して配信する
  4. マージすると Workers Builds が再ビルドし、記事が載る(この4番目だけが、執筆時点ではまだ接続されていない — 後述)

Cloudflare の公式ドキュメントは Workers Builds をこう説明しています。

The Cloudflare Git integration lets you connect a new or existing Worker to a GitHub or GitLab repository, enabling automated builds and deployments for your Worker on push.

つまり「マージ = デプロイ」は追加の CI を書かずに手に入ります。

ただし執筆時点で、この接続は完了していません。 上の 1〜3 は動いていて 4 だけが繋がっておらず、唯一のデプロイは手元からの手動実行です。この記事が説明している「公開 = マージ」は、この記事自身にまだ適用されていません。

未接続の理由は設計ではなく手順の順序で、一部リソースがアカウント側で未有効化のため、それを済ませてから接続する段取りにしています。書いておくべきなのは、この状態が約2時間気づかれなかったことのほうです。最後に本番へ反映されたのが 12:47 UTC、気づいたのが 14:39 UTC。その間にマージされた記事1本とサイト修正1本が本番に出ていなかった(この時刻は本システムの運用記録によるもので、開発リポジトリが非公開のため読者が直接確認できる出典は示せません)。

記事はマージされ、CI は緑で、リポジトリの上では公開が完了しているように見えます。公開の成否をリポジトリの状態でしか確認していなければ、この抜け方は見えません。 マージとデプロイを一致させる構成は、一致していることを別経路で確認して初めて完成します。監視する対象は、デプロイ履歴の最終時刻と main の HEAD が一致しているか — その1点で足ります。

意図的に決めたことが1つあります。ビルドスクリプトは「ファイルを読んで文字列にする」以上のことをしません。 frontmatter の解釈も検証も公開サイト側の1本のモジュールに集約し、CI のスキーマ検証も本番と同じコードを通します。ビルド時(Node)と実行時(Workers)で実装が分かれると、CI が通した記事を本番が別解釈する余地が生まれるからです。

なお公開サイトの Worker にはデータベースをバインドしていません。記事が Git にある以上、公開側からデータストアへ到達する経路をそもそも作らない、という切り方ができます。

検証は CI に置く — スキーマは「厳格に」定義する

AI が frontmatter を埋めるので、人間のレビューの手前で機械が落とすほうが速く済みます。スキーマで実際に効いているのは次の点です。

検査落とす対象なぜ
未知キーの拒否medical_risks: のような綴り違い「任意項目」として黙って通ると分類が消える
ファイル名 = slugicu-origin.mdslug: icuURL とファイルの対応が崩れる
sources 最低1件出典ゼロの記事出典のない断定を公開しないため
medical_risk の語彙固定low/medium/high 以外公開の承認フローが分類で分岐する
出典 URL は http/https のみjavascript:data:出典リンクがスクリプト実行経路になる

最後の項目は、記事スキーマがセキュリティ境界を兼ねている例です。一般的な URL バリデータには javascript: を通すものがあり、frontmatter を埋めるのは外部ページを読む AI で、同一オリジンには問い合わせフォームがあります。目視ではなく CI で落とし、描画側でも二重に絞っています。

現在このリポジトリのテストは 76 件が通ります。記事のスキーマ検証はそのうちの1ファイルで、content/articles/ の実ファイルを1本ずつ読みます。壊れた記事はマージできません。

記事から出す構造化データの検査は本文との一致を機械で保つ検査の作り方で扱っています。

fail-closed と fail-open を場所で使い分ける

同じ「壊れた記事」に対して、CI と本番は逆の振る舞いをします。

  • CI: 1本でも壊れていたら落とす(マージさせない)
  • 本番: 壊れた1本だけを配信対象から外し、理由をログに出して他は配信する

損害の大きさが逆だからです。マージ前は止めるコストが安く、本番では1本の不備で全ページが 500 になるほうがはるかに高くつきます。「安全側」がどちらを指すかは場所によって変わります。

失ったもの(正直に)

公開レイテンシ。 レビューと取り込みが定時のループで回るため、書き上がってから公開まで最大1時間強かかる見込みである(ループの起動間隔からの見積もりで、自動デプロイが未接続の現状では実測が取れていません)。1日数本の運用では問題にならないが、速報メディアには向きません。

非エンジニアの編集体験。 管理画面から本文を直せません。書き手が AI で、レビューが PR である前提だから成立しています。人間の編集者が日常的に触る運用なら摩擦になります。

本文のランタイム A/B。 本文が静的にバンドルされるので出し分けができません。タイトル・CTA・導線の出し分けは配信時の上書きで行い、本文の比較は版のマージで行う設計にしました。

運用のハマりどころも1つあります。 ビルドコマンドの実行ディレクトリは、設定ファイルの場所ではなくプロセスのカレントディレクトリ基準でした。ローカルの型チェックもテストも素通りし、デプロイのビルドで初めて落ちる種類の誤りです。設定を変えたら、デプロイの手前で dry-run を通すこと。

データベースを完全に捨てたわけではない

記事の正本は Git だが、運用データ(計測・リード・実験・監査ログ)はデータベースに置いています。Cloudflare の D1 は公式ドキュメントの説明では「SQLite の SQL セマンティクスを持つマネージドのサーバーレスデータベース」です。

境界はこう引きました。人が読むために書かれ、履歴とレビューが要るもの(記事・仕様・意思決定)は Git。機械が書き、時系列で増えるもの(計測・監査)はデータベース。

FAQ

Q. 記事を Git に置くと、非エンジニアは書けなくなりますか。 日常的に人間が編集する運用なら摩擦になります。書き手が AI で、レビューが PR である前提なら成立します。

Q. 公開の取り消しはどうしますか。 該当コミットを revert してマージします。復元も差し戻しも git の機能で、専用機構を作る必要はありません。

Q. データベースを使う構成と比べて何が一番違いますか。 承認フローを自作しなくてよいことです。レビューと履歴と復元が、コードと同じ道具で済みます。

まとめ

CMS を持たない選択の本質は、保管場所の話ではなく承認の場所の話です。公開をマージに一致させると、レビュー・履歴・差し戻しが既存の機構に乗り、AI に書かせる前提なら人間が見る場所を1か所(PR)に集められます。一方で、公開の速さと非エンジニアの編集体験は確実に失います。速報性や編集部体制が要る媒体にはお勧めしません。

AI に書かせる仕組みを作るとき、難しいのは生成そのものではなく、検証と差し戻しの経路を先に用意することでした。同じ構成の設計・実装のご相談を承っています。

出典

  1. Cloudflare Docs(公式ドキュメントのソース)— Workers Builds(参照 2026-08-09)
  2. Cloudflare Docs(公式ドキュメントのソース)— D1 overview(参照 2026-08-09)