ブログ記事編集アプリ Version 1を作るまで

ChatGPTで長く相談を続けていると、あとから「この内容をブログ記事にしたい」と思うことがあります。ところが、実際に長いセッションを記事化しようとすると、思っていた以上に手間がかかります。記事に使いたい重要な回答だけでなく、確認のための短いやり取り、操作手順、テスト結果、修正依頼、エラー対応なども大量に含まれているからです。

そのままAIに「全部まとめて記事にして」と頼むと、開発中には必要だったものの、読者には不要な操作ログまで本文に入りやすくなります。一方で、「ここで重要なのは元データを壊さないこと」「この方式ならAPIを使わずに済む」といった、やり取りの途中で見つかった設計上の重要ポイントは残しておきたい。
そこで、長いChatGPTセッションから必要なQ&Aだけを選び、順番を変えたり、回答を修正したりしながら、ブログ記事の下書き作成につなげるためのローカルWebアプリを作ることにしました。

完成したアプリの名前は「ブログ記事編集アプリ Version 1」です。見た目はシンプルですが、完成までには何度も方針変更がありました。この記事では、完成した機能を並べるだけではなく、最初に何を考え、どこで問題が見つかり、なぜ仕様を変え、最終的にどのような設計に落ち着いたのかを、開発の流れに沿ってまとめます。


注)本記事の内容は正確性を保証するものではない。

目次

そもそも、なぜ専用アプリを作ろうと思ったのか

きっかけは、ChatGPTとの長い相談内容をブログ記事にしたいと思ったことでした。普通に考えれば、セッション全体をAIに渡して「記事にしてください」と頼めば済みそうです。しかし、実際の長いセッションには、本題とは別のやり取りがかなり含まれます。

たとえば、開発中には「このボタンを押してください」「スクリーンショットを送ってください」「この結果をCodexに伝えてください」といった確認作業が何度も入ります。これらは開発を前に進めるためには必要ですが、ブログ記事の読者にとってはほとんど意味がありません。その一方で、その確認作業の途中で「元データを残したまま編集データを別に持つべき」「画面の見た目ではなく構造化データを正本にした方がよい」といった重要な判断が出てきます。

つまり必要なのは、会話を単純に短くすることではなく、「不要な作業ログは落とすが、そこから得た設計上の学びは残す」という編集です。この選別をしやすくするために、Q&Aをカード形式で一覧表示し、記事に使うものだけを選び、並べ替え、必要なら回答も編集できる専用アプリを作る方針になりました。

最初は「もっとAIに任せる」構成を考えていた

最初の段階では、編集画面の中からAIへ指示を送り、そのまま記事を作らせるような構成も候補にありました。外部から使いやすくするために、Web上のサービスを利用する案も検討しました。ところが、検討を進めるほど、別の問題が見えてきます。

AI機能をアプリ内部へ直接組み込むと、OpenAI APIを使う構成になりやすくなります。私は、普段使っているChatGPTの有料契約の範囲で利用し、APIの従量課金はできるだけ避けたいと考えていました。そこで、アプリの役割を「AIそのもの」から「AIに渡す素材を整理する編集台」へ切り替えました。

この方針転換は、Version 1全体の設計に大きく影響しました。アプリ自身はAIにならず、AIとの通信も行いません。
記事を書くAIは、普段使っているChatGPTの画面をそのまま利用します。アプリはその前後で、Q&Aの整理、保存、Markdown生成、記事下書きの読み込み、WordPress向け形式への変換を担当します。

「APIを使わない」を設計条件にした

最終的なVersion 1では、OpenAI APIを使いません。記事データなどを保存するGoogle DriveのGoogle Drive APIも使わず、Google OAuthも不要です。WordPressへAPIで投稿する機能もなく、ChatGPTの画面を自動操作することもありません。

一見すると、自動化が足りないように見えるかもしれません。しかし、記事作成では「どのQ&Aを使うか」「この構成でよいか」「この文章を公開してよいか」という判断が重要です。そこで、人が確認した方がよい部分はあえて人に残し、その間にある面倒な整理や変換だけをアプリに任せることにしました。

