はじめに
WordPressで運営していたブログを、Astro + Cloudflare Pagesへ移行した。
移行によってサイトの表示や管理は軽快になった一方で、新たに気になり始めたのが記事の執筆環境だった。WordPressにはブラウザ上で使える専用エディターがあったが、Astroでは基本的にMarkdownやMDXファイルを直接編集することになる。
そこで、記事の編集から画像処理、Gitへのコミット・プッシュまでを一つの画面で完結させるデスクトップアプリケーション「Fokus Editor」を開発した。
この記事では、Fokus Editorを作ることになった経緯、現在の技術構成、実装した機能、そしてAI駆動開発で直面した問題についてまとめる。内容は2026年8月時点の開発版に基づいている。
開発の経緯
ブログの移行作業を始めた当初は、開発に使っていたCursorで記事も編集するつもりだった。
しかし、開発と執筆を同じツールで行うと、どうしても作業の切り替えが曖昧になる。コードを書く環境としては快適でも、文章を書くための道具として気分が上がるかというと、少し違った。
WordPressのリッチなエディターに慣れていたこともあり、ソースコードと同じ画面で記事を書くことには、機能面でも見た目の面でも物足りなさがあった。
そこで、以前から愛用していたiA Writerなどの外部エディターも検討した。
AstroはMarkdownに標準対応しており、MDXインテグレーションを追加すれば、Markdown内でコンポーネントや式も扱える。また、タイトル、公開日時、説明文、タグなどの情報は、ファイル先頭のFrontmatterに記述できる。Astroの公式ドキュメントでも、この構成が案内されている。
ただし、一般的な文章エディターでFrontmatterまで編集しようとすると、YAMLの記法を常に意識しなければならない。インデントや引用符を崩すと、記事のビルド自体が失敗する可能性もある。
さらに、記事を公開するには次の作業が必要だった。
- MDXファイルを保存する
- 画像を所定の場所へ配置する
- Gitで変更を確認する
- コミットする
- リモートリポジトリへプッシュする
- Cloudflare Pagesのデプロイを待つ
これらを毎回別々のツールで行うのではなく、一つのアプリケーションにまとめたいと考えたのが、Fokus Editor開発の出発点である。
Fokus Editorで実現したかったこと
開発時に設定した主な要件は、次のとおり。
- MDX記事の作成・編集・保存
- Frontmatterをフォームから安全に編集
- Markdown記法に対応したショートカット
- リアルタイムプレビュー
- 画像のドラッグ&ドロップ
- Gitの変更確認、コミット、プッシュ
- コミットメッセージの自動入力
- R2への画像アップロード
- WebPへの自動変換
- ライトモードとダークモード
- 記事リポジトリと記事ディレクトリの切り替え
「汎用的な開発エディター」ではなく、自分のAstroブログの記事を書くことに特化したアプリを目指した。
技術選定
PyQt6か、Tauriか
最初に検討したのは、次の2案だった。
| 選択肢 | メリット | デメリット |
|---|---|---|
| PyQt6 | Python中心で構築でき、試作が速い | Web技術を使ったUIと比べてデザイン調整の自由度が低い |
| Tauri + React | Web技術でUIを作れ、デスクトップアプリとして配布できる | TypeScriptとRustの両方を扱う必要がある |
初期の設計資料では、ReactとRustの間にFastAPIを置く構成も検討していた。しかし、ローカルファイルの読み書きやGit操作はTauriのRust側で完結できるため、最終的にFastAPIは採用しなかった。
現在の構成は、ReactからTauri Commandsを呼び出し、Rust側でファイル操作やGit処理を実行する形になっている。Tauriには、フロントエンドからRust関数を呼び出すためのCommand機構が標準で用意されている。Tauri公式ドキュメントでも、invokeを使った同様の構成が紹介されている。
現在の技術構成
| 技術 | 役割 |
|---|---|
| Tauri 2 | デスクトップアプリの基盤 |
| React + TypeScript | ユーザーインターフェース |
| Vite | フロントエンドの開発・ビルド |
| Rust | ファイル操作、Git操作、URLメタデータ取得 |
| Monaco Editor | MDX本文の編集 |
| react-markdown + remark-gfm | リアルタイムプレビュー |
| gray_matter + serde_yaml | Frontmatterの解析と保存 |
| git2 | Gitの状態取得とコミット |
| Sharp + AWS SDK | WebP変換とR2アップロード |
Phase 1:MVPの開発
プロジェクトの作成
Tauriプロジェクトは、公式の案内と同じくcreate-tauri-appから作成した。
npm create tauri-app@latest fokus-mdx-editor
このコマンドは現在もTauri公式ドキュメントで案内されている。Create a Project
最初の目標は、「記事を一覧から選び、編集して保存できる」状態にすることだった。
実装した基本機能は次のとおり。
- MDX記事の一覧表示
- 新規記事の作成
- 既存記事の読み込み
- 記事の保存・削除
- スラッグ変更に伴うファイル名変更
- Frontmatterと本文の分離
- リポジトリパスと記事パスの設定
Monaco Editorの採用
本文エディターにはMonaco Editorを採用した。
Monaco EditorはVS Codeを支えるコードエディターで、VS Codeのソースから生成されている。Monaco Editor公式リポジトリ
MDXの編集に必要なシンタックスハイライトに加え、次のショートカットを実装した。
Cmd + B:太字Cmd + I:斜体Cmd + D:取り消し線Cmd + K:リンクCmd + 1〜6:見出しCmd + L:箇条書きCmd + Shift + L:番号付きリストCmd + Shift + C:コードブロックCmd + ':引用Cmd + S:保存Cmd + N:新規記事Cmd + + / -:文字サイズ変更
箇条書き、番号付きリスト、引用では、Enterキーを押したときに次の行へ記号を自動継続する機能も追加した。
Frontmatterをフォーム化
Frontmatterは本文から分離し、専用のフォームで編集できるようにした。
現在は主に次の項目を扱える。
- タイトル
- スラッグ
- 著者
- 公開日時
- 下書き
- おすすめ記事
- タグ
- 説明文
- リード文
- OGP画像
- canonical URL
- 編集リンクの表示・非表示
- タイムゾーン
YAMLを直接編集する必要がなくなり、記法を壊す心配も大幅に減った。
Phase 2:Git連携
次に実装したのが、Git操作をアプリ内で完結させる機能である。
Fokus Editorでは、設定したリポジトリの現在のブランチや変更ファイルを検出し、サイドバーのGitパネルに表示する。
対応している主な操作は次のとおり。
- 追加・変更・削除された記事と画像の検出
- コミット対象ファイルの一覧表示
- コミットメッセージの入力
- コミット
- リモートリポジトリへのプッシュ
- ローカルブランチがリモートより進んでいるコミット数の表示
変更ファイルに応じて、コミットメッセージも自動入力される。
add: new-article.mdx
update: existing-article.mdx
必要であれば、コミット前に手動で書き換えることもできる。
Fokus Editorのプッシュ先は、対象リポジトリに設定されたoriginである。アプリ自体がCloudflareのAPIを直接呼び出しているわけではない。
Cloudflare PagesとGitHubリポジトリを連携している場合は、対象ブランチへのプッシュをきっかけに自動デプロイが開始される。Cloudflare PagesのGit連携
つまり、Fokus Editor上では「保存、コミット、プッシュ」までを行い、その先のビルドと公開はCloudflare Pagesに任せる構成である。
Phase 3:画像処理の自動化
記事編集で特に手間がかかっていたのが、画像の管理だった。
Fokus Editorでは、画像ファイルをMonaco Editorへドラッグ&ドロップすると、記事のスラッグに対応するディレクトリへ保存し、Markdownの画像記法を自動挿入する。
https://pub-0775e09aef814c42bd2da63d4c64076a.r2.dev/images/記事スラッグ/画像ファイル.webp
コミット時には、ブログ側のスクリプトを順番に実行する。
wp:upload-imageswp:convert-image-paths- Gitへのステージングとコミット
wp:upload-imagesでは、Sharpを使ってJPEG、PNG、WebP画像を品質80のWebPへ変換する。その後、最大10件ずつ並列でCloudflare R2へアップロードする。すでに同じキーの画像がR2に存在する場合は、再アップロードしない。
R2はS3互換APIを提供しているため、アップロードにはAWS SDKを利用できる。Cloudflare R2のS3互換API
アップロード後は、MDX内のローカルパスをR2のWebP URLへ変換する。
https://pub-0775e09aef814c42bd2da63d4c64076a.r2.dev/images/article/photo.webp
https://R2の公開URL/images/article/photo.webp
これにより、記事を書く段階ではローカル画像を扱い、公開時にはR2上のWebP画像へ自動的に切り替えられる。
R2の認証情報はブログリポジトリの.envから読み込む。Access KeyやSecret Access KeyをGitへコミットしないことは、この仕組みを運用するうえで特に重要である。
Phase 4:プレビューの改善
ローカル画像とR2画像の両方に対応
プレビューにはreact-markdownとremark-gfmを使用している。
当初は文章や見出しは表示できたものの、ローカル画像をプレビューできなかった。TauriのWebViewからローカルファイルを直接参照できないことと、Content Security Policyの制限が原因だった。
そこで、設定したリポジトリ配下だけをTauriのAsset Protocolで読み込み可能にし、画像パスをWebView用のURLへ変換する処理を追加した。
現在は、次の画像を同じプレビュー画面で表示できる。
https://pub-0775e09aef814c42bd2da63d4c64076a.r2.dev/images/から始まるローカル画像.webp- 記事ファイルからの相対パス
- R2へ変換済みのHTTPS画像
- data URLやAsset Protocolの画像
これにより、R2へアップロードする前と後のどちらでも、画像を確認しながら執筆できるようになった。
URLのリンクカード表示
現在の開発版では、単独行にURLを貼り付けると、ブログと同じ形式のリンクカードとして表示される。
カードには次の情報を表示する。
- ページタイトル
- ホスト名
- favicon
- 外部リンクアイコン
WebViewから外部サイトへ直接アクセスするとCORSの影響を受けるため、メタデータの取得はRust側で行う。取得にはタイムアウトとレスポンスサイズの上限を設け、localhostやプライベートIPへのアクセスは拒否する。
メタデータ取得に失敗した場合も、URLとホスト名を使った簡易カードへフォールバックする。文章中に含まれるURLはカード化せず、通常のインラインリンクとして表示する。
なお、このプレビューはMarkdownとGitHub Flavored Markdownを中心とした簡易プレビューであり、Astroのビルド環境そのものではない。MDX内に埋め込んだすべてのAstroコンポーネントを完全に再現するものではない点は、今後の課題である。
開発中に苦労した問題
Monaco Editorが本番環境で読み込まれない
開発環境では正常に表示されていたMonaco Editorが、本番ビルドでは「Loading…」のまま停止する問題が発生した。
原因は、Monaco Editorが外部のCDNからリソースを読み込もうとし、TauriのContent Security Policyにブロックされていたことだった。
Monacoをローカルバンドルから読み込むように設定して解決した。
import { loader } from "@monaco-editor/react";
import * as monaco from "monaco-editor";
loader.config({ monaco });
デスクトップアプリでは、開発サーバー上で動くことだけでなく、バンドル後のリソース配置やCSPまで含めて確認する必要がある。
公開日時がずれる
FrontmatterのpubDatetimeでは、日本時間で入力した日時が意図しないUTC日時へ変換される問題があった。
原因は、画面表示用のローカル時間と、ファイルへ保存するUTC時刻の変換を同じ方向に処理していたことだった。
現在は、入力欄ではローカル日時として表示し、保存時にtoISOString()でUTCへ変換している。
日時処理は一見単純に見えるが、表示用の時間、保存用の時間、記事側のタイムゾーンを分けて考えなければならない。
コミット中にUIが固まる
画像が増えると、R2へのアップロード前に1,000枚以上の画像を走査することになり、処理に20秒以上かかる場合があった。
Gitコミット処理を同期的に実行していたため、その間UIが停止したように見えていた。
そこで、時間のかかる処理をRust側の別スレッドで実行し、Tauri Eventsを通じてフロントエンドへ進捗を送る構成へ変更した。
現在は次のステップをプログレスバーで確認できる。
- 画像をR2へアップロード
- 画像パスを変換
- Gitへコミット
プッシュ時にも、プッシュ処理とローカル画像パス変換の進捗を表示する。
画像パスの後処理に失敗しても、すでに成功したプッシュまで失敗扱いにしないよう、エラーと警告を分けた。長時間処理では、単に非同期化するだけでなく「今どこまで進んでいるか」を利用者へ伝えることも重要だった。
自動保存と未保存警告
本文を変更すると、30秒後に自動保存する。変更後に再び入力した場合はタイマーをリセットするため、入力中に何度も保存処理が走ることはない。
手動保存はCmd + Sから行える。保存完了時にはトースト通知と最終保存時刻を表示する。
未保存の本文がある状態で別の記事を開く場合や、新規記事を作成する場合は確認ダイアログを表示する。ウィンドウを閉じようとした場合も、未保存の変更があれば警告する。
ビルドと配布
macOS版は次のコマンドでビルドしている。
npm run tauri build
Tauriのbuildコマンドは、実行ファイルだけでなく、設定された形式のアプリケーションバンドルも生成する。Tauriの配布ドキュメント
現在の構成では、Apple Silicon向けに次のファイルが生成される。
fokus-editor.app
fokus-editor_0.1.1_aarch64.dmg
ランディングページも日本語版と英語版を用意し、機能紹介、スクリーンショット、ダウンロードリンク、OGP設定、スマートフォン表示への対応を行った。
-
Fokus Editor 日本語版
shek-fokus.com
(新しいタブで開く) -
Fokus Editor English
shek-fokus.com
(新しいタブで開く) -
GitHubリポジトリ
github.com
(新しいタブで開く)
現時点では、コード署名と公証、自動更新は未実装である。Tauriの公式ドキュメントでも、macOSでアプリを一般配布する場合はコード署名と公証が必要とされている。macOS Code Signing
AI駆動開発で学んだこと
MVPを作る速さと、完成度を上げる速さは別
最初のMVPは、CursorのComposerを使いながら約半日で実用できるところまで作れた。
自分用のツールだったため、必要な機能や画面構成を最初から具体的に説明できたことが大きい。要件が明確であれば、AIは非常に速い。
一方で、画像パス、CSP、タイムゾーン、IME、Git認証、非同期処理など、実際に運用しなければ見つからない問題も多かった。
「半日で完成した」というより、「半日で使い始められるMVPができ、その後の運用で完成度を上げ続けた」という表現が正確である。
設計資料と実装は簡単にずれる
初期資料ではFastAPIを使う構成だったが、実装の過程で不要になった。それにもかかわらず、古い設計資料にはFastAPIの記述が残っていた。
R2アップロードやWebP変換についても、以前の資料では「未実装」となっていたが、現在はコミット処理に組み込まれている。
AIは大量の設計書やコードを短時間で作成できる一方、それらが常に同期されるわけではない。最終的には、動いているソースコード、テスト結果、実際のビルドを基準に確認する必要がある。
エラー処理は人間が方針を決める必要がある
画像アップロードに失敗した場合はコミットを止めるべきか。プッシュ後のローカル変換に失敗した場合、成功済みのプッシュもエラーとして見せるべきか。
このような判断に、唯一の正解はない。
Fokus Editorでは、公開結果に影響する画像アップロード失敗はエラーとして停止し、プッシュ後のローカル変換失敗は警告として扱うことにした。
AIにコードを書かせる場合でも、「何を成功とみなし、どこで停止するか」という製品側の判断は、人間が明確に決める必要がある。
現在実装できていること
2026年8月時点では、次の機能を実装している。
- MDX記事の作成・編集・保存・削除
- Frontmatterのフォーム編集
- Monaco Editorによる本文編集
- Markdown用ショートカット
- 30秒後の自動保存
- 未保存警告
- ライトモード/ダークモード
- リアルタイムプレビュー
- ローカル画像とR2画像のプレビュー
- URLのリンクカード表示
- 画像のドラッグ&ドロップ
- WebPへの自動変換
- Cloudflare R2への画像アップロード
- Gitの変更検出
- コミットメッセージの自動入力
- コミット・プッシュ
- コミット・プッシュ処理の進捗表示
- macOS向け
.app/.dmgビルド - 日本語/英語のランディングページ
今後の予定
今後は、次の機能を追加・改善したい。
- macOSアプリのコード署名と公証
- アプリの自動更新
- 画像の一覧表示・削除・再アップロードを行う管理画面
- Git pull、ブランチ切り替え、コンフリクト確認
- Astroコンポーネントを含むMDXプレビューの再現性向上
- URLメタデータキャッシュの永続化
- Intel MacやWindowsへの対応
- GitHub Actionsを使ったリリースビルドの自動化
まとめ
Fokus Editorは、Astroブログの記事を書くときに感じていた小さな不満を解消するために作った。
Frontmatterをフォームから編集し、本文を書き、画像を追加し、プレビューを確認する。最後にコミットしてプッシュすれば、Cloudflare Pagesがブログを公開する。
一つひとつは既存ツールでも実現できる作業だが、それらを一つの流れとしてまとめたことで、記事を書くためにターミナルや複数のアプリを行き来する必要がなくなった。
AIを使えば、個人専用のツールでも短時間で形にできる。ただし、実用レベルへ仕上げるには、実際に使い、問題を見つけ、設計と実装の差を一つずつ埋めていく作業が欠かせない。
Fokus Editorはまだ完成ではない。それでも、文章を書くために作ったアプリで、今まさにこの記事を書けている。その時点で、この開発には十分な価値があったと思っている。