WooCommerceチェックアウトでPayPalボタンが表示されない原因と修正手順
独自テーマを使った WooCommerce サイトのチェックアウトページで PayPal ボタンが表示されない場合、主な原因は DOM の準備完了前に async で読み込まれた JavaScript がコンテナ要素を取得できずに失敗することだ。また、PayPal JS SDK の読み込み完了後に buttons メソッドが存在しない問題は、コンストラクタの引数順序の誤りやスクリプトの初期化タイミングの競合で発生する。これらの問題を順に切り分けて修正すれば、数分でボタンが復活する。
PayPalボタンが表示されない原因を特定する

まずコンソールに出力されているエラーを把握する。大半のケースで、Error: Document is ready and element #paypal-button-container does not exist(日本語環境では同様のエラーメッセージが英語で表示される)という致命的なエラーが記録されている。これは PayPal JS SDK が #paypal-button-container という要素を DOM から見つけられず、ボタンの描画を中止したことを意味する。同時に、スクリプトがチェックアウトページではなく商品カテゴリページで動作してしまう現象や、CORS(クロスオリジンリクエスト)がブロックされたというエラーも散見される。
以下のフローで問題を切り分けると、原因に早くたどり着ける。
#paypal-button-container が実在するか確認する
console.log でオブジェクトを確認する
init() 後の this.paypal.buttons が undefined でないか検証する
この4つのチェックポイントをもとに、次のセクションで具体的な修正を加えていく。
async読み込みによるDOM参照の競合を解決する

WordPress 7.0 では、wp_enqueue_script に 'strategy' => 'async' を指定すると、スクリプトが非同期で読み込まれる。この設定自体は高速化に有効だが、DOM の構築が完了する前に document.querySelector('#paypal-button-container') が実行されると、要素がまだ存在しないために null が返ってしまう。
解決策はシンプルだ。PayPal ボタンの初期化処理全体を DOMContentLoaded イベントの中に包み、DOM の準備完了を待つ。具体的には index.js の setupPayment() を次のように修正する。
document.addEventListener('DOMContentLoaded', async function() {
const paypalManager = new PayPalManager(
clientIdHere,
'.checkoutForm',
'#paypal-button-container',
orderEndPoint,
emailEndPoint
);
await paypalManager.init();
await paypalManager.renderButtons(paypalManager.buttonContainer);
});
こうすれば、DOMContentLoaded が発火した時点でコンテナ要素が確実に存在するため、null エラーが解消する。なお、async ストラテジーはそのままでも問題ないが、より確実な制御を求めるなら defer に切り替えてもよい。ただし defer も DOM 構築の完了前に実行される可能性があるため、イベントリスナーを組み合わせるのが最も安全だ。
コンストラクタの引数順序と初期化タイミングを修正する

コンソールに出力されたオブジェクトを見ると、buttonContainer が null、orderEndPoint と emailEndPoint の値が意図したものと逆になっているケースが多い。これは PayPalManager のコンストラクタを呼び出す際の引数の順序がずれているか、引数が不足しているときに起こる。
典型的なミスは、最初の引数(clientId)を空にしてしまったり、セレクタ文字列を間違った順番で渡してしまうことだ。次のように constructor と new の呼び出し側を一致させる必要がある。
// paypal_manager.js の constructor 定義(本来の正しい順序の例)
constructor(clientId, formSelector, containerSelector, orderEndPoint, emailEndPoint) {
this.clientId = clientId;
this.formSelector = formSelector;
this.containerSelector = containerSelector;
this.buttonContainer = null;
this.orderEndPoint = orderEndPoint;
this.emailEndPoint = emailEndPoint;
this.paypal = null;
}
// index.js でのインスタンス化(すべての引数を順序通りに正しく与える)
const paypalManager = new PayPalManager(
'your-client-id', // clientId
'.checkoutForm', // formSelector
'#paypal-button-container',// containerSelector
'https://api-m.sandbox.paypal.com/v2/checkout/orders', // orderEndPoint
'/wp-json/auto-parts/v3/send_order_email' // emailEndPoint
);
このコードのように引数を明示的に記述すれば、プロパティの食い違いが一掃される。また、init() 内で this.paypal.buttons が undefined になる問題は、loadScript() の完了を await で待つ処理が正しく記述されていれば解決する。paypal-js パッケージの loadScript は Promise を返すので、必ず await loadScript(...) の形で使う。
チェックアウトページにだけスクリプトを読み込む方法