この役割分担にしたことで、仕組みが分かりやすくなり、トラブル時の切り分けもしやすくなりました。AIをできるだけ多く使うことよりも、AIに任せる場所、アプリに任せる場所、人が判断する場所を明確にすることの方が、実用的なツールでは重要だと感じました。

画面は「左が素材、右が記事、下がAIへの指示」にした

最終的な画面構成は、左側が「Q&A素材」右側が「記事下書き」下部が「プロンプト」です。
左側では、採用するQ&Aを選び、順番を変え、AIの回答の確認・編集ができます。
右側には、ChatGPTが作成した記事下書きと構成確認コメントが表示されます。
下部では、ChatGPTへ渡すプロンプトやMarkdownを生成します。

開発途中では、採用Q&Aと非採用Q&Aを左右に分けて表示する画面も試しました。しかし、それでは右側を記事下書き専用にしたいという最終仕様と衝突します。そこで、本実装では採用Q&Aと非採用Q&Aを左ペイン内で分け、右側は記事専用にしました。

また、一覧画面の質問・回答表示についても、AI要約は使いませんでした。内部には全文を保存し、一覧カードではCSSのline-clampを使って2行程度だけ見せます。意味を要約してしまうと、毎回AI処理が必要になるだけでなく、原文と意味がずれる可能性もあります。「短く表示する」と「要約する」は別物として扱うことにしました。

上図の「採用・不採用を切り替え」をタブで切り替える機能は実装しておらず、下図のように、不採用(非採用)のQ&Aは、採用されるQ&Aの後に配置されるように構成した。下図の例では、採用されたQ06のQ&Aの後に、不採用(非採用)のQ&A(Q05)が配置されるように構成されており、採用・非採用を切り替えるチェックボックスのチェックを外すと、自動的に非採用の位置に移動するように構成している。


このアプリはOpenAI APIを使ってAIを呼び出して「記事下書き」を作成するのではなく、AIのチャットのプロンプト入力欄に入力するプロンプトを生成する機能を有するものであるため、編集画面下部に設けた「プロンプト」セクションで、指示概要をプルダウンメニューで選択し、「プロンプト生成」をクリックすると、AIのチャットのプロンプト入力欄に入力するプロンプトが生成される。この欄でプロンプトを編集することもできる。

元の順番と、記事用の順番を分けて持つ

Q&Aを記事にする場合、並べ替えは欠かせません。会話の順番が、そのまま記事として読みやすいとは限らないからです。
そこで各Q&Aには、original_orderuser_order の2種類の順序情報を持たせました。

original_order は、元のChatGPTセッションでの順番で、後から変更しません。user_order は、記事用にユーザーが並べ替えた現在の順番です。
たとえば元の会話が Q01 → Q02 → Q03 → Q04 → Q05 → Q06 だったとしても、記事用にはQ05を非採用にして Q01 → Q02 → Q03 → Q04 → Q06 とすることができます。それでもQ05の original_order=5 は残ります。

ここで大切なのは、「元の状態」と「現在の編集状態」を同じ値で管理しなかったことです。元順序を上書きしてしまうと、あとから会話本来の流れを確認したり、元へ戻したりするのが難しくなります。これは回答本文でも同じ考え方を採用しました。

回答も「元回答」と「編集回答」を分けた

AIの回答には answer_originalanswer_edited を用意し、ユーザーがAIの回答を修正できるように構成しました。answer_original はChatGPTから取得した元回答で、原則として変更しません。ユーザーが修正した内容は answer_edited に保存します。

記事生成時には、answer_edited が存在すればそちらを優先し、なければ answer_original を使います。
これにより、「少し直したけれど、やっぱり元に戻したい」という操作が安全にできます。編集画面には「元回答に戻す」を用意し、編集中の文字列もすぐには本データへ反映せず、一時バッファに保持します。「保存」で確定し、「キャンセル」なら破棄する方式です。

この設計は地味ですが、実際に長い記事を扱うと安心感が大きく違います。元データを上書きせず、加工結果を別に持つことは、今回のアプリ全体で何度も繰り返し採用した基本方針になりました。

本アプリでは、Q&AのA(AIの回答)の部分を、オリジナル(元回答)は残したまま、編集できる機能を設けており、これにより、回答中の不要な段落の削除や、用語・言い回しの修正を「Q&A素材」の段階で行えるようになった。

