はじめに
2025年5月、ヘッドレスCMSのNewtがサービスの終了予定を発表しました。2026年11月24日をもって全サービスが停止される予定です。
本ブログはNewtで記事を管理していたため移行先が必要になりましたが、microCMS等への乗り換えではなく自作を選びました。AI時代の今なら、ブログ管理システムとエディターくらい作れるだろうという判断です。
本ブログはフロントエンドも含めて全てRustで構築しています(フレームワークはLeptos、技術構成の詳細は過去の記事をご覧ください)。
本記事では、移行にあたってのインフラ構成、エディター実装、CDNキャッシュ戦略、データ移行について紹介します。
インフラ構成
Newtの代わりに、記事データはPostgreSQLで管理することにしました。セッション管理にはValkey(Redis互換)を利用しています。
画像管理
Newtには画像をGCSにアップロードして管理する機能があり、そのBucketにイメージオプティマイザサービスのimgixを接続していました。移行後もこのBucketとimgixをそのまま継続利用し、アプリケーションから直接GCSにアップロードする形に変更しています。
認証
自作のエディター画面を設けたので、認証が必要になりました。管理画面はGoogleログインで保護しています。Google Workspaceを使っていないので組織内アカウント制限ができず、アプリケーション側でメールアドレスをチェックしています。
コスト
Newtはフリー枠で使っていました。移行後のPostgreSQL・ValkeyもOCI Always Free枠内で動かしているので、コスト0のままです。
マークダウンエディター
Newtがマークダウン記法だったので、既存の記事をコピペで移行できるようにマークダウンエディターを実装しました。DBにはマークダウンのまま保存して、表示時にHTMLへ変換しています。
マークダウンパーサーの選定
マークダウンからHTMLへの変換クレートを選定するにあたり、Rustで利用可能な主要クレートを比較しました。
| comrak | pulldown-cmark | markdown-rs | |
|---|---|---|---|
| CommonMark準拠 | 652/652 (100%) | 100%目標 | 100% (2,300+テスト) |
| GFM準拠 | 670/670 (100%) | table, strikethrough, tasklist, footnotes(オプション) | table, strikethrough, tasklist, autolink, footnotes |
| 追加拡張 | superscript, description lists, math, wikilinks, emoji | なし | MDX, math, frontmatter |
| アーキテクチャ | AST構築(typed-arena) | プルパーサー(イテレータ) | ステートマシン + AST(mdast) |
| 速度 | 正確性優先 | 高速、メモリ効率◎、SIMD対応 | - |
| WASM | comrak-wasm あり | no_std対応 | 言及なし |
| 採用実績 | crates.io, docs.rs, GitLab | 広く利用 | - |
結果としてcomrakを採用しました。決め手はGFM 100%準拠です。comrakはcmark-gfm(GitHubの公式Cパーサー)のRustポートで、GFM仕様に対する670/670のテストをパスしています。crates.io・docs.rs・GitLabで使われている実績も安心材料でした。
pulldown-cmarkもGFMの一部拡張をオプションでサポートしていますが、仕様全体への準拠は保証していません。速度面ではpulldown-cmarkに分がありますが、ブログ記事程度の文量であれば差は体感できません。markdown-rsはMDXやmath対応が魅力的でしたが、WASMでの利用実績が確認できなかったため今回は見送りました。
comrakでは以下のGFM拡張を有効にしています。
- strikethrough(打ち消し線)
- table(テーブル)
- autolink(URL自動リンク)
- tasklist(チェックリスト)
プレビュー
エディターには3つのビューモードがあります。Split(左右並列表示)、Editor(エディターのみ)、Preview(プレビューのみ)。
マークダウンからHTMLへの変換はクライアントサイド(WASM)で実行しています。comrakはWASMバインディング(comrak-wasm)が提供されており、サーバー往復なしにブラウザ上で変換が完結します。WASMはネイティブコードに近い速度が出ると言われており、実際ブログ記事程度の文量なら入力に追従してプレビューがリアルタイムに更新されます。エディターとプレビューのスクロール同期も実装しています。
画像アップロード
画像管理にはNewtで使っていたGCSバケットとimgixをそのまま使い回しています。サーバーで署名付きURLだけ発行して、実ファイルはブラウザからGCSへ直送しています。
ドラッグ&ドロップかファイル選択で画像を挿入すると、カーソル位置にimgixのレスポンシブ対応<img>タグが自動挿入されます。imgixのURL変換パラメータで800px・1200px・1920pxの3段階のsrcsetを生成しています。
<img
src="https://example.imgix.net/photo.jpg"
srcset="https://example.imgix.net/photo.jpg?w=800&auto=format&q=75 800w,
https://example.imgix.net/photo.jpg?w=1200&auto=format&q=75 1200w,
https://example.imgix.net/photo.jpg?w=1920&auto=format&q=75 1920w"
sizes="(max-width: 800px) 800px, (max-width: 1200px) 1200px, 1920px"
/>
auto=formatでブラウザに応じてWebPやAVIFを自動選択し、q=75で品質を調整しています。元画像を1枚アップロードすれば、imgixのCDN側で画面幅に応じた画像を生成・配信してくれます。管理画面のサムネイルはw=200&h=200&fit=cropで正方形にクロップするなど、用途で使い分けています。記事へのタグ設定も実装しており、テキスト入力で既存の候補から選択でき、未登録のタグは保存時に自動作成されます。
Cloudflare Cache-Tag
本ブログはCloudflare CDNでキャッシュしているので、記事を更新したらキャッシュパージが必要です。Newt時代はWebhookをトリガーにGitHub Actions経由でCloudflare APIを叩いていました。
Freeプランの制約
本ブログの開発当初(2024年頃)、Cloudflare Freeプランで使えるパージ方法はURL指定の単一パージか全体パージの2択でした。全体パージは関係ない記事のキャッシュまで飛ぶので無駄ですし、URL単一パージは対象ページを1つずつ列挙しないといけない。記事ページだけでなくトップページやsitemapも対象になるので、地味に面倒でした。
Cache-TagのFreeプラン開放
ところが2025年4月、Cloudflareが「Instant Purge for All」としてCache-Tag(Purge by Tag)を含む全パージ方法をFreeプランに開放しました。Cache-Tagは元々2015年にEnterprise専用として導入された機能で、約10年間上位プラン限定だったものです。
レスポンスヘッダーにCache-Tag: tag1,tag2を付けておくと、あとからタグ指定でまとめてパージできます。URL一覧を組み立てるコードが要らなくなります。
実装
Cache-TagがFreeプランで使えるようになっていたので、今回の移行で採用しました。各ページのレスポンスにCache-Tagを付与しています。
- トップページ:
top-page - 記事ページ:
article:{slug} - sitemap:
sitemap
記事を更新したらarticle:{slug}とtop-pageをパージして、その記事ページとトップページのキャッシュだけ消します。Freeプランのレート制限は5リクエスト/分ですが、個人ブログの更新頻度なら問題ないと判断しました。
Newtからの移行戦略
データ移行
既存の記事はNewtからエクスポートしてPostgreSQLに投入しました。Newtがマークダウンで記事を管理していたので、本文データはそのままDBに入れられました。マークダウンエディターを自作した理由の一つもここにあって、既存記事をコピペで移行できる形式を維持したかったからです。
リダイレクト
Newt時代の記事URLと新しいURLが異なるので、旧URLからのリダイレクトが必要でした。対象は4記事だけだったので、Newt記事IDと新DB記事slugのマッピングをアプリ内にベタ書きしています。SSRは301リダイレクト、SPA遷移はreplaceで処理しています。301にしておけば検索エンジンのインデックスが新URLに引き継がれます。
段階的リリース
移行はfeatures=localというCookieトグルを使い、旧Newt表示と新DB表示を切り替えながら段階的にリリースしました。個人ブログレベルなので一気にリリースしても問題はなかったのですが、Rustブログとしてそこそこ検索に引っかかるようになっていたので、切り替え時の不具合でインデックスを失いたくなかったためです。動作確認が済んだ段階でトグルを撤去し、DB記事をデフォルト表示に切り替えました。
Newtの管理画面と、移行後の自作管理画面の比較です。
Before: Newt CMS管理画面
After: 自作管理画面
開発規模とAIとの協業
今回の移行の規模感です。
- 開発期間: 約20日間(2026年1月25日 〜 2月14日)
- PR数: 31件マージ(#170〜#202)
- コミット数: 88コミット
- コード差分: Rustコード +4,518行 / -608行(純増 +3,910行)、SCSS +583行を含め全体で90ファイル変更
- 1日あたり約1.6PR、4.4コミットのペース
主な追加領域の内訳です。
| 領域 | 追加行数 |
|---|---|
| 管理画面テスト | +605行 |
| 画像管理 | +415行 |
| カテゴリ管理 | +218行 |
| CDNキャッシュパージ | +160行 |
| 認証 | +101行 |
| SEO | +91行 |
開発はAIと進めました。フロントエンドRustというニッチな領域でAIがどこまで使えるか不安でしたが、Leptosのコードは普通に書けました。コンパイラのエラーメッセージが丁寧なので、AIに投げ返すとだいたい直してくれます。型が厳格で「コンパイルが通れば動く」言語なので、AIが生成したコードの品質をコンパイラが担保してくれる面があります。動的型付けの言語なら実行時に初めて気づくような型の不一致やnullアクセスも、Rustではコンパイル時に弾かれます。もちろんロジックのバグはコンパイラでは防げませんが、型レベルの不整合はAI任せでも安心できました。
20日間でRust約4,500行のフルスタック自作ブログ管理システムを1人+AIで開発できたのは、なかなかいい体験でした。
まとめ
ヘッドレスCMSのサービス終了をきっかけに、自作ブログ管理システムへ移行しました。AIを使って、約20日間・Rust約4,500行で実現できています。
自作のメリットは自由度です。Cache-Tagパージ、WASMプレビュー、署名付きURLでの画像アップロード。欲しい機能を好きなタイミングで追加できます。一方、メンテナンスは全部自分持ちです。ヘッドレスCMSなら勝手にやってくれていた可用性やセキュリティアップデートを、自前で面倒見ないといけません。
コストはOCI Always Free枠で0円を維持できています。メンテナンスは自分持ちになりますが、今の時代AIに聞けばだいたいなんとかなるので、そこまで心配していません。個人開発者で技術的な自由度を求めるなら、AI時代の今は自作も十分ありだと思います。
