Manifest V3 対応 Chrome 拡張機能の開発・安全設計・ストア公開審査対策ガイド
Manifest V3、Service Worker、Side Panel、DeclarativeNetRequest、パーミッション申請理由書作成まで網羅した、高品質Chrome拡張機能構築プロトコル。
Chrome Manifest V3 エクステンション開発&ストア公開プロトコル
Manifest V3(MV3)に準拠した高品質なChrome拡張機能を構築し、審査リジェクションを防いでChromeウェブストアへ安全に公開するための実践ガイドライン。
Part 1 — 拡張機能の開発(Building Extensions)
必須ルール(Mandatory Rules)
以下のルールは、拡張機能開発で最も頻繁に発生する破壊・バグの原因に対処するものです。違反すると拡張機能が正常に動作しなくなります。
1. アイコン: 実際に存在するファイルのみ参照する(または完全省略)
❌ 失敗例 — 存在しないファイルや、1つのファイルを全サイズで使い回す
"icons": { "16": "icon.png", "48": "icon.png", "128": "icon.png" }
✅ 正しい例 — 各サイズごとに正しいピクセル解像度のファイルを準備する
"icons": { "16": "icons/icon-16.png", "48": "icons/icon-48.png", "128": "icons/icon-128.png" }
(icon-16.png は 16×16px、icon-48.png は 48×48px、icon-128.png は 128×128px)
✅ 省略例 — PNG画像を用意できない場合は manifest から icons を削除する
(icons と default_icon の記述を消せば、Chromeデフォルトのアイコンが使われます)
アイコンファイルを参照する場合は、必ず物理的な画像ファイルを配置してください。 存在しないパスを指定することは厳禁です。
2. サイドパネル: 必ず開くためのトリガーを用意する
manifest.json に "side_panel": {"default_path": "..."} を定義しただけでは、サイドパネルは自動で開きません。開くためのコードが必要です:
// service-worker.js — アイコンクリックでサイドパネルを開く
// ※注意: default_popup が定義されていない場合のみ onClicked が発火します
chrome.action.onClicked.addListener(async (tab) => {
await chrome.sidePanel.open({ windowId: tab.windowId });
});
ポップアップとサイドパネルを両方使う場合は、ポップアップ内に chrome.sidePanel.open() を呼び出すボタンを配置してください。また、chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) を使う場合、プロパティ名は openPanelOnActionIconClick ではなく openPanelOnActionClick です(誤ったプロパティ名は TypeError を引き起こします)。
3. コード実行: サンドボックス化された iframe のみ許可
拡張機能の CSP(コンテンツセキュリティポリシー)により、拡張機能ページ内での eval() や new Function()、インライン <script> は全面的に禁止されています。
// ❌ NG — iframe の DOM に直接アクセスすると SecurityError になる
iframe.contentDocument.write(html);
// ❌ NG — 拡張機能ページ内での eval
eval(userCode); // CSPによりブロックされる
// ✅ 解法A: manifest に sandbox を定義 + postMessage 通信
// manifest.json: { "sandbox": { "pages": ["sandbox.html"] } }
iframe.contentWindow.postMessage({ html, css, js }, '*');
// ✅ 解法B: Blob URL を使用(別オリジン化され拡張機能CSPを回避)
iframe.src = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));
// ✅ 解法C: srcdoc の使用
iframe.srcdoc = `<style>${css}</style>${html}<script>${js}<\/script>`;
4. `tab.url` の参照には `tabs` パーミッションが必要
tabs パーミッションがない場合、tab.url はエラーを出さずに黙って undefined を返します。
// manifest.json — tab.url や tab.title を参照する場合は必須:
{ "permissions": ["tabs"] }
5. 常に async/await を使用する(.then() チェーンの禁止)
// ❌ 悪い例
chrome.tabs.query({active: true, currentWindow: true}).then(tabs => {
chrome.scripting.executeScript({target: {tabId: tabs[0].id}, files: ['content.js']}).then(() => {});
});
// ✅ 良い例
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
runtime.onMessage で非同期処理を行う場合:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
const data = await chrome.storage.local.get('key');
sendResponse({ data });
})();
return true; // 非同期レスポンスのために通信チャンネルを保持
});
6. コンテンツスクリプト: メインスレッドをブロックしない
多数の DOM 要素を操作する場合は、requestAnimationFrame を用いてバッチ処理し、メインスレッドに処理を譲ります:
async function highlightAll(elements) {
const BATCH = 20;
for (let i = 0; i < elements.length; i += BATCH) {
await new Promise(r => requestAnimationFrame(() => {
elements.slice(i, i + BATCH).forEach(el => el.style.backgroundColor = 'yellow');
r();
}));
if (globalThis.scheduler?.yield) await scheduler.yield();
}
}
7. サービスワーカーは一時的 — 変数に状態を保持しない
サービスワーカーは約30秒の非アクティブで停止するため、グローバル変数に保持した状態は消失します。
// ❌ 失敗例 — サービスワーカー停止時にカウントがリセットされる
let count = 0;
chrome.tabs.onUpdated.addListener(() => { count++; });
// ✅ 正しい例 — chrome.storage に永続化し、イベントごとに読み込む
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
if (changeInfo.status !== 'complete') return;
const { count = 0 } = await chrome.storage.local.get('count');
await chrome.storage.local.set({ count: count + 1 });
await chrome.action.setBadgeText({ text: String(count + 1) });
});
タイマー処理には setTimeout/setInterval ではなく chrome.alarms を使用してください。
8. `chrome.identity`: 開発環境と本番環境でのIDの違い
OAuth 認証等を使用する際、開発中のアンパック拡張機能とウェブストア公開後で拡張機能IDが変わります。
開発中のIDを固定するには、manifest.json に "key" フィールドを追加します。
9. コンテキストメニュー: 実行後にフィードバックを表示する
コンテキストメニューの操作(保存、コピー等)が完了したら、トースト表示や通知、バッジ変更でユーザーに完了をフィードバックしてください。
10. Prompt API: サービスワーカー・ポップアップ・サイドパネルで利用可能
Chrome組み込みAIの LanguageModel APIは、特別なパーミッションなしで全拡張機能コンテキストで利用できます。
const params = await LanguageModel.params();
// { defaultTopK: 3, maxTopK: 128, defaultTemperature: 1, maxTemperature: 2 }
11. `chrome.action` API には manifest 内の "action" キーが必須
chrome.action.setBadgeText や chrome.action.setIcon を使う場合、manifest.json に "action" キー(空のオブジェクト {} でも可)の宣言が必要です。無い場合は chrome.action が undefined になります。
12. `activeTab` は直接のユーザー操作でのみ発火する
activeTab はアイコンクリック、コンテキストメニュー、ショートカットキー操作時のみ一時アクセス権を付与します。サイドパネル内のボタンクリックなどプログラム経由の操作では付与されないため、tabs + host_permissions を明示指定します。
13. DevTools パネルのパスは拡張機能ルート相対指定
chrome.devtools.panels.create() に渡す HTML パスは、devtools/ からではなく拡張機能のルート相対パスで指定します。
14. Offscreen ドキュメントはほとんどの chrome.* API が使用不可
Offscreen ドキュメント内では chrome.downloads や chrome.action などが使えません。chrome.runtime.sendMessage 通信経由でサービスワーカー側に処理を委任します。
15. 通知やアイコンには実在する画像パスを指定する
chrome.notifications.create() に存在しない画像ファイルを指定するとエラーが発生します。ファイルを用意するか、OffscreenCanvas から生成した Data URL を指定します。
16. タブキャプチャ: 二重起動を防止するステートロック
chrome.tabCapture.getMediaStreamId() の二重呼び出しを防ぐため、chrome.storage.session 等を用いて 'idle' → 'starting' → 'recording' の状態遷移フラグを制御します。
17. `chrome.desktopCapture`: targetTab と tabs パーミッションが必須
サービスワーカーから chrome.desktopCapture.chooseDesktopMedia() を呼ぶ際は、"tabs" パーミッションを宣言し、アクティブな targetTab を渡す必要があります。
18. `chrome.windows`: `.query()` メソッドは存在しない
chrome.windows には .query() メソッドがありません。getAll, getLastFocused, getCurrent を使用します。
Part 2 — Chrome ウェブストアへの公開手順(Publishing to the Chrome Web Store)
プロジェクト直下に CHROMEWEBSTORE.md を作成し、ストア掲載情報、パーミッションの申請理由(Justifications)、プライバシーポリシー、バージョン履歴を一元管理します。
審査リジェクションを防ぐポイント
- パーミッション申請理由: 「機能に必要だから」などの漠然とした理由は不可。「ユーザーが選択したテキストをローカル保存・翻訳するために tabs および storage を使用する」のように具体的な理由を記述する。
- プライバシーポリシー: 収集するデータ項目(ストレージ保存内容等)と完全に一致する外部公開URLを準備する。
- 画像アセット: 1280×800 または 640×400 ピクセルのスクリーンショットを最低1枚準備する。
- ZIPアーカイブの作成:
.git/,node_modules/,.env,CHROMEWEBSTORE.mdを除外して ZIP 化する。
チェックリスト(Output Checklist)
- [ ]
manifest_version: 3を指定(V2 APIは一切不可) - [ ] manifest 内で参照するアイコンファイルがすべて実在する
- [ ] サイドパネルを開く明確なJavaScriptトリガーが用意されている
- [ ] コード実行に
eval()を使わず、sandbox / blob / srcdoc を使用している - [ ]
tab.urlを参照する箇所にtabsパーミッションを宣言している - [ ] すべての非同期処理で
async/awaitを使用している - [ ] サービスワーカーの変数はグローバルに保持せず
chrome.storageに永続化している - [ ] HTML内にインラインスクリプトやインラインイベントハンドラが含まれていない
- [ ] Offscreen ドキュメントからは
chrome.runtimeメッセージングのみを使用している - [ ]
chrome.windowsの呼出しで存在しない.query()を使っていない