並べ替えは、最初のドラッグ方式がうまくいかなかった

Q&Aの並べ替えでは、最初にHTML標準のドラッグ&ドロップを試しました。見た目ではドラッグできるように見えたのですが、実際の並べ替えは安定しませんでした。そこで、専用の「≡」ハンドルを使い、pointerイベントで移動先を検出する方式へ変更しました。pointermove で移動中の位置を追い、pointerup で並べ替えを確定します。また、ドラッグだけに依存しないよう、「上へ」「下へ」ボタンも残しました。

一番難しかったのは、ChatGPTの会話を正確に取り込む部分だった

本アプリは、ChatGPTページの右上にある「共有する」の機能を使ってQ&Aの情報を取り出すが、今回の開発で、想像以上に難しかったのがQ&Aの取込です。最初は、共有されたChatGPTページの画面表示からテキストを取り出す、いわゆるDOMベースの抽出を試しました。一見すると問題なく読めているように見えましたが、詳しく確認すると不具合が見つかりました。

回答本文の中に WordPress.org+1 のような、本文ではないUI文字列が混ざっていたのです。さらに、元の会話では6組あるQ&Aのうち、DOM方式では4組しか取れていないケースもありました。画面に見えている文字列をそのまま正本にするのは危険だと分かりました。

そこで本実装では、DOMの innerTexttextContent を本文抽出に使わず、共有ページ内部の構造化データを利用する方式へ変更しました。linear_conversation からユーザーとアシスタントのメッセージ対応を取り、message.content.parts を本文の正本として扱います。

この変更により、画面上の引用UIや補助表示が本文へ混ざる問題を避けられ、DOMでは取りこぼしていたQ&Aも取得できるようになりました。ここで分かったのは、「人間に見えている画面」と「データとして正しい本文」は同じとは限らない、ということです。

※「PoC」とは、Proof of Concept(概念実証)の略で、完成版を作る前に、重要な機能が実現可能かを小さく試すことであり、上図は、機能が実現可能かを試す段階で問題を発見し、方針を変更した例が示されている。

citationは、推測で削除しない

ChatGPTの回答には、引用情報や出典メタデータが含まれる場合があります。これを単純な文字列置換で消してしまうと、本来の本文まで削除する危険があります。

そこで、metadataに含まれる位置情報と対象文字列が完全一致した場合だけ、citationとして本文から分離する方式にしました。位置はUnicode code pointベースで扱い、一致しない場合は「たぶん引用記号だろう」と推測して削除しません。

この考え方は、後述するwriting blockの処理にも共通しています。分からないものを勝手に直さず、確実に判定できたものだけを変換する。結果として多少の情報が残ることよりも、元データを誤って壊すことの方を避ける設計です。

保存データは、役割ごとに分けた

データ構造も、途中で整理しました。中心になるのは qa_source.json で、Q&A本体、元順序、現在順序、採用状態、元回答、編集回答などを保存します。AIが提案した順番や記事構成に関する情報は article_info.json に分け、AIへ渡す素材は qa_selected.md、記事の下書きは article_draft.md としました。

役割ごとにファイルを分けたことで、どのデータが正本なのかが分かりやすくなりました。

Google Driveは「API」ではなく「普通のフォルダ」として使う

AIとのファイル受け渡しではGoogle Driveを使いますが、Google Drive APIは使いません。Google Drive for desktopでMac上に同期されるフォルダを、普通のローカルフォルダとして扱います。

アプリから見れば、Google Driveだから特別なAPI処理をしているわけではありません。ローカルフォルダへファイルを書き込むと、それがDrive側へ同期されます。これならOAuthも不要で、API利用料もありません。

ChatGPTが作った article_draft.md (記事下書き)も、ユーザーがダウンロードして同期フォルダへ入れるだけです。
途中に手作業はありますが、どこで何が起きているのかが見えるため、トラブル時の切り分けはしやすくなりました。

AIには採用Q&Aだけ渡しながら、元の全体順も伝える

AI受け渡しMarkdownにも、受入テスト中に重要な仕様変更がありました。最初は、採用したQ&Aだけを qa_selected.md に入れていました。たとえばQ05を非採用にすると、AIにはQ01 → Q02 → Q03 → Q04 → Q06しか見えません。

