chrome-extension

tmhr1850's avatarfrom tmhr1850

Chrome拡張機能(Manifest V3)の開発ガイド。tabCapture、Content Scripts、Service Worker、Offscreen Documentの実装時に使用。

0stars🔀0forks📁View on GitHub🕐Updated Jan 8, 2026

When & Why to Use This Skill

This Claude skill serves as a comprehensive development guide and boilerplate for building modern Chrome Extensions using Manifest V3. It provides production-ready templates for complex browser APIs, including tabCapture for audio processing, Offscreen Documents for media handling, and Service Workers for background logic, significantly accelerating the development lifecycle for browser-based tools.

Use Cases

  • Case 1: Building AI-powered meeting transcription tools that require capturing high-quality audio directly from browser tabs like Google Meet, Zoom, or Microsoft Teams.
  • Case 2: Developing browser extensions with advanced UI requirements using the Side Panel API and React for a seamless user experience.
  • Case 3: Implementing real-time web page monitoring and DOM manipulation using Content Scripts and MutationObservers to detect specific user states or page changes.
  • Case 4: Migrating legacy Manifest V2 extensions to Manifest V3, specifically handling the transition of background pages to Service Workers and implementing Offscreen Documents for restricted APIs.
namechrome-extension
descriptionChrome拡張機能(Manifest V3)の開発ガイド。tabCapture、Content Scripts、Service Worker、Offscreen Documentの実装時に使用。
allowed-toolsRead, Write, Edit, Bash, Glob, Grep

Chrome Extension Skill (Manifest V3)

会議音声キャプチャ用Chrome拡張機能の開発ガイド。

プロジェクト構成

apps/extension/
├── src/
│   ├── manifest.ts          # Manifest V3定義
│   ├── background/
│   │   └── service-worker.ts # Background Service Worker
│   ├── content/
│   │   ├── index.ts         # Content Script
│   │   └── meet-detector.ts # Google Meet検出
│   ├── offscreen/
│   │   └── recorder.ts      # Offscreen音声録音
│   ├── sidepanel/
│   │   └── App.tsx          # Side Panel UI
│   └── popup/
│       └── App.tsx          # Popup UI
├── vite.config.ts
└── package.json

Manifest V3 設定

// manifest.ts
import { defineManifest } from '@crxjs/vite-plugin';

export default defineManifest({
  manifest_version: 3,
  name: 'Meeting Transcriber',
  version: '1.0.0',
  permissions: [
    'tabCapture',
    'offscreen',
    'storage',
    'sidePanel',
    'activeTab',
  ],
  host_permissions: [
    'https://meet.google.com/*',
    'https://*.zoom.us/*',
    'https://teams.microsoft.com/*',
  ],
  background: {
    service_worker: 'src/background/service-worker.ts',
    type: 'module',
  },
  content_scripts: [
    {
      matches: [
        'https://meet.google.com/*',
        'https://*.zoom.us/*',
      ],
      js: ['src/content/index.ts'],
    },
  ],
  side_panel: {
    default_path: 'src/sidepanel/index.html',
  },
  action: {
    default_popup: 'src/popup/index.html',
    default_icon: {
      '16': 'icons/icon16.png',
      '48': 'icons/icon48.png',
      '128': 'icons/icon128.png',
    },
  },
});

tabCapture による音声キャプチャ

Service Worker

// background/service-worker.ts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'START_CAPTURE') {
    startCapture(sender.tab!.id!);
    sendResponse({ success: true });
  }
  return true;
});

async function startCapture(tabId: number) {
  // Offscreen documentを作成
  await chrome.offscreen.createDocument({
    url: 'src/offscreen/index.html',
    reasons: [chrome.offscreen.Reason.USER_MEDIA],
    justification: 'Recording tab audio for transcription',
  });

  // tabCaptureでストリームID取得
  const streamId = await chrome.tabCapture.getMediaStreamId({
    targetTabId: tabId,
  });

  // Offscreenに送信
  chrome.runtime.sendMessage({
    type: 'START_RECORDING',
    streamId,
    tabId,
  });
}

