結論
YouTube Shorts の説明文を 2 タップから 1 クリックに変える Chrome 拡張を、Claude Code との対話 1 セッション(約 2.5 時間)で完成させた。
DOM セレクタが 10 回以上 null を返し続けたが、5 つの落とし穴を特定して突破した。Manifest V3 + Shadow DOM + MutationObserver の組み合わせが解答だった。
なぜ Shorts の DOM は難しいのか
通常の YouTube は ytp-button クラスで操作できるが、Shorts は独自の "spec button" 仕様(ytSpecButtonShapeNextHost)で構築されており、セレクタが全く異なる。
// ❌ 通常 YouTube では動く — Shorts では null
document.querySelector('.ytp-button')
document.querySelector('[aria-label="詳細"]')
10 回失敗した 5 つの落とし穴
1. Shadow DOM の壁
Shorts のボタンは Shadow DOM 内に配置。通常の querySelector は貫通できない。
// ❌ Shadow DOM には届かない
document.querySelector('ytd-shorts button')
// ✅ shadowRoot 経由でアクセス
const host = document.querySelector('ytSpecButtonShapeNextHost')
if (host?.shadowRoot) {
host.shadowRoot.querySelector('button')?.click()
}
DevTools の Elements タブで #shadow-root (open) が表示されていたらこれが原因。
2. 非同期レンダリング
Shorts は SPA。DOMContentLoaded を待っても目的の要素は存在しない。MutationObserver で待ち構える必要がある。
// ❌ 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 })
setTimeout(() => observer.disconnect(), 5000)
3. Manifest V3 の host_permissions 設定ミス
content_scripts の matches だけでは不足。host_permissions も必要。
{
"manifest_version": 3,
"content_scripts": [{
"matches": ["*://www.youtube.com/shorts/*"],
"js": ["content.js"]
}],
"host_permissions": ["*://www.youtube.com/*"]
}
4. 拡張のキャッシュ
コードを変更しても chrome://extensions/ から「再読み込み」しないと古いコードが動き続ける。「直したはずなのに変わらない」を 3 回やってから気づいた。
5. スワイプ時の URL 変化
2 本目の動画からボタンが機能しなくなる。Shorts をスワイプするたびに URL が変わるため、URL 変化を監視して再初期化が必要。
let lastUrl = location.href
new MutationObserver(() => {
if (lastUrl !== location.href) {
lastUrl = location.href
setTimeout(initExpandButton, 500)
}
}).observe(document, { subtree: true, childList: true })
完成した最小構成
manifest.json:
{
"manifest_version": 3,
"name": "Shorts 説明を 1 クリックで開く",
"version": "1.0",
"content_scripts": [{
"matches": ["*://www.youtube.com/shorts/*"],
"js": ["content.js"]
}],
"host_permissions": ["*://www.youtube.com/*"]
}
content.js:
function tryExpand() {
const btn = document.querySelector('ytd-shorts #expand-button button')
if (btn) { btn.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()
new MutationObserver(() => {
if (lastUrl !== location.href) {
lastUrl = location.href
setTimeout(initExpandButton, 500)
}
}).observe(document, { subtree: true, childList: true })
Claude Code との対話で突破した方法
詰まりを突破したのは、DevTools で確認した DOM 構造を Claude Code に貼り付けて「なぜ null になるか」を聞いたことだった。
「#shadow-root (open) という表示があります」と伝えると、Shadow DOM の存在とアクセス方法を即座に返してくれた。一人でデバッグしていたらさらに 1〜2 時間かかっていたと思う。
効果的だった使い方:
- DevTools で実際の DOM をコピーして貼り付ける
- 「このセレクタが null を返す原因を推測してください」と聞く
- 返ってきた 3〜5 候補を上から試す
まとめ
Shorts の Chrome 拡張で詰まるポイントは決まっている:
| 問題 | 原因 | 解決策 |
|---|---|---|
| querySelector が null | Shadow DOM |
shadowRoot 経由でアクセス |
| DOMContentLoaded で null | 非同期レンダリング | MutationObserver を使う |
| 拡張が動かない | host_permissions 欠落 | manifest.json に追記 |
| コード変更が反映しない | キャッシュ | chrome://extensions/ で再読み込み |
| 2 本目から動かない | URL 変化未検知 | URL 変化監視の MutationObserver を追加 |
詳細な実録・「で、どう稼ぐ?」の観点はこちら: masatoman.net 元記事