すると、AIが構成確認コメントで「元のQ&A順」を書こうとしても、Q05が存在したことを知る方法がありません。実際のテストでも、ChatGPTは「Q05は添付されていないため元の全順序は分からない」と正しく判断しました。これはAIの失敗ではなく、渡している情報が不足していたためです。

そこで qa_selected.md の先頭にHTMLコメント形式のメタデータを追加しました。そこには「元の全Q&A順」「現在の採用順」「非採用Q&A」を記録し、本文部分には引き続き採用したQ&Aだけを入れます。これにより、非採用Q&Aの本文を渡さずに、全体構造だけをAIへ伝えられるようになりました。

プロンプトはアプリ内で生成するが、自動送信しない

アプリには「おすすめ構成」「更新準備」「誤字脱字修正」「表現のゆらぎ検出」「です・ます調統一」といったプロンプト種別を用意し、修正量も「少なめ」「おすすめ」「多め」から選べるようにしました。

ただし、ボタンを押してもChatGPTへ送信はしません。プロンプト欄に文章を生成するだけで、ユーザーが内容を確認し、必要なら編集してからコピーします。ここでも「自動化できるか」ではなく、「人が確認した方がよいか」を基準に線引きしました。

article_draft.mdは、構成確認コメントと記事本文に分けた

ChatGPTに記事下書きを作ってもらうとき、本文だけ返してもらう方法もあります。しかし今回は、「なぜこの順番にしたのか」「どのQ&Aを採用したのか」を後から確認できるようにしたかったため、記事本文の前に構成情報を付けてもらうことにしました。

ただし、その構成情報がWordPress本文へ入ってしまっては困ります。そこで、article_draft.md の先頭をHTMLコメントにし、その中に元のQ&A順、ユーザー指定順、AI推奨順、実際に採用した順、記事のストーリー方針などを入れます。HTMLコメントの後ろには、通常のMarkdown記事本文を置きます。

アプリはこの2つを機械的に分離し、右ペインで「構成確認コメント」と「記事本文」として別々に表示します。WordPressエクスポート時には記事本文だけを使うので、編集用の情報が公開記事へ混ざることはありません。

WordPressへの出力はSWELL専用形式にしなかった

WordPressではSWELLを使っていますが、Version 1ではSWELL独自形式へ強く依存しないようにしました。
第一候補はGutenberg標準ブロックのシリアライズ形式で、代替としてHTML + plainも用意しています。

標準ブロックを中心にした理由は、見出し、段落、リスト、引用、表、区切り線などを比較的安定して扱えるからです。SWELL独自形式だけに寄せると、テーマ側の仕様変更の影響を受けやすくなります。

画像についても、Version 1では自動アップロードしません。【図解01:○○を示す図】 のようなプレースホルダを通常段落として残し、画像作成や挿入は別工程として扱います。

AIが作成した「記事下書き」を、WordPress(テーマ:Swell)の編集画面に貼り付けられる形式に変換する機能を有しているが、上図のように、変換する形式を、Gutenberg標準ブロックのシリアライズ形式か、HTML + plainの形式かを選択できるように構成している。

本実装で見つかったH1問題

本アプリの実装後、WordPressエクスポート画面を確認すると、Markdownの先頭 # がそのままH1へ変換されていました。Markdownとしては自然な変換ですが、WordPress/SWELLでは投稿タイトルがすでにH1になるため、本文にもH1を作ると見出し階層が重複します。

そこで、本文内ではH1を作らない変換規則へ変更しました。Markdownの ### はH2、### はH3、それ以降はH4〜H6として扱います。これにより、WordPressのタイトル欄がH1、本文がH2から始まる自然な構成になりました。

実貼付で見つかったコードブロック問題

見出し問題を直したあと、Gutenberg形式を実際のWordPressテスト投稿へ貼り付けました。見出しや段落、箇条書き、番号付きリストはきれいに入りましたが、コードブロックでは別の問題が見つかりました。

Markdownの text のような開始フェンスや終端の がそのまま文字として表示され、raw-post-content.html のようなインラインコードもバッククォート付きで残っていたのです。つまり、Markdownコード表現からWordPressブロックへの変換が不足していました。

