メインコンテンツへスキップ
← 記事一覧に戻る
·開発·13 min read

Claude Code で YouTube Shorts Chrome 拡張を 1 セッションで作り切った — DOM 探索 10 回失敗と突破パターン

Claude CodeChrome拡張個人開発JavaScriptDOM
Claude Code で YouTube Shorts Chrome 拡張を 1 セッションで作り切った — DOM 探索 10 回失敗と突破パターン

YouTube Shorts を毎日見ていて、地味な不便が積もっていました。

説明文を確認しようとすると、説明アイコンをタップして、さらに「もっと見る」をタップする。たった 2 ステップです。でも 1 日 100 回やると、無意識のストレスとして蓄積されます。「これ、Chrome 拡張で自動化できるんじゃないか」と思い立ったのが出発点でした。

Claude Code を使えば小さなツールを素早く作れることは知っていました。ただし、今回は YouTube という巨大なフロントエンドが相手です。「DOM を調べてボタンを押すだけ」のつもりが、10 回以上のデバッグループに突入することになります。

今回の Chrome 拡張は 1 セッション(実作業時間 約 2.5 時間)で完成しました。DOM セレクタが一度も当たらず 10 回以上のデバッグループを経験しましたが、Claude Code に DevTools で確認した DOM 構造を貼り付けながら対話することで突破できました。Manifest V3・Shadow DOM・MutationObserver の組み合わせが最終的な解でした。

この記事でわかること:

  • Manifest V3 Chrome 拡張の最小構成(3 ファイルで完結)
  • YouTube Shorts の DOM が通常 YouTube と全く異なる理由と実態
  • 10 回以上失敗したセレクタ探索の 5 つの落とし穴
  • Claude Code との対話で詰まりを突破した具体的なやり取り

なぜ Shorts の DOM は難しいのか

通常の YouTube ページは ytp-button クラスや標準的な button 要素で構成されています。しかし Shorts は独自の Web Components 仕様で動いており、セレクタが根本的に違います。

最初に試みたアプローチはすべて null を返しました。

// ❌ 通常 YouTube ではよく見るクラス — Shorts には存在しない
document.querySelector('.ytp-button')
document.querySelector('[aria-label="詳細"]')
document.querySelector('.yt-icon-button')
document.querySelector('button.expand')

DevTools を開いてページを確認しても、見慣れた要素構造がありません。Shorts は YouTube の「次世代デザインシステム」(spec button と呼ばれる)を採用しており、コンポーネント名が全く異なります。

Shorts 固有の仕様: ytSpecButtonShapeNextHost

10 回以上のデバッグを経て発見したのが、ytSpecButtonShapeNextHost という要素です。

// ✅ Shorts で機能したセレクタの候補
document.querySelector('ytd-shorts #expand-button button')
document.querySelector('ytSpecButtonShapeNextHost')

これは YouTube のコンポーネントライブラリが Shorts 専用に実装したカスタム要素です。公式ドキュメントはなく、DevTools の Elements タブで地道に追跡する以外に方法はありません。「なぜこんな名前なのか」はわかりませんが、これが正解でした。


10 回の失敗から見つけた 5 つの落とし穴

落とし穴 1: Shadow DOM の壁

YouTube Shorts のボタン類は Shadow DOM の内部に配置されています。通常の querySelector は Shadow DOM を貫通できないため、表面からは見えない構造になっています。

// ❌ Shadow DOM 内には届かない
document.querySelector('ytd-shorts button')

// ✅ shadowRoot を経由してアクセスする
const host = document.querySelector('ytSpecButtonShapeNextHost')
if (host && host.shadowRoot) {
  const btn = host.shadowRoot.querySelector('button')
  btn?.click()
}

DevTools で Elements タブを見ると、#shadow-root (open) という表示がある場合がこれに該当します。open なら shadowRoot プロパティからアクセス可能です。

落とし穴 2: 非同期レンダリングのタイミング

