【拡張機能】記事数が200を超えたら。「検索」を一瞬にし、記事という「資産」を守る、自作ツールの話。【コピペで完成】
あなたは、「自分の過去記事」を探すのに、どれくらいの時間を使っていますか?
noteを長く続けていると、ある日突然、壁にぶつかります。それは「記事の管理」という壁です。
私の場合、下書きを含めて記事数が200を超えたあたりで、限界が訪れました。
ふと、「あ、以前書いたあのテーマ、今のニュースと絡めて紹介したいな」と思い立つ。
自分のクリエイターページを開く。
「記事」タブをクリックする。
スクロールする。
「もっとみる」を押す。
さらにスクロールする。
ブラウザの検索機能(Ctrl+F)でキーワードを入れる。
ヒットしない。
(まだ読み込まれていないからだ……)
ため息をついて、またスクロールする。
この「探す」という行為に、毎回数分。
積もり積もれば、膨大な時間が「創作」ではなく「捜索」に消えています。
最初はスプレッドシートやNotionでの管理も試みました。
タイトルをコピーし、URLを貼り付け、公開日を入力する。
……続きませんでした。
記事を書くたびに別ツールを更新するのは手間がかかりますし、更新を忘れると最新情報とズレてしまい、結局noteの画面を確認しに行くことになるからです。
「noteの画面そのままで、もっとスムーズに検索したい」
「管理ツールを別に開くのではなく、今の作業画面で完結させたい」
「下書きの中に眠っている『書きかけの宝』を一瞬で発掘したい」
そう考えて、自分のためにブラウザ拡張機能を作ってみました。
派手な機能はありません。魔法のような自動化もしません。
ただ、あなたのnote管理画面に「静かに」常駐し、必要な時だけ最高の仕事をする。そんな執事のようなツールです。

200記事の中から一瞬で発掘。これが「Master Mode」。
今回は、このツールの全コードと導入方法、そして開発者向けに「こだわりの実装技術」を余すことなく公開します。

1. 「探す」を終わらせ、「書く」に戻るために

