2つのWebサイトを並べて Gemini AI に比較させる Chrome 拡張機能「DualyzeAI」を作り、Chrome ウェブストアに公開しました。
作ってみて想定外に詰まったポイントが複数あったので、実装の記録として残しておきます。
拡張機能の概要
- 2つのURLを入力するとサイドバイサイドで表示
- 6種類のAI分析モード(比較・価格比較・メリデメ・要約・要点・おすすめ判定)
- サイト同士だけでなく「サイト + テキスト」「テキスト + テキスト」にも対応
- BYOK(Bring Your Own Key)方式で Gemini API キーを使用
- Manifest V3(MV3)/ 純粋な JavaScript(ビルドツールなし)
詰まりポイント① iframe がほぼ全滅する問題
問題
X-Frame-Options: SAMEORIGIN や Content-Security-Policy: frame-ancestors 'none' を設定しているサイトは iframe でロードできません。Amazon・Google・Apple・Samsung など、比較したいサイトの大半がこれに該当しました。
解決策:declarativeNetRequest でレスポンスヘッダーを除去
Manifest V3(MV3)では declarativeNetRequest API でレスポンスヘッダーをルールベースで書き換えることができます。
rules.json
[
{
"id": 1,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{ "header": "X-Frame-Options", "operation": "remove" },
{ "header": "Content-Security-Policy", "operation": "remove" }
]
},
"condition": {
"resourceTypes": ["sub_frame"]
}
}
]
manifest.json
{
"declarative_net_request": {
"rule_resources": [{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}]
},
"permissions": ["declarativeNetRequest", "declarativeNetRequestFeedback"]
}
sub_frame のみに適用することで、メインフレームのリクエストには影響しません。これでほぼすべてのサイトを表示できるようになりました。
詰まりポイント② iframe 内のページ遷移を追跡できない問題
問題
iframe 内でリンクをクリックしてページが遷移したとき、上部のURLバーを更新したいのですが、iframe.src はリダイレクト後のURLを反映しません。chrome.webNavigation.getAllFrames でURLの完全一致を試みても、リダイレクトが挟まるとマッチしません。
たとえば google.com にアクセスすると www.google.com にリダイレクトされるため、getAllFrames で google.com を探しても見つからないケースが発生します。
解決策:onCompleted + ホスト名照合で frameId を特定
let frame1Id = null;
let pendingHost1 = null; // ロード中のホスト名
chrome.webNavigation.onCompleted.addListener((details) => {
if (!currentTabId || details.tabId !== currentTabId) return;
if (details.frameId === 0) return; // メインフレームは無視
if (!details.url.startsWith('http')) return;
const host = new URL(details.url).hostname.replace(/^www\./, '');
// 初回: ホスト名マッチで frameId を取得
if (frame1Id === null && pendingHost1 && host === pendingHost1) {
frame1Id = details.frameId;
pendingHost1 = null;
url1Input.value = details.url;
return;
}
// 以降: frameId で追跡
if (details.frameId === frame1Id) {
url1Input.value = details.url;
}
});
function loadFrame(iframe, url) {
frame1Id = null;
pendingHost1 = new URL(url).hostname.replace(/^www\./, '');
iframe.src = url;
}
「初回だけホスト名で照合して frameId を特定し、それ以降は frameId で追跡する」という2段階の設計にすることで、リダイレクトがあっても確実に追跡できるようになりました。
詰まりポイント③ クォータエラーの自動フォールバックが逆効果
問題
Gemini API の無料枠には上限があり、超えると 429 が返ってきます。最初は Flash が失敗したら自動で Flash Lite に切り替える実装にしていましたが、これだと:
- Flash でリクエスト失敗(クォータを消費)
- Flash Lite で再リクエスト(さらにクォータを消費)
- Flash Lite も失敗
消費が加速する一方で、待ち時間も延びてしまいました。
解決策:ユーザーに確認してから切り替える
// background.js: エラー情報を返す
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') ?? '60');
return {
error: 'QUOTA_EXCEEDED',
retryAfter,
failedModel: model
};
}
// viewer.js: ダイアログで確認してから切り替え
if (result.error === 'QUOTA_EXCEEDED') {
const fallback = result.failedModel.includes('flash-lite')
? 'gemini-2.5-flash'
: 'gemini-2.5-flash-lite';
if (confirm(`${currentModelName}のレート制限に達しました。${fallbackName}に切り替えますか?`)) {
analyze({ modelOverride: fallback });
} else {
startCountdown(result.retryAfter);
}
}
カウントダウンは Date.now() ベースで実装しています。setInterval はバックグラウンドタブで停止することがあるため、visibilitychange で復帰時に残り時間を再計算しています。
function startCountdown(seconds) {
const resetAt = Date.now() + seconds * 1000;
function tick() {
const remaining = Math.ceil((resetAt - Date.now()) / 1000);
if (remaining <= 0) {
countdownEl.textContent = '';
return;
}
countdownEl.textContent = `リセットまで ${remaining}秒`;
requestAnimationFrame(tick);
}
document.addEventListener('visibilitychange', () => {
if (!document.hidden) tick();
});
tick();
}
Chrome Web Store 審査で気をつけたこと
declarativeNetRequest でヘッダーを書き換える拡張機能は審査で引っかかりやすいと思っていましたが、結果的に一発通過でした(申請から公開まで約6日)。
申請時に意識した点をまとめます。
Why do you need this permission? に具体的なユースケースを書く
「ユーザーが任意の2サイトを並列表示するために X-Frame-Options と CSP を除去する必要がある。対象は sub_frame のみで、メインフレームには影響しない」という内容を記述しました。
プライバシーポリシーを用意する
BYOK なのでサーバーサイドでのデータ収集は一切ありませんが、それでもプライバシーポリシーのページを用意してURLを登録しました。
APIキーの扱いを説明文に明記する
chrome.storage.local に保存するだけで外部サーバーに送信しないことを、ストアの説明文にも明示しました。
まとめ
| 課題 | 解決策 |
|---|---|
| iframe が X-Frame-Options でブロックされる |
declarativeNetRequest でレスポンスヘッダーを除去 |
| iframe 内のナビゲーションを追跡できない |
onCompleted + ホスト名照合で frameId を特定 |
| 自動フォールバックがクォータを加速消費する | ユーザー確認ダイアログ方式に変更 |
| バックグラウンドタブでカウントダウンが止まる |
Date.now() ベース + visibilitychange で再計算 |
Chrome 拡張(Manifest V3)でiframeを扱う機会がある方の参考になれば幸いです。
公式サイト: https://dualyzeai.com