修正前
修正後

そこで、fenced codeはGutenbergでは core/code、HTML + plainでは <pre><code>、インラインコードは <code> へ変換するよう修正しました。開始・終了フェンスや言語指定は本文へ出さず、HTML特殊文字はエスケープし、改行や罫線文字はそのまま保持します。

修正後にもう一度WordPressへ貼り付けると、ZIP構成のツリーはきれいなコードブロックになり、ファイル名はインラインコードとして表示されました。最終的に、Gutenberg形式もHTML + plain形式も実貼付でPASSになりました。

PoC(概念実証)を先に行った効果と、それでも残った問題

今回、いきなり本実装へ進まず、共有チャット取込、2行プレビュー、データスキーマ、ドラッグ並べ替え、回答全文編集、Google Drive同期、ChatGPTへのファイル受け渡し、記事ファイル監視、構成コメント分離、プロンプト生成、クリップボード、WordPress貼り付け、画像プレースホルダなどをPoCとして一つずつ確認しました。

この進め方には大きな効果がありました。DOM抽出の問題やドラッグ方式の不安定さ、Google Drive同期特有の挙動を、本体を作り込む前に発見できたからです。もし本実装後に構造から見直していたら、かなり大きな手戻りになっていたはずです。

一方で、PoCがすべてPASSでも十分ではありませんでした。本実装で実データを読み込むとlegacy schemaとの互換問題が出て、writing block正規化の不足も見つかりました。AI受け渡しMarkdownには元の全Q&A順が足りず、WordPress実貼付ではH1問題、その後コードブロック問題も見つかりました。

この経験から、「部品単位のPoC」と「完成品を人が最初から最後まで使う受入テスト」は別物だと分かりました。

セキュリティ面では「ローカルだけ」を徹底した

Version 1はローカルWebアプリで、サーバーは 127.0.0.1 だけへbindします。外部へ公開せず、ファイル操作についてもpath traversal、symlink escape、workspace allowlistを検証しました。つまり、指定した作業領域の外へ勝手にアクセスしないことを確認しています。

iPhoneなど外部端末からの利用も検討しましたが、Version 1ではまずMac上で安全に使えることを優先し、外部アクセスは保留にしました。

完成したVersion 1の実際の流れ

完成後の使い方は、かなり単純になりました。まずQ&Aデータをアプリへ読み込み記事に使うQ&Aを選びます。
不要なものは非採用にし、必要なら順番を変え、回答全文を編集します。その状態を qa_source.json として保存します。

次に、アプリでプロンプトと qa_selected.md を生成します。qa_selected.md の本文には採用Q&Aだけが入り、先頭のHTMLコメントには元の全Q&A順、現在の採用順、非採用IDが記録されます。それをChatGPTへ渡して記事下書きを作り、article_draft.md としてダウンロードします。

article_draft.md をGoogle Drive同期フォルダへ保存すると、アプリが自動検知し、右側の「記事下書き」に構成確認コメントと記事本文を表示します。内容を確認したらWordPressエクスポートを開き、第一候補のGutenberg標準ブロック形式、または必要に応じてHTML + plainをコピーし、WordPress/SWELLへ貼り付けます。

あえて自動化しなかった部分

今回のアプリは、多くの繰り返し作業を楽にしていますが、最後まで自動化しなかった部分もあります。
ChatGPTへの送信、ChatGPTが作った回答の採用判断、WordPressへの貼り付け、投稿の保存、公開です。

記事作成では、「本当にこのQ&Aを使うのか」「この構成でよいのか」「公開して問題ないか」といった判断が重要です。
そこまで自動化してしまうと、便利さよりも不安の方が大きくなります。

Version 1では、「繰り返し作業はアプリ、人の判断は人」という線引きにしました。この線引きこそ、今回の開発で最も重要だった設計の一つだと思います。

今回の開発で得た教訓

今回の開発で一番大きかったのは、「AIを使うシステムだからといって、AI APIを組み込む必要はない」と分かったことです。通常のChatGPT UIとローカルアプリを、ファイルとクリップボードでつなぐだけでも、十分に実用的なワークフローを作れます。