全記事228件の中でタイトルに馬が入っているのは3件でした
この拡張機能「Note Article Master」を導入すると、あなたのnote管理画面(/notes)に、小さなボタンがひとつ追加されます。
それを押した瞬間、noteの使い勝手が「データベース」へと進化します。
① 全記事を「一瞬」でインクリメンタルサーチ
ボタンを押すと、公開済み・下書きを含む「全記事」のデータを一度だけ読み込みます。
その後は、キーワードを一文字入れるごとに、瞬時に記事が絞り込まれます(インクリメンタルサーチ)。
標準機能のように、毎回読み込みを待つ必要はありません。200記事あろうが1000記事あろうが、JavaScriptがメモリ上で処理するため、検索にかかる時間はラグを感じさせない「瞬時」です。
② 本文まで含めた「深掘り検索」
「タイトルは覚えていないけど、確かにあのフレーズを使ったはず」
そんな時は、「全文検索」モードをONにしてください。
記事のタイトルだけでなく、下書きを含む全記事の本文からキーワードを探し出します。
検索にヒットした箇所はハイライト表示され、前後の文脈も表示されるため、クリックして記事を開かなくても中身がわかります。
③ 「埋蔵金」の発掘(下書き管理)
クリエイターにとって最大の資産は、実は「公開記事」ではなく「書きかけの下書き」かもしれません。
「下書き」フィルターを使えば、放置されていたアイデアの断片が一瞬で並びます。
「あ、これ今なら書けるかも」
過去の自分が残したメモが、未来のコンテンツに変わる瞬間です。
④ 【最重要】CSVデータのエクスポート(資産管理・AI活用)
個人的に、実務で最も欲しかった機能がこれです。
ボタン一つで、全記事の以下のデータをCSV形式でダウンロードできます。
記事タイトル
公開ステータス(公開/下書き)
公開日時
スキ(いいね)の数
記事URL
編集画面へのURL
本文のスニペット
万が一のアカウントトラブルに備えたバックアップとしてはもちろん、ExcelやGoogleスプレッドシートで「どの時期にスキが伸びたか」を分析する際にも役立ちます。
さらに面白い活用法として、この全記事データをChatGPT(Custom GPTs)などに読み込ませれば、あなたの過去の文体や思考を学習した「自分専用AIアシスタント」の種としても活用できます。 あなたの記事は、あなただけの貴重な資産なのです。
2. なぜ、こんなに「普通」に動くのか?―開発の裏側にある5つのこだわり
ここからは、少し技術的な話をします。
プログラミングに興味がない方は読み飛ばして「3. 導入手順」へ進んでいただいて構いません。
ただ、もしあなたがエンジニアや、これからプログラミングを学びたい方なら、ここが一番面白い部分かもしれません。
「使いやすいツール」には、必ず理由があります。
今回、私がこのツールを開発するにあたって直面した困難と、それを乗り越えた「実装ベスト5」を紹介します。
【第5位】APIの再帰的Fetchとプログレスバー同期
noteの記事一覧データは、API経由で取得しますが、一度に全件は取れません(ページネーションされています)。
全記事を取得するには、「次のページはあるか?(isLastPage)」を確認しながら、何度もサーバーに問い合わせる必要があります。
これを単純なループで回すと、処理が終わるまで画面が固まったように見えてしまいます。
そこで今回は、非同期処理(async/await)を使いつつ、「いま何件目を取得中か」を計算し、画面上部にプログレスバーを表示する実装を行いました。
// 実装イメージ
pBar.style.width = `${Math.round((allArticles.length / totalArticlesCount) * 100)}%`;ただ待たせるのではなく、進捗を可視化することで体感待ち時間を減らすUX上の配慮です。
また、サーバーに短時間で大量のリクエストを送らないよう、意図的に setTimeout でウェイト(待機時間)を挟むことで、API制限に引っかからないような「礼儀正しい」クローラーとして実装しています。
【第4位】Shadow DOM的なCSS設計(DOM汚染の回避)
ブラウザ拡張機能開発で最も恐ろしいのは、「サイトのデザインを壊してしまうこと」です。
例えば、拡張機能側で .button { color: red; } なんて指定をしてしまったら、note本体のボタンまで赤くなってしまうかもしれません。
これを防ぐため、今回のツールは #master-panel という独立したコンテナ(箱)の中に全ての要素を閉じ込めました。
さらに、CSSセレクタには全て固有のIDやクラスを付与し、既存のnoteのCSSと衝突しないよう細心の注意を払っています。
デザインもnoteのブランドカラー(#2cb696)をスポイトで抽出し、違和感なく溶け込むように調整しました。
【第3位】キーボードナビゲーションのUX
「検索ツール」において、いちいちマウスに持ち替えるのはストレスです。
Googleの検索結果のように、矢印キー(↓↑)でリストを移動し、Enterで開きたい。
これを実現するために、独自のキーボードイベントリスナーを実装しました。
特にこだわったのは、scrollIntoView の挙動です。
ただ選択行を変えるだけでは、画面外に選択行が行ってしまった時に見えなくなります。
target.scrollIntoView({ block: 'nearest', behavior: 'auto' });
この一行を入れることで、リストのスクロール位置が選択行にピタリと追従し、ネイティブアプリのような操作感を実現しました。
【第2位】レイアウトシフト(ガタつき)の防止
リストで選択している行の左端に、緑色のバーが表示されるデザインにお気づきでしょうか。
これを実装する際、単純に「選択された時だけ border-left: 5px solid green をつける」とすると、ボーダーの太さ分(5px)だけ文字が右にズレてしまい、選択するたびに画面がガタつきます(レイアウトシフト)。
これを防ぐため、「プロの技」を使いました。
「選択されていない行にも、透明なボーダー(transparent)を最初からつけておく」のです。
/* 未選択時:透明な5pxの壁を作っておく */
td { border-left: 5px solid transparent; }
/* 選択時:色だけを変える(幅は変わらないのでズレない!) */
.is-selected td { border-left-color: #2cb696; }この地味な工夫が、ツールの「静かな」使い心地を支えています。
【第1位】CSVエクスポート時の文字化けとエスケープ処理
栄えある第1位は、最も実務的な機能である「CSV出力」です。
実は、日本語のCSV出力は鬼門です。単純に出力すると、Excelで開いた時に盛大に文字化けします。
これを防ぐため、ファイルの先頭に BOM(Byte Order Mark) という「これはUTF-8ですよ」という合図(\uFEFF)を付与しました。
さらに、記事のタイトルや本文には、CSVの区切り文字である「カンマ(,)」や「改行」、あるいは「ダブルクォート(")」が含まれている可能性があります。これらをそのまま出力すると、列がズレてデータが壊れてしまいます。
// CSV用エスケープ関数: ダブルクォートを2つ重ね、全体を囲む
const escapeCsv = (val) => `"${String(val).replace(/"/g, '""')}"`;この処理を徹底することで、どんなタイトルの記事であっても、正確にExcelに取り込める強固なCSV生成ロジックを完成させました。
3. 【コピペで完成】拡張機能の作り方と導入手順
お待たせしました。それでは、実際にこのツールをあなたのブラウザに導入しましょう。
プログラミングの知識は一切不要です。WindowsでもMacでも、以下の手順通りに進めれば5分で完了します。
ステップ1:必要なファイル(2つ)を準備する
まず、デスクトップなど分かりやすい場所に、「note-article-search-master」か「note--search-extension」という名前でフォルダを1つ新規作成してください。
そのフォルダの中に、メモ帳などのテキストエディタを使って、以下の2つのファイルを作成します。
① 設定ファイル:manifest.json
以下のコードをすべてコピーし、manifest.json という名前で保存してください。
{
"manifest_version": 3,
"name": "Note Article Master",
"version": "1.0",
"description": "note記事一覧を高度に管理・検索・ソートするツール",
"permissions": [],
"content_scripts": [
{
"matches": ["https://note.com/notes"],
"js": ["content_script.js"]
}
]
}② プログラム本体:content_script.js
次に、以下のコードをすべてコピーし、content_script.js という名前で保存してください。
これがツールの頭脳となる部分です。
(async () => {
let allArticles = [];
let totalArticlesCount = 0;
let filterStatus = 'all';
let sortOrder = 'date';
let isFullSearch = false;
let lastClickedKey = null;
let selectedIndex = -1;
// --- 1. CSS注入 ---
const style = document.createElement('style');
style.textContent = `
#master-launch-btn { position: fixed; top: 12px; right: 280px; z-index: 9999; padding: 8px 20px; background: #2cb696; color: white; border: none; border-radius: 20px; cursor: pointer; font-weight: bold; box-shadow: 0 4px 12px rgba(44,182,150,0.3); transition: 0.3s; }
#master-launch-btn:hover { background: #249178; transform: translateY(-2px); }
#master-panel { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #f4f7f6; z-index: 10000; display: none; flex-direction: column; font-family: sans-serif; outline: none; }
.master-header { padding: 25px 25px 15px; background: white; border-bottom: 1px solid #e1e4e8; display: flex; flex-direction: column; align-items: center; gap: 15px; position: relative; }
#progress-bar-container { position: absolute; bottom: 0; left: 0; width: 100%; height: 3px; background: #eee; }
#progress-bar { width: 0%; height: 100%; background: #2cb696; transition: width 0.3s; }
#master-close { position: absolute; left: 25px; top: 22px; cursor: pointer; background: none; border: none; font-size: 24px; color: #999; transition: 0.2s; }
#master-close:hover { color: #333; }
.search-row { display: flex; align-items: center; justify-content: center; gap: 15px; width: 100%; max-width: 950px; }
.search-container { position: relative; flex-grow: 1; display: flex; align-items: center; }
.search-box { width: 100%; padding: 14px 40px 14px 20px; border: 2px solid #e1e4e8; border-radius: 12px; font-size: 16px; outline: none; transition: 0.2s; }
.search-box:focus { border-color: #2cb696; background: #f9fffb; }
.search-clear { position: absolute; right: 12px; cursor: pointer; color: #ccc; font-size: 20px; display: none; background: none; border: none; }
.hit-counter { font-size: 13px; font-weight: bold; color: #333; background: #f0f2f5; padding: 0 20px; height: 50px; display: flex; flex-direction: column; justify-content: center; align-items: center; border-radius: 12px; border: 1px solid #e1e4e8; min-width: 140px; white-space: nowrap; }
.hit-main-num { font-size: 16px; color: #2cb696; }
.hit-sub-info { font-size: 10px; color: #888; font-weight: normal; }
/* CSV Export Button Style */
#csv-export-btn {
height: 50px; padding: 0 20px; border-radius: 12px; border: 1px solid #2cb696; background: white; color: #2cb696; font-weight: bold; cursor: pointer; transition: 0.2s; display: flex; align-items: center; gap: 6px; white-space: nowrap;
}
#csv-export-btn:hover { background: #eefcf9; }
#csv-export-btn:active { transform: translateY(1px); }
.icon-dl { font-size: 18px; }
.chip-container { display: flex; gap: 10px; align-items: center; justify-content: center; width: 100%; }
.chip { padding: 8px 16px; border-radius: 20px; border: 1.5px solid #dcdfe6; background: white; font-size: 12px; font-weight: bold; cursor: pointer; color: #606266; transition: 0.2s; white-space: nowrap; }
.chip.active { background: #2cb696; color: white; border-color: #2cb696; box-shadow: 0 2px 8px rgba(44,182,150,0.25); }
.chip-label { font-size: 11px; color: #999; margin-left: 10px; margin-right: 2px; }
.master-main { flex-grow: 1; overflow-y: scroll; padding: 10px 40px 40px; scroll-behavior: auto; }
.article-table { width: 100%; border-collapse: separate; border-spacing: 0 8px; }
.article-table td {
background: white; padding: 16px 20px; vertical-align: middle; transition: background 0.1s; border-left: 5px solid transparent;
}
.article-row:hover td { background: #f9fffb; }
.article-row.is-selected td {
background: #eefcf9 !important; box-shadow: inset 0 2px 0 #2cb696, inset 0 -2px 0 #2cb696; transition: none !important;
}
.article-row.is-selected td:first-child { box-shadow: inset 5px 2px 0 -3px #2cb696, inset 0 -2px 0 #2cb696, inset 0 2px 0 #2cb696; }
.article-row.is-selected td:last-child { box-shadow: inset -2px 2px 0 -0px #2cb696, inset 0 -2px 0 #2cb696, inset 0 2px 0 #2cb696; }
.is-selected .col-no { color: #2cb696; font-weight: bold; }
.last-active td { border-left-color: #2cb696 !important; }
.col-no { width: 50px; text-align: center; color: #bbb; font-size: 11px; white-space: nowrap; }
.col-status { width: 100px; text-align: center; white-space: nowrap; }
.col-likes { width: 80px; text-align: center; font-size: 15px; white-space: nowrap; }
.col-date { width: 120px; text-align: center; color: #666; font-size: 13px; white-space: nowrap; }
.col-action { width: 220px; text-align: right; white-space: nowrap; }
.likes-high { color: #f56c6c; font-weight: 900; }
.likes-mid { color: #e6a23c; font-weight: bold; }
.status-badge { display: inline-block; width: 55px; text-align: center; padding: 4px 0; border-radius: 6px; font-size: 11px; color: white; font-weight: bold; white-space: nowrap; }
.badge-pub { background: #2cb696; }
.badge-draft { background: #909399; }
.thumb { width: 80px; height: 45px; border-radius: 6px; object-fit: cover; background: #eee; margin-right: 20px; flex-shrink: 0; border: 1px solid #f0f0f0; }
.title-link { font-weight: bold; font-size: 16px; color: #2c3e50; text-decoration: none; display: inline-block; margin-bottom: 6px; line-height: 1.4; }
.title-link:hover { color: #2cb696; text-decoration: underline; }
.btn-small { text-decoration: none !important; font-weight: bold; padding: 10px 16px; border-radius: 8px; font-size: 12px; transition: 0.2s; display: inline-block; margin-left: 8px; cursor: pointer; border: 1.5px solid; background: white; white-space: nowrap; opacity: 0.4; }
.article-row:hover .btn-small, .article-row.is-selected .btn-small { opacity: 1; }
.edit-btn { color: #2cb696; border-color: #2cb696; }
.copy-btn { color: #606266; border-color: #dcdfe6; }
mark { background: #fff59d; color: #d84315; font-weight: bold; padding: 0 2px; }
.month-divider { position: sticky; top: -10px; z-index: 5; padding: 25px 15px 12px; color: #2cb696; font-weight: bold; font-size: 18px; background: #f4f7f6; display: flex; align-items: center; gap: 15px; }
.month-badge { font-size: 12px; background: white; border: 1.5px solid #2cb696; padding: 3px 10px; border-radius: 15px; font-weight: normal; }
@keyframes flash-green { 0% { background: white; } 30% { background: #eefcf9; } 100% { background: white; } }
.flash-success td { animation: flash-green 1s ease-out; }
.empty-state { padding: 150px; text-align: center; color: #999; font-size: 18px; }
.reset-link { color: #2cb696; cursor: pointer; text-decoration: underline; font-weight: bold; margin-left: 10px; }
`;
document.head.appendChild(style);
// --- 2. UI構築 ---
const launchBtn = document.createElement('button');
launchBtn.id = 'master-launch-btn';
launchBtn.textContent = '📊 Master Mode';
document.body.appendChild(launchBtn);
const panel = document.createElement('div');
panel.id = 'master-panel';
panel.tabIndex = 0;
panel.innerHTML = `
<div class="master-header">
<button id="master-close" title="Escで閉じる">✕</button>
<div class="search-row">
<div class="search-container">
<input type="text" class="search-box" id="master-search" placeholder="タイトルのみ検索...">
<button id="search-clear" class="search-clear">✕</button>
</div>
<div class="hit-counter" id="hit-count">読込中...</div>
<button id="csv-export-btn" title="全データをCSVで保存"><span class="icon-dl">📥</span> CSV出力</button>
</div>
<div class="chip-container" id="control-chips">
<span class="chip-label">検索:</span>
<button class="chip" data-id="mode-full">全文検索(本文含)</button>
<span class="chip-label">表示:</span>
<button class="chip active" data-id="status-all">すべて</button>
<button class="chip" data-id="status-published">公開中</button>
<button class="chip" data-id="status-draft">下書き</button>
<span class="chip-label">並び替え:</span>
<button class="chip active" data-id="sort-date">日付順</button>
<button class="chip" data-id="sort-likes">人気順</button>
</div>
<div id="progress-bar-container"><div id="progress-bar"></div></div>
</div>
<div class="master-main" id="master-scroll-area">
<table class="article-table">
<tbody id="article-list-body"></tbody>
</table>
</div>
`;
document.body.appendChild(panel);
// --- 3. データ取得 ---
async function fetchAll() {
if (allArticles.length > 0) return;
let page = 1; let isLastPage = false;
const pBar = document.getElementById('progress-bar');
while (!isLastPage) {
try {
const res = await fetch(`https://note.com/api/v2/note_list/contents?page=${page}`).then(r => r.json());
totalArticlesCount = res.data.totalCount;
res.data.notes.forEach(note => {
allArticles.push({
key: note.key, status: note.status,
title: note.name || (note.noteDraft ? note.noteDraft.name : "") || "タイトル未設定",
peek: note.peekBody || "",
fullContent: (note.status === 'draft' && note.noteDraft ? note.noteDraft.body : (note.peekBody || "")).replace(/<[^>]*>/g, ''),
likes: note.likeCount || 0, date: note.displayDate, thumb: note.eyecatch || "",
viewUrl: `https://note.com/${note.user.urlname}/n/${note.key}`,
editUrl: `https://editor.note.com/notes/${note.key}/edit/`
});
});
pBar.style.width = `${Math.round((allArticles.length / totalArticlesCount) * 100)}%`;
updateGlobalCounter(allArticles.length, totalArticlesCount, allArticles);
isLastPage = res.data.isLastPage; page++;
if (!isLastPage) await new Promise(r => setTimeout(r, 600));
} catch(e) { console.error("Fetch error:", e); break; }
}
setTimeout(()=> pBar.parentElement.style.display = 'none', 600);
render();
}
function updateGlobalCounter(shownCount, totalCount, list) {
const pub = list.filter(a => a.status === 'published').length;
const hitCountEl = document.getElementById('hit-count');
if (hitCountEl) {
// 修正箇所:display:flex columnに対応するため、行ごとにdivでラップする
hitCountEl.innerHTML = `
<div><span class="hit-main-num">${shownCount}</span> / ${totalCount} 件</div>
<div class="hit-sub-info">( 公開:${pub} | 下書:${list.length - pub} )</div>
`;
}
}
// --- 4. CSVエクスポートロジック ---
function downloadCSV() {
if (allArticles.length === 0) {
alert("記事データがありません。データを読み込んでから実行してください。");
return;
}
const headers = ["ID", "Status", "Title", "Date", "Likes", "ViewURL", "EditURL", "BodySnippet"];
// CSV用エスケープ関数: ダブルクォートを2つに、全体をダブルクォートで囲む
const escapeCsv = (val) => `"${String(val).replace(/"/g, '""')}"`;
const csvRows = [
headers.join(','), // ヘッダー行
...allArticles.map(a => {
return [
escapeCsv(a.key),
escapeCsv(a.status),
escapeCsv(a.title),
escapeCsv(a.date),
escapeCsv(a.likes),
escapeCsv(a.viewUrl),
escapeCsv(a.editUrl),
escapeCsv(a.fullContent.substring(0, 500)) // 本文は長すぎるため500文字で切る
].join(',');
})
];
// BOM付与でExcel文字化け回避
const csvString = '\uFEFF' + csvRows.join('\r\n');
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `note_articles_backup_${new Date().toISOString().slice(0,10)}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// --- 5. 描画・検索・インタラクション ---
const tbody = document.getElementById('article-list-body');
const searchInput = document.getElementById('master-search');
const clearBtn = document.getElementById('search-clear');
tbody.addEventListener('click', (e) => {
const row = e.target.closest('.article-row');
if (row) {
const rows = Array.from(tbody.querySelectorAll('.article-row'));
selectedIndex = rows.indexOf(row);
syncSelection(false);
}
if (e.target.classList.contains('copy-btn')) {
const url = e.target.getAttribute('data-url');
navigator.clipboard.writeText(url).then(() => {
const row = e.target.closest('tr');
row.classList.add('flash-success');
const old = e.target.textContent; e.target.textContent = '完了!';
setTimeout(() => { e.target.textContent = old; row.classList.remove('flash-success'); }, 1000);
});
}
if (e.target.classList.contains('edit-btn')) {
lastClickedKey = e.target.closest('tr').getAttribute('data-key');
render();
}
});
function render() {
const query = searchInput.value.trim();
const queryLower = query.toLowerCase();
const statusFilter = filterStatus;
const sort = sortOrder;
selectedIndex = -1;
searchInput.placeholder = isFullSearch ? "全文検索(下書きは全本文)..." : "タイトルのみ検索...";
clearBtn.style.display = query ? 'block' : 'none';
let filtered = allArticles.filter(a => {
const mStatus = (statusFilter === 'all' || a.status === statusFilter);
const target = isFullSearch ? (a.title + a.fullContent).toLowerCase() : a.title.toLowerCase();
return mStatus && target.includes(queryLower);
});
filtered.sort((a, b) => {
if (sort === 'likes') return b.likes - a.likes;
return new Date(b.date.replace(/\//g, '-')) - new Date(a.date.replace(/\//g, '-'));
});
updateGlobalCounter(filtered.length, totalArticlesCount, filtered);
if (filtered.length === 0) {
tbody.innerHTML = `<tr><td colspan="6" class="empty-state">一致する記事がありません。<span class="reset-link" id="reset-all">リセット</span></td></tr>`;
const resetEl = document.getElementById('reset-all');
if (resetEl) resetEl.onclick = resetAllFilters;
return;
}
const highlight = (text) => query ? text.replace(new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<mark>$1</mark>') : text;
const getSnippet = (text) => {
if (!query || !isFullSearch) return text.substring(0, 80);
const idx = text.toLowerCase().indexOf(queryLower);
if (idx === -1) return text.substring(0, 80);
const start = Math.max(0, idx - 30);
const end = Math.min(text.length, idx + 50);
return (start > 0 ? '...' : '') + text.substring(start, end) + (end < text.length ? '...' : '');
};
let html = ''; let lastMonth = '';
filtered.forEach((a, index) => {
const currentMonth = a.date.substring(0, 7).replace('/', '年') + '月';
if (sort === 'date' && currentMonth !== lastMonth) {
const mPub = filtered.filter(i => i.date.startsWith(a.date.substring(0, 7)) && i.status === 'published').length;
html += `<tr><td colspan="6"><div class="month-divider">${currentMonth}<span class="month-badge">公開 ${mPub} | 下書 ${filtered.filter(i => i.date.startsWith(a.date.substring(0, 7)) && i.status === 'draft').length}</span></div></td></tr>`;
lastMonth = currentMonth;
}
const rowGuideClass = a.key === lastClickedKey ? 'last-active' : '';
const likeClass = a.likes >= 50 ? 'likes-high' : (a.likes >= 10 ? 'likes-mid' : '');
html += `<tr class="article-row ${rowGuideClass}" data-key="${a.key}">
<td class="col-no">${index + 1}</td>
<td class="col-status"><span class="status-badge ${a.status === 'published' ? 'badge-pub' : 'badge-draft'}">${a.status === 'published' ? '公開' : '下書'}</span></td>
<td><div style="display:flex; align-items:center; overflow:hidden;">
${a.thumb ? `<img src="${a.thumb}" class="thumb">` : `<div class="thumb"></div>`}
<div style="overflow:hidden;"><a href="${a.viewUrl}" target="_blank" class="title-link">${highlight(a.title)}</a>
<div style="font-size:12px; color:#7f8c8d; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">${highlight(getSnippet(a.fullContent))}</div></div>
</div></td>
<td class="col-likes ${likeClass}">${a.likes > 0 ? a.likes : '-'}</td>
<td class="col-date">${a.date.split(' ')[0]}</td>
<td class="col-action">
<button class="btn-small copy-btn" data-url="${a.viewUrl}">URLコピー</button>
<a href="${a.editUrl}" target="_blank" class="btn-small edit-btn">編集</a>
</td>
</tr>`;
});
tbody.innerHTML = html;
}
// --- 6. キーボードナビゲーションエンジン ---
function syncSelection(shouldScroll = true) {
const rows = Array.from(tbody.querySelectorAll('.article-row'));
if (rows.length === 0) return;
if (selectedIndex < 0) selectedIndex = 0;
if (selectedIndex >= rows.length) selectedIndex = rows.length - 1;
rows.forEach(r => r.classList.remove('is-selected'));
const target = rows[selectedIndex];
target.classList.add('is-selected');
if (shouldScroll) {
target.scrollIntoView({ block: 'nearest', behavior: 'auto' });
}
}
panel.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
panel.style.display = 'none';
document.body.style.overflow = '';
return;
}
const rows = tbody.querySelectorAll('.article-row');
if (rows.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex++;
syncSelection();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex--;
syncSelection();
} else if (e.key === 'PageDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 10, rows.length - 1);
syncSelection();
} else if (e.key === 'PageUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 10, 0);
syncSelection();
} else if (e.key === 'Home') {
e.preventDefault();
selectedIndex = 0;
syncSelection();
} else if (e.key === 'End') {
e.preventDefault();
selectedIndex = rows.length - 1;
syncSelection();
} else if (e.key === 'Enter' && selectedIndex !== -1) {
const link = rows[selectedIndex].querySelector('.title-link');
if (link) link.click();
}
});
// --- 7. イベントリスナー統合 ---
function resetAllFilters() {
searchInput.value = ''; filterStatus = 'all'; sortOrder = 'date'; isFullSearch = false;
updateChipsUI(); render(); searchInput.focus();
}
function updateChipsUI() {
document.querySelectorAll('.chip').forEach(c => {
const id = c.getAttribute('data-id');
c.classList.remove('active');
if (id === 'mode-full' && isFullSearch) c.classList.add('active');
if (id === `status-${filterStatus}`) c.classList.add('active');
if (id === `sort-${sortOrder}`) c.classList.add('active');
});
}
launchBtn.addEventListener('click', () => { panel.style.display = 'flex'; document.body.style.overflow = 'hidden'; searchInput.focus(); fetchAll(); });
document.getElementById('master-close').addEventListener('click', () => { panel.style.display = 'none'; document.body.style.overflow = ''; });
clearBtn.addEventListener('click', () => { searchInput.value = ''; render(); searchInput.focus(); });
searchInput.addEventListener('input', render);
document.getElementById('csv-export-btn').addEventListener('click', downloadCSV);
document.getElementById('control-chips').addEventListener('click', (e) => {
const chip = e.target.closest('.chip');
if (!chip) return;
const id = chip.getAttribute('data-id');
if (id === 'mode-full') isFullSearch = !isFullSearch;
if (id.startsWith('status-')) filterStatus = id.replace('status-', '');
if (id.startsWith('sort-')) sortOrder = id.replace('sort-', '');
updateChipsUI(); render();
});
})();ステップ2:ブラウザに導入する
フォルダに2つのファイルが入っていることを確認したら、ブラウザに読み込ませます。Google Chrome、またはEdgeでの手順です。
拡張機能の管理画面を開く
Chromeの場合:アドレスバーに chrome://extensions と入力してEnter。
Edgeの場合:edge://extensions と入力してEnter。
デベロッパーモードをONにする
画面右上の「デベロッパーモード」というスイッチをONにします。
フォルダを読み込む
「パッケージ化されていない拡張機能を読み込む」というボタンをクリックします。
先ほど作成した「note-tool」フォルダを選択して、「開く」を押します。
不要になったら?(アンインストール)
このツールが不要になった場合、上記と同じ拡張機能の管理画面から「削除」を押すだけで、PC内にファイルやデータは一切残りません。安心して試してみてください。
これでインストールは完了です!
さっそく、以下の手順で動作確認をしてみてください。
noteのトップページ(https://note.com)にアクセスします。
右上の自分のアイコンをクリックし、メニューから 「記事」(または「自分の記事」)を選んで管理画面(/notes)へ移動します。
ここで必ず、ブラウザの「再読み込み(リロード)」ボタンを一度押してください。(F5キーまたはURLバー横の更新ボタン)
拡張機能はページが読み込まれた瞬間に準備を始めますが、初回だけはリロードしないと反映されないことがあります。
画面右上に、緑色の「📊 Master Mode」というボタンが出現していれば成功です。

図解で解説する使い方









200記事の中から一瞬で発掘。これが「Master Mode」。
4. ご利用いただく前の、正直な話
このツールを安心して、そして長く使っていただくために、いくつか重要なことをお伝えします。
① サーバーへの配慮と「待ち時間」について
初めてボタンを押したとき、全記事の読み込みに時間がかかる場合があります。
目安として、記事数が200件ほどある場合、読み込み完了までに約30秒ほどお待ちいただくことになります。
「少し長いな」と感じるかもしれませんが、これはnoteのサーバーに負荷をかけないよう、人間がページをめくる速度に合わせてゆっくりデータを取得しているためです。
プログレスバーが動いている間は、焦らずコーヒーでも飲みながらお待ちください。一度読み込めば、画面をリロードするまで再読み込みは不要です。
② プライバシーとセキュリティ
このツールは、取得した記事データをあなたのブラウザ(メモリ)の中だけで処理します。
外部のサーバーにデータを送信したり、誰かに共有したりする機能は一切ありません。
また、パスワードや決済情報などにアクセスする権限も持っていませんので、安心してご利用ください。
③ 非公式ツールであること(自己責任)
この拡張機能は、私個人が趣味で開発したものであり、note公式とは一切関係ありません。
noteの仕様変更により、将来的に突然動かなくなる可能性もあります。
また、万が一このツールの使用によって何らかの問題が発生しても、責任を負うことはできません。あくまで「自己責任」の範囲で、便利な実験ツールとしてお楽しみください。
おわりに

ツールはあくまで「手段」です。
この拡張機能の目的は、あなたの記事をきれいに並べることではなく、「探す時間」を減らし、あなたが「新しい記事を書く時間」を増やすことにあります。
もしこのツールが、あなたの創作活動を少しでも快適にし、埋もれていた下書きから名作が生まれるきっかけになれば、開発者としてこれ以上の喜びはありません。
あなたのnoteライフが、より豊かで、静かなものになりますように。
音声解説
#note #プログラミング #エンジニア #Chrome拡張機能 #JavaScript #業務効率化 #コピペで使える #noteの書き方 #ライフハック #Web開発 #生産性向上 #自作ツール #バックアップ #資産形成 #駆け出しエンジニアと繋がりたい #note書き初め #つくってみた
CSV出力機能なしの初期バージョン(自分用の備忘録)
ここから先は
¥ 100
この記事が参加している募集
この記事が気に入ったらチップで応援してみませんか?