Offscreen Document

// offscreen/recorder.ts
let mediaRecorder: MediaRecorder | null = null;

chrome.runtime.onMessage.addListener(async (message) => {
  if (message.type === 'START_RECORDING') {
    await startRecording(message.streamId);
  }
  if (message.type === 'STOP_RECORDING') {
    stopRecording();
  }
});

async function startRecording(streamId: string) {
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: {
      mandatory: {
        chromeMediaSource: 'tab',
        chromeMediaSourceId: streamId,
      },
    } as any,
  });

  mediaRecorder = new MediaRecorder(stream, {
    mimeType: 'audio/webm;codecs=opus',
  });

  mediaRecorder.ondataavailable = async (e) => {
    if (e.data.size > 1024) { // 無音スキップ
      const arrayBuffer = await e.data.arrayBuffer();
      chrome.runtime.sendMessage({
        type: 'AUDIO_CHUNK',
        data: Array.from(new Uint8Array(arrayBuffer)),
      });
    }
  };

  // 5秒ごとにチャンク生成
  mediaRecorder.start(5000);
}

function stopRecording() {
  mediaRecorder?.stop();
  mediaRecorder = null;
}

Content Script(会議検出)

// content/meet-detector.ts
class MeetingDetector {
  private isInMeeting = false;

  constructor() {
    this.observeMeetingState();
  }

  private observeMeetingState() {
    // Google Meet: 参加ボタンの検出
    const observer = new MutationObserver(() => {
      const inMeeting = this.detectGoogleMeet();
      if (inMeeting !== this.isInMeeting) {
        this.isInMeeting = inMeeting;
        this.notifyStateChange(inMeeting);
      }
    });

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

  private detectGoogleMeet(): boolean {
    // 会議中の特徴的な要素を検出
    return !!document.querySelector('[data-call-state="connected"]');
  }

  private notifyStateChange(inMeeting: boolean) {
    chrome.runtime.sendMessage({
      type: inMeeting ? 'MEETING_STARTED' : 'MEETING_ENDED',
      platform: 'google-meet',
      url: window.location.href,
    });
  }
}

new MeetingDetector();

Side Panel UI

// sidepanel/App.tsx
import { useState, useEffect } from 'react';

export function App() {
  const [isRecording, setIsRecording] = useState(false);
  const [transcript, setTranscript] = useState<string[]>([]);

  useEffect(() => {
    chrome.runtime.onMessage.addListener((message) => {
      if (message.type === 'TRANSCRIPTION_RESULT') {
        setTranscript(prev => [...prev, message.text]);
      }
    });
  }, []);

  const toggleRecording = async () => {
    if (isRecording) {
      chrome.runtime.sendMessage({ type: 'STOP_CAPTURE' });
    } else {
      chrome.runtime.sendMessage({ type: 'START_CAPTURE' });
    }
    setIsRecording(!isRecording);
  };

  return (
    <div className="p-4">
      <button
        onClick={toggleRecording}
        className={`px-4 py-2 rounded ${
          isRecording ? 'bg-red-500' : 'bg-blue-500'
        } text-white`}
      >
        {isRecording ? '録音停止' : '録音開始'}
      </button>

      <div className="mt-4 space-y-2">
        {transcript.map((text, i) => (
          <p key={i} className="text-sm">{text}</p>
        ))}
      </div>
    </div>
  );
}

ビルド設定

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { crx } from '@crxjs/vite-plugin';
import manifest from './src/manifest';

export default defineConfig({
  plugins: [react(), crx({ manifest })],
  build: {
    rollupOptions: {
      input: {
        offscreen: 'src/offscreen/index.html',
      },
    },
  },
});

デバッグ方法

  1. chrome://extensions を開く
  2. 「デベロッパーモード」を有効化
  3. 「パッケージ化されていない拡張機能を読み込む」
  4. dist フォルダを選択
  5. Service Worker: 「Service Worker」リンクをクリック
  6. Content Script: ページでDevTools > Console