もう一つは、「元データを壊さない設計」の重要性です。original_orderuser_orderanswer_originalanswer_editedanswer_raw と正規化後本文というように、元データと加工データを分けておけば、途中で失敗しても戻ることができます。

さらに、「画面に見えているものを正本だと思わない」という点も印象に残りました。ChatGPT共有画面のDOMには本文ではないUI文字列が混ざることがあり、構造化データを使うことで問題を避けられました。

そして、PoCだけでも、自動テストだけでも不十分です。実際に人が最初から最後まで使う受入テストを行って初めて、H1重複やコードブロックのような実運用上の問題が見つかりました。最後に、全部を自動化しないことも立派な設計です。人が確認した方がよいところを人に残すことで、結果として安心して使えるツールになりました。

ローカルAIに記事下書きを作成させる構成はコストの面で魅力的で、記事作成の自動化を図りやすいが、初期投資が大きく、導入・メンテナンスの手間がかかるのでローカルAIの導入は検討しなかった。また、クラウドAIをAPI経由で利用することにより、高性能なクラウドAIを活用することができ、自動化も図りやすいが、API利用料が発生するので、この構成も採用しなかった。そのため、ユーザーがいくつかの作業を手動で行わなければならなくなり、操作手順が分かりにくくなったため、AIに下記の操作手順書を作成してもらった。ユーザーには操作手順書が必要である。

今後の改善候補

Version 1は完成しましたが、まだ拡張できる部分はあります。たとえば、iPhoneなど外部端末からの安全なアクセス、画像プレースホルダ一覧から画像生成用プロンプトを作る機能、article_draft.md の複数版を保存して修正前後を比較する履歴機能、structured importの対応範囲拡大などです。

また、今回作ったアプリそのものをStream Deckなどから起動できるようにすれば、普段のブログ作業への組み込みもさらに簡単になります。ただし、Version 1でうまくいった「自動化しすぎない」という方針は、今後の機能追加でも維持した方がよいと考えています。

まとめ

「ブログ記事編集アプリ Version 1」は、最初から完成形が見えていたわけではありません。DOM抽出ではUI文字列の混入やQ&A欠落が起き、ドラッグ方式も変更しました。Google Driveとの連携はAPIではなく同期フォルダ方式に変え、AI受け渡しMarkdownには元の全Q&A順を伝えるメタデータを追加しました。WordPress出力ではH1問題が見つかり、さらに実貼付でコードブロックの変換不足も見つかりました。

そのたびに、なぜ失敗したのかを確認し、元データを壊さず、人が判断すべきところは残す方向へ設計を直しました。最終的には、ChatGPTのQ&Aを読み込み、必要なものだけ選び、順序を変え、回答を編集し、AI向け素材を作り、通常のChatGPTで記事を書き、Google Drive経由でアプリへ戻し、WordPress/SWELLへ貼り付けるという一連の流れが完成しました。

API課金はなく、ChatGPTの自動操作も、WordPressへの自動投稿もありません。それでも、記事作成で本当に面倒だった整理や変換の部分はかなり減りました。

今回作ってみて分かったのは、AI活用では「どこまで自動化できるか」だけを考えるよりも、どこをAIに任せ、どこをアプリに任せ、どこを人が確認するかを決める方が、実用的な仕組みを作るうえでは重要だということです。Version 1は、その役割分担がようやく形になった最初の完成版です。

ようやく、本アプリの一通りの機能の実装を終えたが、本アプリの編集機能が、より良い記事の作成にどの程度役立つのか、どの程度効率化が図れるのかは検証できていない。今後、実使用において、改良点などをAIにレビューしてもらいながら使い勝手を向上させていきたい。

本アプリの開発は、ChatGPTのチャット(モデル:ChatGPT-5.6 Sol)で実装計画作成、Codexへの実装指示書作成を行い、Codexで実装するという形で進めたが、何度もチャット画面とCodex画面を往復することになり、実装状態をAIに確認してもらうために、スクリーンショットを何度もチャット画面に貼り付ける必要があった。チャットとCodexで容易に情報共有できるようにしてほしい。または、チャットのAI エージェントが、CodexのAIエージェントを、現状費用負担の範囲内で使えるようになってほしい。

目次