症状のひとつに「スクリプトが商品カテゴリページでは動くのにチェックアウトページで動かない」という現象があった。これは wp_enqueue_scripts フックがすべてのページで実行されるために、意図しないページで PayPal のコードが動き出し、逆にチェックアウトページでは何らかの理由でコンテナが存在したにもかかわらず失敗していることを示す。
第一に、スクリプトの読み込みをチェックアウトページに限定する。WordPress の条件分岐タグ is_checkout() を使い、functions.php を次のように変更する。
function mainModules() {
if ( is_checkout() && ! is_order_received_page() ) {
wp_enqueue_script(
'paypal-checkout',
get_theme_file_uri('/build/index.js'),
array(),
'1.0.0',
array( 'strategy' => 'async' )
);
}
}
add_action('wp_enqueue_scripts', 'mainModules');
この条件を追加すれば、商品一覧やカテゴリページで PayPal 関連の JavaScript エラーがコンソールに表示されなくなり、意図したページだけが初期化を行う。また、キャッシュプラグインが原因で古いスクリプトが配信されている可能性があるため、修正後にキャッシュをクリアすることも忘れずに行う。
CORSエラーの対処と無視できる場合

コンソールに記録される Cross-Origin Request Blocked エラーは、PayPal のロガー API(https://www.sandbox.paypal.com/xoplatform/logger/api/logger)へ送信されるリクエストがオリジン間制限に引っかかったものだ。このエラーは、PayPal ボタンの描画が失敗した後に二次的に発生するケースがほとんどで、サイトの主要機能には影響しない。
ボタンが正しく表示されるようになれば、多くの場合この CORS エラーは自然に消える。もし修正後も CORS エラーが出続けるなら、PayPal の Sandbox アプリケーション設定で許可されたオリジンに本番ドメインが登録されているか確認する。通常は無視して問題ないが、セキュリティ上の懸念がある場合は、PayPal のビジネスサポートにオリジン制限の解除を依頼できる。
よくある質問
asyncとdeferのどちらを選ぶべきか
両者とも非同期読み込みだが、defer は HTML のパース完了後に実行順序を保ちながら実行される。PayPal ボタンのような DOM 操作が絡むスクリプトは、defer と DOMContentLoaded の組み合わせが扱いやすい。ただし、パフォーマンスを優先するなら async のままイベントリスナーで制御する形で問題ない。
PayPal SDK の読み込みに時間がかかる場合の改善策
@paypal/paypal-js の loadScript は PayPal の CDN からスクリプトを取得する。自前でキャッシュすることは難しいが、async 読み込みによってページ全体のレンダリングをブロックしないようにできる。どうしても速度が遅い場合は、チェックアウトページ遷移時にローディングスピナーを表示し、init() が完了するまでユーザーに待機を伝える UI を実装するとよい。
コンソールのCORSエラーを完全に消す方法はあるか
PayPal 側のロガーAPIのオリジンを制御することはできないため、完全に消すことは難しい。実害がないため無視するのが一般的だ。もしどうしても気になる場合は、エラーハンドリングで該当のリクエストをキャッチして無視するか、PayPal のサポートに問い合わせてロガー機能を無効化できないか相談する。
WordPressのバージョンが7.0でなくてもこの問題は起きるか
async ストラテジーは WordPress 6.6 以降で導入されたため、それ以前のバージョンでは異なる方法で非同期読み込みを行っている可能性がある。しかし、DOM の準備完了前に要素を取得できない問題は、wp_enqueue_script の設定にかかわらず生じるため、同じ修正が有効だ。
子テーマで上書きする場合の注意点
親テーマの functions.php で定義された wp_enqueue_scripts フックを子テーマで解除するには、remove_action を使うか、子テーマ側のフックで wp_dequeue_script を使って親のスクリプトを外した後、新たに読み込む。ペイパルマネージャーの JavaScript ファイルは通常、テーマのビルドフォルダにあるため、ファイル自体を子テーマにコピーして上書きする方法が確実だ。
この記事のポイント
- PayPalボタンが表示されない根本原因は、DOMの準備完了前にスクリプトがコンテナ要素を取得できないこと
- async読み込みの対策として、DOMContentLoadedイベントの内側で初期化を行う
- コンストラクタ引数の順序ずれで、buttonContainerがnullになったりエンドポイントが入れ替わる
- is_checkout() 条件を使ってチェックアウトページにだけスクリプトを読み込む
- 二次的に発生するCORSエラーはボタン描画が成功すれば消えることが多く、実害がなければ無視できる

・ Reddit、Stack Overflow、WordPress.org フォーラムを日々巡回し、現場の悩みを拾い上げて記事化
・ WordPress、WooCommerce、Next.js などモダンWeb制作領域のトラブルシューティングが専門
・ 「検索しても答えが見つからなかった」を一つでも減らすことが目標
・ エラーメッセージから根本原因にたどり着く粘り強い調査が得意
・ 初心者がつまずきやすい箇所を先回りで解決する記事作りを心がけている