
既存サイトにReactアプリを統合しようとして、Content Security Policy(CSP)の競合問題にハマった経験を共有します。同じ悩みを持つ方の参考になれば幸いです。
やりたかったこと
既存のNext.jsサイト akashi-n.com に、別途開発したReactアプリ(MoneyFlip)を /moneyflip/ というサブディレクトリで公開したいと考えました。
example.com/ ← Next.jsメインサイト
example.com/moneyflip/ ← Reactアプリ(静的HTML)
構成としては以下のようなイメージです。
┌─────────────────────────────────────────┐
│ Netlify(ホスティング) │
├─────────────────────────────────────────┤
│ / │
│ ├── Next.jsアプリ(メインサイト) │
│ │ └── CSP: 親サイト用の設定 │
│ │ │
│ └── /moneyflip/ │
│ └── Reactアプリ(静的HTML) │
│ └── CSP: アプリ固有の設定が必要 │
└─────────────────────────────────────────┘
単体開発時のCSP設定
MoneyFlipは為替変換アプリで、外部APIや国旗画像などのリソースを使用しています。単体開発時は index.html の <meta> タグでCSPを設定していました。
<!-- index.html -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://pagead2.googlesyndication.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https://flagcdn.com https://www.google-analytics.com;
connect-src 'self' https://open.er-api.com https://www.google-analytics.com;
">
このアプリで必要だった外部リソース
ディレクティブ | リソース | 用途 |
|---|---|---|
|
| Google AdSense |
|
| Google Analytics |
|
| GA/AdSense用(理想はnonceだが...) |
|
| 国旗画像の取得 |
|
| GAのビーコン |
|
| 為替レートAPI |
|
| GA |
|
| Google Fonts(MUI) |
|
| Google Fonts |
単体では問題なく動作していました。
親サイト統合で発生した問題
いざ親サイトに統合してデプロイすると、コンソールが真っ赤になりました。
Refused to load the script 'https://pagead2.googlesyndication.com/...'
because it violates the following Content Security Policy directive:
"script-src 'self' 'unsafe-inline' https://www.googletagmanager.com"
Refused to connect to 'https://open.er-api.com/v6/latest/USD'
because it violates the following Content Security Policy directive:
"connect-src 'self' https://api.microcms.io..."
原因:metaタグとHTTPヘッダーの両方が適用される
調査の結果、問題の原因がわかりました。
- 親サイト(Next.js) が
netlify.tomlでサーバーレベルのCSPヘッダーを設定 - 子アプリ(React) が
index.htmlのmetaタグでCSPを設定 - 両方のポリシーが適用される
よくある誤解
「HTTPヘッダーがmetaタグより優先される」と思いがちですが、実際は両方のポリシーが適用されます。
つまり、両方で許可されているリソースしか読み込めません。片方だけで許可しても、もう片方で許可されていなければブロックされます。
親サイト(HTTPヘッダー): script-src 'self' https://www.googletagmanager.com
子アプリ(metaタグ): script-src 'self' https://pagead2.googlesyndication.com
→ 実際に許可されるのは 'self' のみ(両方で許可されているもの)
→ googletagmanager.com も pagead2.googlesyndication.com もブロックされる
親サイトのCSPに https://flagcdn.com や https://open.er-api.com が含まれておらず、子アプリのCSPにも親サイトが必要とするリソースが含まれていないため、お互いの機能がブロックされていたのです。
解決策:CSPを一箇所で管理する
CSPは一箇所で管理するのがベストプラクティスです。サブディレクトリにアプリを配置する場合は、子アプリのmetaタグを削除し、親サイトのサーバー設定でパス別にCSPを定義しましょう。
Netlifyの場合(netlify.toml)
# より具体的なパスを先に定義(重要!)
[[headers]]
for = "/moneyflip"
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://pagead2.googlesyndication.com https://*.google.com https://*.googleapis.com https://*.gstatic.com https://*.doubleclick.net https://*.adtrafficquality.google; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://flagcdn.com https://www.google-analytics.com https://*.google.com https://*.googleapis.com https://*.gstatic.com https://*.doubleclick.net https://*.adtrafficquality.google; connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://open.er-api.com https://*.google.com https://*.googleapis.com https://*.gstatic.com https://*.doubleclick.net https://*.adtrafficquality.google; frame-src 'self' https://*.doubleclick.net https://*.adtrafficquality.google https://*.google.com; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests"
[[headers]]
for = "/moneyflip/*"
[headers.values]
Content-Security-Policy = "..." # 同じ内容
# メインサイト用(最後に定義)
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "..." # メインサイト用の設定
ポイント: Netlifyでは上から順にマッチングされるため、具体的なパス(/moneyflip)を先に、汎用的なパス(/*)を後に定義します。
Nginxの場合
server {
# メインサイト用
location / {
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com ...";
}
# MoneyFlip用
location /moneyflip {
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://pagead2.googlesyndication.com https://www.googletagmanager.com ...";
}
}
Apache(.htaccess)の場合
# メインサイト用
<IfModule mod_headers.c>
Header set Content-Security-Policy "default-src 'self'; ..."
</IfModule>
# MoneyFlip用
<Directory "/var/www/html/moneyflip">
<IfModule mod_headers.c>
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://pagead2.googlesyndication.com ..."
</IfModule>
</Directory>
まとめ・チェックリスト
サブディレクトリにReactアプリを統合する際のチェックリストです。
統合前の確認
- [ ] 子アプリの
index.htmlからCSP metaタグを削除したか - [ ] 子アプリが必要とする外部リソースをすべてリストアップしたか
- [ ] サーバー設定でパス別CSPを定義したか
- [ ] パスの優先順位は正しいか(具体的なパスを先に)
外部サービス別の追加設定
Google Analytics(GA4)を使う場合
script-src:https://www.googletagmanager.com,https://www.google-analytics.comconnect-src:https://www.google-analytics.com,https://analytics.google.comimg-src:https://www.google-analytics.com
Google AdSenseを使う場合
script-src:https://pagead2.googlesyndication.com,https://*.doubleclick.net,https://*.adtrafficquality.googleimg-src:https://*.doubleclick.net,https://*.adtrafficquality.googleconnect-src:https://*.doubleclick.net,https://*.adtrafficquality.googleframe-src:https://*.doubleclick.net,https://*.adtrafficquality.google,https://*.google.com
Google Fontsを使う場合
style-src:https://fonts.googleapis.comfont-src:https://fonts.gstatic.com
おわりに
CSPの問題は、エラーメッセージを見れば原因は明確なのですが、「なぜ自分の設定が効かないのか」を理解するまでに時間がかかりました。
ポイントは以下の3つです。
- 両方のポリシーが適用される:HTTPヘッダーとmetaタグは競合ではなく、両方の AND(交差)が取られる
- 結果的に厳しくなる:両方で許可されているリソースしか読み込めない
- 一箇所で管理する:子アプリのmetaタグは削除し、サーバー設定でパス別にCSPを定義
同じ問題でハマっている方の参考になれば幸いです。
実際にこの方法で作ったのが MoneyFlip です。Claude Code で開発した通貨換算アプリで、PWA対応でホーム画面に追加して使えます。
この記事が役に立ったらフォローしてください

