Chromeの拡張機能を自作

Chromeには拡張機能をインストールすることで、基本機能に無い機能を追加することが出来ます。
簡単なサンプルを作って、作り方、仕組みを解説します。

カスタムキーワードブロッカーの製作

今回製作するのは、カスタムワードブロッカーです。

Webページを閲覧する際、特定のキーワード(ネタバレやノイズ情報など)を含む要素を自動的に非表示にしたいケースがあります。
本記事では、Manifest V3に対応したChrome拡張機能を自作し、複数のキーワード登録・個別ON/OFF切り替えも行います。

概要と構成ファイル

最小限の構成でスムーズな操作性を実現するために以下の4つのファイルで構築しています。

JSON
{
  "manifest_version": 3,
  "name": "カスタムキーワードブロッカー",
  "version": "1.0",
  "description": "キーワードを登録・ON/OFFしてページ内の要素を非表示にします",
  "permissions": [
    "storage"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_end"
    }
  ]
}
"manifest_version": 3: 現在の標準である Manifest V3 を指定しています。セキュリティとパフォーマンスが強化された最新の仕様です。
"permissions": ["storage"]:ユーザーが登録したキーワードやON/OFFの状態をブラウザに保存するため、ストレージAPIの権限を要求しています。
"host_permissions": ["<all_urls>"]:すべてのWebサイト(*://*/*)上でキーワードブロックの処理を行わせるための権限です。
全サイト(<all_urls>)ではなく特定のドメインのみで動かしたい場合は、host_permissions と matches の指定を ["[https://news.yahoo.co.jp/](https://news.yahoo.co.jp/)"] などに変更してください。
"content_scripts":どのタイミングで、どのスクリプトをWebページに埋め込むかを定義します。ここではすべてのURLに対して content.js を注入しています。
  • popup.html(UI画面)
    拡張機能のアイコンをクリックした際に表示される設定パネルの見た目を定義します。
    styleの設定と、ポップアップする画面の設定。実行(登録)するファイルの設定を行います。
HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <style>
        body { width: 300px; font-family: sans-serif; padding: 12px; margin: 0; background: #f9f9f9; }
        h3 { margin-top: 0; font-size: 14px; color: #333; }
        .input-group { display: flex; margin-bottom: 12px; gap: 6px; }
        input[type="text"] { flex: 1; padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
        button#addBtn { padding: 6px 12px; background: #0066cc; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
        button#addBtn:hover { background: #0052a3; }
        ul { list-style: none; padding: 0; margin: 0; max-height: 220px; overflow-y: auto; background: #fff; border: 1px solid #ddd; border-radius: 4px; }
        li { display: flex; align-items: center; justify-content: space-between; padding: 8px; border-bottom: 1px solid #eee; }
        li:last-child { border-bottom: none; }
        .keyword-info { display: flex; align-items: center; gap: 8px; }
        .delete-btn { background: none; border: none; color: #cc0000; cursor: pointer; font-size: 12px; }
        .delete-btn:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <h3>キーワードブロッカー設定</h3>
    <div class="input-group">
        <input type="text" id="keywordInput" placeholder="キーワードを追加">
        <button id="addBtn">追加</button>
    </div>
    <ul id="keywordList"></ul>
    <script src="popup.js"></script>
</body>
</html>
body { width: 300px; ... } と指定することで、Chromeの仕様に沿った見やすい小窓サイズ(幅300px)のパネルを実現しています。
キーワード入力用のテキストボックス(keywordInput)、追加ボタン(addBtn)、および動的にリストを表示する領域(keywordList)という最小限かつ直感的なパーツで構成されています。
  • popup.js(UI側ロジック)
    パネル上でのユーザー操作(キーワード追加・チェックボックスON/OFF・削除)を検知し、ブラウザのストレージ(chrome.storage)に保存・更新します。
JavaScript
document.addEventListener('DOMContentLoaded', () => {
    const keywordInput = document.getElementById('keywordInput');
    const addBtn = document.getElementById('addBtn');
    const keywordList = document.getElementById('keywordList');

    loadKeywords();

    addBtn.addEventListener('click', () => {
        const text = keywordInput.value.trim();
        if (!text) return;

        chrome.storage.sync.get({ keywords: [] }, (data) => {
            const keywords = data.keywords;
            if (!keywords.some(k => k.text === text)) {
                keywords.push({ text: text, enabled: true });
                chrome.storage.sync.set({ keywords }, () => {
                    keywordInput.value = '';
                    loadKeywords();
                });
            }
        });
    });

    function loadKeywords() {
        chrome.storage.sync.get({ keywords: [] }, (data) => {
            keywordList.innerHTML = '';
            data.keywords.forEach((kw, index) => {
                const li = document.createElement('li');

                const infoDiv = document.createElement('div');
                infoDiv.className = 'keyword-info';

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.checked = kw.enabled;
                checkbox.addEventListener('change', () => {
                    data.keywords[index].enabled = checkbox.checked;
                    chrome.storage.sync.set({ keywords: data.keywords });
                });

                const span = document.createElement('span');
                span.textContent = kw.text;

                infoDiv.appendChild(checkbox);
                infoDiv.appendChild(span);

                const delBtn = document.createElement('button');
                delBtn.className = 'delete-btn';
                delBtn.textContent = '削除';
                delBtn.addEventListener('click', () => {
                    data.keywords.splice(index, 1);
                    chrome.storage.sync.set({ keywords: data.keywords }, () => {
                        loadKeywords();
                    });
                });

                li.appendChild(infoDiv);
                li.appendChild(delBtn);
                keywordList.appendChild(li);
            });
        });
    }
});
DOMContentLoaded による初期化:
ポップアップが開かれた瞬間に loadKeywords() を呼び出し、保存されている現在のキーワード一覧をストレージから取得してHTML(リスト)を組み立て直します。
追加ボタンが押された際、すでに同じキーワードが登録されていないか some() メソッドでガードしつつ、新しいオブジェクト { text: text, enabled: true } を配列に追加して chrome.storage.sync に書き込んでいます。
document.createElement を使って li、checkbox、span、deleteBtn を安全に一つずつ生成しています。
チェックボックスの change イベントや削除ボタンの click イベントの中で直接ストレージを書き換えるため、ユーザーがUIを操作した結果が瞬時にブラウザ全体(および content.js 側)に同期されます。
  • content.js(Webページ側ロジック)
    閲覧中のWebページ上で動作し、保存された有効なキーワードに基づいて該当する要素を非表示(または再表示)にします。
JavaScript
(function() {
    'use strict';

    function applyBlock() {
        chrome.storage.sync.get({ keywords: [] }, (data) => {
            const activeKeywords = data.keywords
                .filter(k => k.enabled)
                .map(k => k.text);

            // ページ内の要素を走査
            const elements = document.querySelectorAll('p, div, article, li, section');

            elements.forEach(el => {
                const text = el.innerText;
                if (!text) return;

                let shouldHide = false;
                for (let keyword of activeKeywords) {
                    if (text.includes(keyword)) {
                        shouldHide = true;
                        break;
                    }
                }

                if (shouldHide) {
                    el.style.display = 'none';
                    el.dataset.keywordBlocked = "true";
                } else {
                    // チェックを外したり削除したときは再表示する
                    if (el.dataset.keywordBlocked === "true") {
                        el.style.display = '';
                        delete el.dataset.keywordBlocked;
                    }
                }
            });
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', applyBlock);
    } else {
        applyBlock();
    }

    // 無限スクロールなどで動的に増えるコンテンツに対応
    const observer = new MutationObserver(() => {
        applyBlock();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // ★【追加】ポップアップ側での追加・変更・削除をリアルタイム検知して即座に反映
    chrome.storage.onChanged.addListener((changes, areaName) => {
        if (areaName === 'sync' && changes.keywords) {
            applyBlock();
        }
    });
})();
即時関数 (function() { ... })() と 'use strict';
Chrome拡張機能のコンテンツスクリプトは、Webページの元のJavaScriptとグローバル変数が競合(衝突)するのを防ぐため、スコープを独立させる「即時関数」で囲むのがお約束です。
chrome.storage.sync.get で保存されているキーワードを読み込み、有効な(enabled: true)ものだけを抽出します。
document.querySelectorAll('p, div, article, li, section') でページ内の主要なテキストコンテナ要素を網羅的に取得し、innerText にキーワードが含まれているか判定します。
マッチした要素は el.style.display = 'none'; で隠し、カスタムデータ属性 el.dataset.keywordBlocked = "true" を付与して「自分が隠した」目印をつけます。
ユーザーが後からキーワードのチェックを外したり削除したりした場合、else 側の処理で el.style.display = '';(初期値に戻す)と delete el.dataset.keywordBlocked; が実行され、画面を再読み込みしなくてもリアルタイムで要素が元通りに復活する
MutationObserver(動的ページの監視):
近年のWebサイト(Twitter/XやYouTubeなど)は、スクロールや画面遷移なしでコンテンツが次々と追加されるSPA(シングルページアプリケーション)が主流です。
MutationObserver を使うことで、DOMが動的に変化した瞬間に自動で applyBlock() が走り、新しく追加されたノイズも逃さずブロックできます。
chrome.storage.onChanged.addListener(リアルタイムの同期)
開いているタブ側の content.js がそれを検知して即座に画面を書き換えるためのリスナーです。
これにより、ポップアップ側でチェックボックスをON/OFFしたりキーワードを追加・削除した瞬間に、開いているWebページ側へ即座に設定変更が反映されます。

導入手順

  1. 適当なフォルダ(例: keyword-blocker)を作り、4つのファイルを同じ階層に保存する。
  2. Google Chromeで chrome://extensions/ にアクセスします。
  3. 画面右上にある「デベロッパーモード」をONにします。
  4. 画面左上の「パッケージ化されていない拡張機能を読み込む」をクリックし、上記ファイルを入れたフォルダを選択します。

カスタマイズのヒント

  • 非表示対象のタグを変更する (content.js)
    非表示にする要素の粒度を変更したい場合は、document.querySelectorAll('p, div, article, li, section') の対象タグを書き換えます。
    例えば記事ブロック単位に絞りたい場合は 'article, li' のように制限することで、動作負荷を軽減できます。
  • 特定サイトのみに限定する (manifest.json)
    全サイト(<all_urls>)ではなく特定のドメインのみで動かしたい場合は、host_permissions と matches の指定を ["[https://news.yahoo.co.jp/](https://news.yahoo.co.jp/)*"] などに変更してください。
  • 大文字・小文字を区別しない検索 (content.js)
    あいまい検索を行いたい場合は、判定部を text.toLowerCase().includes(keyword.toLowerCase()) に書き換えます。

注意事項

 大量のDOM要素が存在するページや動作が重いSNS等では、要素走査の影響で動作に影響が出る場合があります。必要に応じて監視対象のタグを絞り込んでご利用ください。
 拡張機能のコード更新時に「パッケージ化されていない拡張機能」を再読み込みすると、すでに開いているタブで Uncaught Error: Extension context invalidated. というエラーが出ることがあります。その場合は対象のWebページを一度リロードすることで解消されます。

免責事項

 本コードは学習および技術検証を目的としたサンプルです。導入および利用によって生じた直接的・間接的な損害について、当社は一切の責任を負いかねますのであらかじめご了承ください。

← 技術記事一覧へ戻る