Chrome 拡張からの自動操作で、自分の note 記事 14 本の末尾に同じ 1 行(サイトへの導線)を足そうとしました。「末尾をクリック → End → Enter → 文字を打つ」で 3 回壊れました。原因は note 固有というより、フォーカス・キャレット・保存前の検査を分けて扱っていなかったこと。DOM の Selection で位置を作り、1 手ごとに状態を数える手順にしたら 14/14 通りました。失敗 3 型と手順を書きます。
この記事の位置づけ(2026-09-26 追記)
- この記事で新しく示すこと: note のリッチエディタを自動操作で壊した 3 型と、DOM の Selection で位置を作って 14/14 通った手順。
- 当てはまる範囲: 2026-09 時点の note の編集画面・当方の記事 14 本。画面が変われば変わります。
- 言葉: 本文の「器」=点検や処理のための自作スクリプトのことです。
先に結論
リッチエディタ(contenteditable)を自動化するときの 3 原則(と、その前の第 0 段階):
- これから触るノードが本当に目的のノードか確かめる——対象段落の本文が期待したアンカー文字列と一致するか
- フォーカスを信用しない——クリックした位置とキャレットの位置が一致するとは限らない
- キャレット位置を自分で作る——Selection API で狙った段落の末尾に置いてから入力する
- 入力後の状態を確かめてから保存する——足した文が独立した段落として正確に 1 回だけ存在し、先頭の見出しが文字列そのまま変わっていないこと。保存は人が押す
input/textarea は、今回使ったブラウザ自動化の器では値の差し替えが返る入力手段(form_input)だけで触った。contenteditable には、対象ノードとキャレット位置を確定しないまま type/key を送らない(確定してからの Return と type は下の成功手順のとおり)。
失敗 3 型(note・ProseMirror・2026-09-15)
| 型 | やったこと | 起きたこと |
|---|---|---|
| ① 先頭見出しに混入 | 末尾のリスト項目をクリック → End → Enter → type | キャレットが文頭に飛び、先頭の見出しに 1 行が混入(文字数 3,488→3,575)。Ctrl+Z ×40 で戻し、「Leave site?」を破棄 |
| ② 段落が結合 | 2 段落にまたがる範囲を選択して Enter | 選択範囲に段落の境界(\n\n)が含まれ、2 つの段落が 1 つに結合
|
| ③ リンクカード化 | URL で終わる段落の末尾で Enter | URL が埋め込みカードに変換され、テキストとして残らない |
3 つの原因は少しずつ違います——①はキャレット位置、②は複数段落にまたがる選択範囲、③は URL 末尾で Enter したときのエディタ固有の変換。共通するのは入力位置・選択範囲・編集文脈を暗黙にエディタへ任せていたこと。もう 1 件、別のチャットアプリで検索欄をクリックしたつもりがメッセージ入力欄に文字を打っていた(投稿は確認できず、空にして戻した)。これは第 0 段階=対象ノードの確認を飛ばした事故です。
通った手順(4 手・14/14)
以下は抜粋で、そのまま動くスクリプトではありません。⓪①④は page 側の JS、②③は自動化器からのキー送信(Return・type)で、ここではコメントで示しています。
// ⓪ 触るノードが目的のノードか確かめる(本文が期待したアンカーと一致しなければ止める)
const p = [...document.querySelectorAll('.ProseMirror p')].at(-2);
if (!p.textContent.includes(EXPECTED_ANCHOR)) throw new Error('対象が違う: ' + p.textContent.slice(0, 40));
const h2Before = document.querySelector('.ProseMirror h2')?.textContent;
// ① キャレットを、その段落 p の末尾に自分で置く(URL で終わる段落は避けて、その前の段落を選ぶ)
const range = document.createRange();
range.selectNodeContents(p); range.collapse(false);
const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);
// ② Enter を送る → 段落数が増えたかを数える(今回の note では 1 回目の Return が反映されないケースがあったので、増えていなければもう 1 回)
// ③ 本文を type
// ④ 保存の前に確かめる: 足した文が独立段落として正確に 1 回だけ在るか・先頭の見出しが文字列そのまま h2Before と等しいか
// const ps=[...document.querySelectorAll('.ProseMirror p')].filter(x=>x.textContent===ADDED_LINE); ps.length===1 && document.querySelector('.ProseMirror h2').textContent===h2Before
- ①で「URL で終わる段落」を避けるのは型③のため。1 つ前の段落の末尾に置く。
- ②の Return は、送ったあとに段落数を数える。増えていなければもう 1 回。増えたら type。
- ④は JS で「足した文と完全一致する段落がちょうど 1 つ」「先頭
h2の文字列が編集前と等しい」を見る(文字数だけだと、同じ長さの別文字列に壊れても緑になる)。合っていれば「公開に進む」を人が押す。
14 本で 14 本とも、足した行が独立段落・見出し無傷・URL がテキストのまま、を確認してから公開しました。
3 原則に戻す
失敗は note 固有の話に見えますが、直したのは note でなく手順でした。
- フォーカスを信用しない——型①の原因。クリックは「そのあたり」にフォーカスを置くだけで、キャレットがどこにあるかは別。
- キャレット位置を作る——Selection API で「この段落の末尾」と指定する。型②(段落境界を含む選択)も型③(URL 末尾)も、位置を自分で決めれば避けられる。
- 入力後の状態を確かめてから保存する——1 手ごとに JS で見る。「打った」と「入った」は別。保存ボタンは人が押す(取り返しがつかない操作は自動化しない)。
順番にすると 対象確認 → キャレット作成 → 入力 → 事後検査 → 人が保存。
範囲と限界
- 観測は note のエディタ(ProseMirror 実装)・Chrome 拡張・2026-09-15・「末尾に 1 行足す」操作だけ。他のエディタ(Notion・Slack 等)や他の操作には外挿しません。
- 「1 回目の Return が反映されない」は今回の環境での観測で、一般則ではありません。だから「数える」を手順に入れています。
本家(検証の記録つき)
この記事の本家(検証環境・判定・証拠)は Sumitsuke Lab → 対象確認・キャレット作成・事後検査を分けたら 14/14(検証の記録つき)。
AI の利用について
自動操作と JS の下書きは AI(Claude Code)が行い、失敗 3 型の記録・手順の確定・公開の押下は人間が行いました。対象は自分の記事 14 本。他人の投稿・アカウントは触っていません。
受託でも同じ手順で読んでいます
「自動化したら別の場所に入った」——フォーカス・キャレット・保存前検査の 3 つに分けて見ます。テキストだけで受けています。
▶ Sumitsuke / 動かない箇所の点検と修理
次に読む: 入力後の状態を確かめる、を図の生成にも——目視で取り切れなかったはみ出し 3 件
この記事は Zenn にも同じ内容を掲載しています。
