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_scripts の matches と host_permissions を両方設定する必要があります。content_scripts の matches だけでは権限が足りず、拡張が動作しません。
{
"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 件
- DevTools でその要素の DOM を確認する — Shadow DOM の有無・クラス名・カスタム要素名を控える
- Claude Code に「Chrome 拡張の manifest.json と content.js の最小構成を作って」と頼む — 動く雛形が 30 秒で出てくる
最初の拡張は機能が 1 つで十分です。「動いた」という体験が、次のサイクルへの燃料になります。
Next Step
次に読むならこの導線です
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude 自動化」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude 自動化」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
【第1回】Claude Code Skills 入門 — 自作スキルで開発効率を2倍にする実装ガイド
Claude Code の Skills 機能を自作する手順を、masatoman.net 周辺の自動化を Claude Code で回している立場で実コード付きで解説。1 スキル 15 分の投資で月 10 時間の作業を削減する実装ガイドです。
【第2回】Claude Code MCP サーバー構築ガイド — ゼロから作る実用MCPの全コード
Claude Code に外部システムを接続する MCP サーバーをゼロから自作する手順を、実コード付きで解説。月500万トークン運用の筆者が「実用ライン」の MCP 設計を全公開します。
次の実験記録も追う
Claude Code × 個人開発の実験ログ、失敗、判断変更をまとめて追いたい人向けに、月次でLab Freeを届けます。
masatoman のメルマガ — 毎週月曜の朝に 1 通
masatoman.net で今週公開した記事の中から 1 本を、読者目線で深掘りした手紙が届きます。「自分も同じことやってる」「ここで詰まってた」が見つかる予告編。
この記事が役に立ったらシェア