Pinterestノート用Chrome拡張機能の作り方

Salo 執筆:Salo 読了目安:5分
Pinterestノート用Chrome拡張機能の作り方

このチュートリアルでは、実用に耐える最小構成の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. 実用製品に必要な機能を加える

9. 失敗する場合もテストする

  1. パッケージ化されていない拡張機能をchrome://extensionsから読み込む。
  2. フィード、ボード、ピン詳細ページ、ピン以外のページを開く。
  3. 再読み込みせず二つのピン間を移動し、ノートが混ざらないことを確認する。
  4. 同じピンを二つのタブで開き、競合時の動作を確認する。
  5. ストレージを無効にするか容量超過を再現し、警告が表示されることを確認する。
  6. エクスポート後にテスト用レコードを削除し、復元する。
  7. キーボード操作、拡大表示、スクリーンリーダー用ラベル、明るいページと暗いページでの見え方を確認する。

設計上の要点

テキストエリアを置くこと自体は簡単です。信頼できるPinterest拡張機能には、安定した識別子、画面遷移に耐える処理、アクセシブルなUI、必要最小限の権限、説明された保存方式、検証済みのデータ持ち出し手段が必要です。機能を増やす前に、この土台を作ってください。

Pinterestの整理方法をさらに改善する

次の目的に合うガイドへ進んでください。

Salo

著者について

Saloはプロダクトデザイナーであり、Pinterestのヘビーユーザーです。デジタル情報の整理術やPinterestの活用法、思考を助けるツールについて執筆しています。

Chrome版Notestopinを試す

ピンに非公開メモとタグを追加して、保存したアイデアをあとからすぐに見つけられます。

Chromeに追加