Shorts は SPA(Single Page Application)です。URL が変わっても DOM が即座には更新されません。DOMContentLoaded を待っても、目的の要素はまだ存在していません。

// ❌ DOMContentLoaded だけでは間に合わない
document.addEventListener('DOMContentLoaded', () => {
  document.querySelector('ytd-shorts #expand-button')  // null
})

// ✅ MutationObserver で DOM 変化を監視して待ち構える
const observer = new MutationObserver(() => {
  const target = document.querySelector('ytd-shorts #expand-button button')
  if (target) {
    target.click()
    observer.disconnect()
  }
})
observer.observe(document.body, { childList: true, subtree: true })

// 5 秒後に監視を自動停止(無限ループ防止)
setTimeout(() => observer.disconnect(), 5000)

落とし穴 3: Manifest V3 の host_permissions 設定ミス

Manifest V3 では content_scriptsmatcheshost_permissions を両方設定する必要があります。content_scriptsmatches だけでは権限が足りず、拡張が動作しません。

{
  "manifest_version": 3,
  "name": "Shorts 説明を 1 クリックで開く",
  "version": "1.0",
  "content_scripts": [{
    "matches": ["*://www.youtube.com/shorts/*"],
    "js": ["content.js"],
    "css": ["content.css"]
  }],
  "host_permissions": [
    "*://www.youtube.com/*"
  ]
}

host_permissions を省略すると、拡張のインストール自体は成功しますが、content.js が注入されません。エラーメッセージも出ないため、コードの問題だと思い込んで無駄なデバッグに時間を溶かしました。

落とし穴 4: 拡張のキャッシュ問題

Chrome 拡張の content_scripts は一度ページにインジェクトされると、拡張コードを更新してもリロードされません。chrome://extensions/ から拡張を「再読み込み」してさらにページをリフレッシュしないと、古いコードが動き続けます。

「コードを直したはずなのに変わらない」を 3 回繰り返してから気づきました。開発中は毎回この手順が必要です。

落とし穴 5: スワイプ時の URL 変化を検知できない

ユーザーが Shorts をスワイプするたびに URL が /shorts/VIDEO_ID に変わります。最初に設定した MutationObserver は 1 本目の動画で disconnect() しているため、2 本目以降でボタンが機能しなくなります。

// URL 変化を監視して再初期化する仕組みが必要
let lastUrl = location.href

new MutationObserver(() => {
  if (lastUrl !== location.href) {
    lastUrl = location.href
    // 少し待ってから再初期化(DOM が落ち着くのを待つ)
    setTimeout(initExpandButton, 500)
  }
}).observe(document, { subtree: true, childList: true })

この「URL 変化を検知する MutationObserver」と「ボタンを探す MutationObserver」の 2 層構造が最終的なアーキテクチャになりました。


Claude Code との対話でどう突破したか

10 回の失敗は、すべて「試行 → 結果を Claude Code に伝える → 次の仮説を立てる」のサイクルで乗り越えました。

特に効果的だったのは、DevTools で見た実際の DOM 構造を丸ごとコピーして Claude Code に貼り付けることです。

[ユーザー]
DevTools でこの要素が見えています:
<ytd-shorts-desc-renderer ...>
  <yt-formatted-string class="description" ...>
  ...
  <tp-yt-paper-button id="expand-button">
なぜ querySelector('#expand-button') が null を返すのですか?

[Claude Code]
`#expand-button` が `ytd-shorts` の Shadow DOM 内部にある可能性があります。
通常の querySelector はドキュメントのルートから検索しますが、
Shadow DOM の内部には届きません。
`document.querySelector('ytd-shorts')?.shadowRoot?.querySelector('#expand-button')` を試してみてください。

このやり取りで Shadow DOM の存在に気づきました。ひとりでデバッグしていたら、さらに 1〜2 時間かかっていたと思います。

