このチュートリアルでは、実用に耐える最小構成のChrome Manifest V3試作版を作ります。Pinterestのピン詳細ページに非公開ノート用のテキストエリアを表示し、数値のピンIDをキーとしてchrome.storage.localへ保存します。これは意図的に機能を絞った試作版であり、Pinterestに以前あったノート機能の完成品を再現するものではありません。
PinterestのDOMは予告なく変わる可能性があります。セレクターを一か所へまとめ、Pinterestの商標や保護された外観をまねず、対応すると示す各ページ種別と言語環境でテストしてください。
1. Manifest V3拡張機能を作る
新しいフォルダーを作り、manifest.jsonとcontent.jsを置きます。Chromeのマニフェストの公式リファレンスで利用可能なキーを確認してください。
{
"manifest_version": 3,
"name": "Pin Context Prototype",
"version": "0.1.0",
"description": "Pinterestのピンに非公開のローカルノートを保存します。",
"permissions": ["storage"],
"content_scripts": [{
"matches": ["https://*.pinterest.com/*"],
"js": ["content.js"],
"run_at": "document_idle"
}]
}
Pinterestはクライアント側で画面を切り替えるため、この例のURL一致範囲は広めです。対応する経路や国別ドメインが限られるなら、範囲を狭め、その条件を文書に明記してください。
2. ピンURLから安定したキーを得る
カードの表示位置や画面上のタイトルをノートのキーにしないでください。正規の/pin/…/パスから数値IDを取り出し、それ以外ではnullを返します。
function getPinId(url = location.href) {
try {
const { pathname } = new URL(url);
const match = pathname.match(/\/pin\/(\d+)\/?/);
return match ? match[1] : null;
} catch {
return null;
}
}
正しい詳細URL、クエリ文字列付きURL、ピン以外のページ、不正なURL、対応すると示す各言語のURLパターンをテストします。IDを特定できないときに、黙ってノートを紐づけてはいけません。
3. バージョン付きのレコードを保存する
将来データを移行できるよう、スキーマを明示します。
async function readNote(pinId) {
const key = 'pin-note:' + pinId;
const result = await chrome.storage.local.get(key);
return result[key] ?? { version: 1, pinId, text: '', updatedAt: null };
}
async function writeNote(pinId, text) {
const key = 'pin-note:' + pinId;
const record = {
version: 1,
pinId,
text,
updatedAt: new Date().toISOString()
};
await chrome.storage.local.set({ [key]: record });
return record;
}
Chromeの公式資料では、storage.localの標準上限は10MBで、上限を増やすには別途unlimitedStorage権限を要求すると説明されています。また、拡張機能を削除するとローカルの拡張機能用ストレージも消去されます。最新のStorage API公式ドキュメントを確認し、重要なデータを保存してもらう前にエクスポート機能を設計してください。
4. スタイルを分離した、アクセシブルなエディターを表示する
Shadow DOMを使うと、ページ側のスタイルとの意図しない衝突を減らせます。この例はページのbodyへ追加するため、変わりやすいPinterestのクラス名には依存しません。
async function mountEditor(pinId) {
if (document.querySelector('[data-pin-context-root]')) return;
const host = document.createElement('aside');
host.dataset.pinContextRoot = '';
host.setAttribute('aria-label', 'ピンの非公開ノート');
document.body.append(host);
const root = host.attachShadow({ mode: 'open' });
const label = document.createElement('label');
label.textContent = '非公開ノート';
const editor = document.createElement('textarea');
editor.id = 'pin-note-editor';
label.htmlFor = editor.id;
editor.setAttribute('aria-label', 'このピンの非公開ノート');
const status = document.createElement('span');
status.setAttribute('role', 'status');
const saved = await readNote(pinId);
editor.value = saved.text;
root.append(label, editor, status);
bindAutosave(editor, status, pinId);
}
インターフェースの文字列はtextContentで設定し、信頼できない内容をinnerHTMLへ渡さないでください。見やすいフォーカス表示、十分なコントラスト、実際のlabel要素、キーボード操作、フォーカスを奪わない状態表示も加えます。
5. キー入力のたびに書き込まず保存する
function bindAutosave(editor, status, pinId) {
let saveTimer;
editor.addEventListener('input', () => {
status.textContent = '未保存';
clearTimeout(saveTimer);
saveTimer = setTimeout(async () => {
try {
await writeNote(pinId, editor.value);
status.textContent = 'ローカルに保存済み';
} catch (error) {
console.error('ピンノートの保存に失敗しました', error);
status.textContent = '保存できませんでした';
}
}, 400);
});
}
製品版ではさらに、ページを移動する前に未保存の入力を書き込み、二つのタブによる上書き競合を防ぎ、ノートの最大容量を定め、保存上限のエラーを利用者へ表示します。
6. Pinterestのクライアント側ナビゲーションへ対応する
ページ読み込み時に一度だけ動く処理では、フィードと詳細画面の間の移動を検知できません。何度実行しても状態を壊さない調整関数を用意し、DOMが変わるたびに再表示しない形で変更を監視します。
let scheduled = false;
async function reconcile() {
scheduled = false;
const pinId = getPinId();
const existing = document.querySelector('[data-pin-context-root]');
if (!pinId) {
existing?.remove();
return;
}
if (existing?.dataset.pinId === pinId) return;
existing?.remove();
await mountEditor(pinId);
const mounted = document.querySelector('[data-pin-context-root]');
if (mounted) mounted.dataset.pinId = pinId;
}
new MutationObserver(() => {
if (scheduled) return;
scheduled = true;
requestAnimationFrame(reconcile);
}).observe(document.documentElement, { childList: true, subtree: true });
reconcile();
製品版では、可能ならナビゲーション状態も直接監視し、不要になったオブザーバーを切断します。長いフィード上で性能を計測してください。機能が正しくても、ページを遅くするなら良い拡張機能とはいえません。
7. Manifest V3のセキュリティ制約を守る
Manifest V3では、外部でホストされた実行コードを使えません。実行するJavaScriptは拡張機能へ同梱し、正当な理由がなければ標準のコンテンツセキュリティポリシーを維持し、動的なコード実行を避け、機能に必要な権限だけを要求します。ChromeのManifest V3の概要にプラットフォームの考え方が説明されています。
8. 実用製品に必要な機能を加える
- スキーマ検証と競合処理を備えたJSONのエクスポート・インポート
- 復元時に人が読めるエクスポート
- 検索と任意のタグ
- 使用中の保存容量と上限の表示
- ノート単位と全データの削除
- 実際に確認できる動作と一致するプライバシーポリシー
- スキーマの各バージョン間の移行テスト
- アンインストール時のデータ消失と、同期しない場合についての明確な警告
- 収集量を慎重に減らし、内容を開示したエラーテレメトリー
9. 失敗する場合もテストする
- パッケージ化されていない拡張機能を
chrome://extensionsから読み込む。 - フィード、ボード、ピン詳細ページ、ピン以外のページを開く。
- 再読み込みせず二つのピン間を移動し、ノートが混ざらないことを確認する。
- 同じピンを二つのタブで開き、競合時の動作を確認する。
- ストレージを無効にするか容量超過を再現し、警告が表示されることを確認する。
- エクスポート後にテスト用レコードを削除し、復元する。
- キーボード操作、拡大表示、スクリーンリーダー用ラベル、明るいページと暗いページでの見え方を確認する。
設計上の要点
テキストエリアを置くこと自体は簡単です。信頼できるPinterest拡張機能には、安定した識別子、画面遷移に耐える処理、アクセシブルなUI、必要最小限の権限、説明された保存方式、検証済みのデータ持ち出し手段が必要です。機能を増やす前に、この土台を作ってください。
Pinterestの整理方法をさらに改善する
次の目的に合うガイドへ進んでください。