Claude Code が有効だったのは「知識の補完」だけではありません。「5 種類のセレクタ候補を一度に出してもらい、上から試していく」という探索速度の向上も大きかったです。通常の Google 検索では 1 つずつしか試せませんが、Claude Code は同時に複数のアプローチを提示してくれます。


完成した最小構成(3 ファイル)

manifest.json

{
  "manifest_version": 3,
  "name": "Shorts 説明を 1 クリックで開く",
  "version": "1.0",
  "description": "YouTube Shorts の説明文を自動展開します",
  "content_scripts": [{
    "matches": ["*://www.youtube.com/shorts/*"],
    "js": ["content.js"],
    "css": ["content.css"]
  }],
  "host_permissions": ["*://www.youtube.com/*"]
}

content.js

function tryExpand() {
  const expandBtn = document.querySelector('ytd-shorts #expand-button button')
  if (expandBtn) {
    expandBtn.click()
    return true
  }
  return false
}

function initExpandButton() {
  if (tryExpand()) return

  const observer = new MutationObserver(() => {
    if (tryExpand()) observer.disconnect()
  })
  observer.observe(document.body, { childList: true, subtree: true })
  setTimeout(() => observer.disconnect(), 5000)
}

// 初回実行
let lastUrl = location.href
initExpandButton()

// スワイプ時の URL 変化を監視
new MutationObserver(() => {
  if (lastUrl !== location.href) {
    lastUrl = location.href
    setTimeout(initExpandButton, 500)
  }
}).observe(document, { subtree: true, childList: true })

content.css

/* スタイル変更なし — YouTube のデフォルト UI をそのまま使用 */

で、どう稼ぐ?

「自分の不便を解決する小ツール」の直接的な収益化は難しいですが、間接効果が 3 つあります。

1. ポートフォリオとしての価値

Chrome Web Store に公開すると「問題発見 → 実装 → 公開」のサイクルを証明できます。インストール数が増えれば、フリーランス案件やクライアント獲得で「ユーザーのいるプロダクトを作れる人」として見られます。

2. Claude Code 活用力の証明

「DOM の未公開仕様を調べながら Claude Code と対話して 1 セッションで完成させた」という実績は、AI 開発ツールを生産的に使いこなせることの証拠になります。採用担当や発注者に見せるポートフォリオとして機能します。

3. 有料コンテンツの素材

今回の経験から「Manifest V3 移行ガイド」「Chrome 拡張 × Claude Code テンプレ」「YouTube DOM 仕様まとめ」などの有料記事が書けます。技術的な詰まりポイントは、同じ問題で困っている人が多いほど記事の需要が高くなります。

今回の拡張自体は無料公開します。ただしこのパターン(個人の不便 → Claude Code で 1 セッション実装 → 実録記事化)は繰り返せます。量が増えると「このブログを読めば Claude Code の実践例が見つかる」という信頼資産になっていきます。


今日やること

  1. 自分が毎日感じる小さな不便を 1 つ書き出す — ブラウザ操作・ターミナル・エディタのいずれかで 1 件
  2. DevTools でその要素の DOM を確認する — Shadow DOM の有無・クラス名・カスタム要素名を控える
  3. Claude Code に「Chrome 拡張の manifest.json と content.js の最小構成を作って」と頼む — 動く雛形が 30 秒で出てくる

最初の拡張は機能が 1 つで十分です。「動いた」という体験が、次のサイクルへの燃料になります。


Next Step

次に読むならこの導線です

すべての記事を見る
有料で次へ進む¥1,000

【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード

Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude 自動化」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。

次の実験記録も追う

Claude Code × 個人開発の実験ログ、失敗、判断変更をまとめて追いたい人向けに、月次でLab Freeを届けます。

masatoman のメルマガ — 毎週月曜の朝に 1 通

masatoman.net で今週公開した記事の中から 1 本を、読者目線で深掘りした手紙が届きます。「自分も同じことやってる」「ここで詰まってた」が見つかる予告編。

この記事が役に立ったらシェア