WordPressでお問い合わせフォームを自作して埋め込む|プラグインなし|reCAPTCHAも導入

WordPressでお問い合わせフォームを「Contact Form 7」などのプラグインを使わずに実装してみます。

シンプルに考えた場合、問い合わせたい人が入力すべきものは「名前」と「メールアドレス」と「内容」だけでいいはず。

たったこれだけを取得するだけですから、プラグインを使わずにjavascriptとPHPで実装してみました。

これで、全てのページに「Contact Form 7」のcssとjsファイルが読み込まれてしまうことを回避できます。また、「reCAPTCHA」の評価対象もお問い合わせフォームを実装したページのみになります。

結構簡単でしたので、備忘録として残します。

なお、当方はその道のプロではありませんのでご注意ください。

目次
    スポンサーリンク

    SMTPの設定

    まずは、SMTPの設定を行います。

    昨今、「なりすましメール」や「フィッシングメール」が問題化してきているため、メールを取り巻く環境が一段と厳しくなっています。特にGmailは厳しい・・・

    問い合わせ者が送信ボタンを押すと、問い合わせ者と運営サイドにメールを自動送信しますので最低でもSMTPの設定はしておきます。

    回答は別のメールソフトを使って送信する場合でも、そもそも運営サイドにお問い合わせがあったことを知らせるメールが来ないと意味ないですから。

    SMTPは、メールをより確実に、より安全に届けてくれます。

    さらに、SPF、DKIM、DMARCも設定しておくと完璧です。

    詳しくは、別記事で。

    お問い合わせフォームの実装

    次のようにシンプルに実装します。

    1. 必須入力欄は「お名前」「メールアドレス」「お問い合わせ内容」のみ。
    2. お問い合わせ内容の確認ページは実装しない。
      ➡画面遷移させない
    3. 送信ボタンを押すと1秒間ローディング画面を表示させる。
      ➡これがないと何度も送信ボタンを押されてしまう
    4. 完了ページも使わない。
      ➡画面遷移させずに送信に成功・エラーでメッセージをモーダル表示
    5. 自動返信メールを運営サイドと問い合わせ者に送る。
      ➡WordPress の wp_mail() を使ってメール送信
    6. セキュリティ対策
      ➡Googleの「reCAPTCHA v3」を導入してスパムボットの自動送信を防ぐ
      ➡入力値のサニタイズ処理(WordPress関数)でHTMLタグなどの不正挿入を防ぐ
      ➡メールヘッダインジェクション対策でメールの乗っ取りや不正送信を防ぐ
      ➡送信回数制限をかけ、30秒以内の再送信をブロックする
    7. 必須項目に未入力がないかチェックをする。
      ➡未入力箇所があれば今のブラウザではこの時点で送信できないが、JSで補完的に入れておく
      ➡未入力箇所があればalert表示で警告
    8. 問い合わせ者のIPアドレスを取得する。
      ➡直接的には個人を特定できないが、個人情報に準ずる扱いとしプライバシーポリシーに取得と目的を表示させること
      ➡運営サイドへのメールにこの情報を入れる

    完成形は次のとおり。(送信ボタンを押しても何もなりません)

    お問い合わせフォーム
    全角で入力してください
    例:㈱YAMADA
    例:example@email.com
    数字入力
    送信中
    スポンサーリンク

    reCAPTCHA v3の導入

    reCAPTCHAを使用するために、「サイトキー」と「シークレットキー」を用意。
    さらにGoogleCloudにまだ移行していなかったので、重い腰を上げて移行しました。
    無料のまま使いたいので「課金システム」は無効状態です。

    なので、無料分を超過するとその月はサービスが使えなくなりますが、私の小規模サイトであればまず超えることはないでしょう。

    reCAPTCHAは10,000件の評価までは無料で使えますが、それを超えて使いたい場合は有料になります。
    それを越えても無料のものを使いたい場合は他のCaptchaサービスを利用したほうがいいので、これ以降の内容を読んでも意味がありません。最初からreCAPTHAを導入しない方がいいです。
    hCaptcha」「Friendly Captcha」「Cloudflare Turnstile」などを探して導入してください。

    まずはGoogleのreCAPTCHAのスクリプトタグを読み込む必要があるので、次のコードをカスタムHTMLブロックで入れます。

    ただし、カスタムHTMLブロックで入れるのはWordPress非推奨ですので、マネしない方がいいです。推奨されているのは、wp_enqueue_scriptsフックを使用してfunctions.phpに書き込んで読み込ませる方法です。

    <script src="https://www.google.com/recaptcha/api.js?render=あなたのサイトキー"></script>

    JavaScriptのメール送信コードでも「サイトキー」が必要になります。
    なお、「シークレットキー」はPHPで使います。

    吹き出し枠(ツールチップ)を表示してメッセージを伝える

    入力欄にfocusしたときに吹き出し枠(ツールチップ)を表示させて、入力するうえで必要なメッセージをユーザーに伝えます。

    placeholderにもメッセージを表示できますが、それ以外に伝えたい事柄があれば表示することができます。

    吹き出し枠

    実装するには、HTMLに一行を加えて、cssでスタイル設定します。

    <!-- HTML -->
    <div class="form-group">
      <label for="name" class="required">お名前</label>
      <input type="text" id="name" class="namewaku" name="name" required placeholder="例:山田 太郎">
      <div class="tooltip">全角で入力してください</div> <!-- ←この一行を加えます -->
    </div>
    /* css */
    .tooltip {
      position: absolute;
      top: 100%;
      left: 0;
      background-color: #2c3e50;
      color: white;
      padding: 8px 12px;
      border-radius: 6px;
      font-size: 14px;
      white-space: nowrap;
      z-index: 1000;
      opacity: 0;
      visibility: hidden;
      transform: translateY(-10px);
      transition: all 0.3s ease;
      margin-top: 5px;
    }
    .tooltip::before {
      content: '';
      position: absolute;
      top: -8px;
      left: 20px;
      border-left: 8px solid transparent;
      border-right: 8px solid transparent;
      border-bottom: 8px solid #2c3e50;
    }
    input:focus + .tooltip,
    input.keypad-active + .tooltip {
      opacity: 1;
      visibility: visible;
      transform: translateY(0);
    }

    数字の入力はキーパッド方式を用いる

    電話番号の入力はそれほど手間ではありませんが、生年月日や日付などの入力はそれなりに面倒だと思われるユーザーが多い(私もそのひとり)ので、次のようにキーパッドをモーダル表示させ、数字をタップして入力する方式を採用します。

    タッチパッド方式

    電話番号だとあまり効果はないかもしれませんが、他の数字入力でも代用できるので実装しておきます。

    HTML+css+JavaScriptで実装します。

    <!-- HTML -->
    <div class="keypad-overlay" id="keypadOverlay">
      <div class="keypad-container">
        <div class="keypad-header">
          <div id="keypadTitle">数字入力</div>
        </div>
        <div class="keypad-display" id="keypadDisplay"></div>
        <div class="keypad-grid">
          <button type="button" class="keypad-btn" data-value="1">1</button>
          <button type="button" class="keypad-btn" data-value="2">2</button>
          <button type="button" class="keypad-btn" data-value="3">3</button>
          <button type="button" class="keypad-btn" data-value="4">4</button>
          <button type="button" class="keypad-btn" data-value="5">5</button>
          <button type="button" class="keypad-btn" data-value="6">6</button>
          <button type="button" class="keypad-btn" data-value="7">7</button>
          <button type="button" class="keypad-btn" data-value="8">8</button>
          <button type="button" class="keypad-btn" data-value="9">9</button>
          <button type="button" class="keypad-btn special" data-value="/">/</button>
          <button type="button" class="keypad-btn" data-value="0">0</button>
          <button type="button" class="keypad-btn special" data-value="-">-</button>
        </div>
        <div class="keypad-controls">
          <button type="button" class="clear-btn" id="keypadClear">全削除</button>
          <button type="button" class="back-btn" id="keypadBackspace">一つ戻る</button>
          <button type="button" class="cancel-btn" id="keypadCancel">閉じる</button>
          <button type="button" class="complete-btn" id="keypadComplete">完了</button>
        </div>
      </div>
    </div>
    <!-- ここまでHTML -->
    
    /* css */
    .keypad-trigger {
      cursor: pointer;
      user-select: none;
    }
    .keypad-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.7);
      z-index: 1000;
      display: none;
      backdrop-filter: blur(5px);
    }
    .keypad-container {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: white;
      border-radius: 20px;
      padding: 20px;
      box-shadow: 0 20px 60px rgba(0,0,0,0.3);
      width: 95%;
      max-width: 350px;
    }
    .keypad-header {
      text-align: center;
      margin-bottom: 10px;
      font-size: 17px;
    }
    .keypad-display {
      background: #f8f9fa;
      border: 2px solid #e1e8ed;
      border-radius: 12px;
      padding: 10px;
      font-size: 15px;
      text-align: center;
      margin-bottom: 20px;
      min-height: 30px;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .keypad-grid {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 8px;
      margin-bottom: 10px;
    }
    .keypad-btn {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      border-radius: 12px;
      padding: 16px;
      font-size: 18px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.3s ease;
      box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
    }
    .keypad-btn:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
    }
    .keypad-btn:active {
      transform: translateY(0);
    }
    .keypad-btn.special {
      background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
      color: #333;
      box-shadow: 0 4px 15px rgba(252, 182, 159, 0.4);
    }
    .keypad-controls {
      display: flex;
      gap: 4px;
    }
    .keypad-controls button {
      flex: 1;
      padding: 15px 5px;
      border: none;
      border-radius: 12px;
      font-size: 15px;
      font-weight: 500;
      cursor: pointer;
      transition: all 0.3s ease;
    }
    .clear-btn {
      background: #e74c3c;
      color: white;
    }
    .back-btn {
      background: #e74c3c;
      color: white;
    }
    .cancel-btn {
      background: #3498db;
      color: white;
    }
    .complete-btn {
      background: #27ae60;
      color: white;
    }
    .complete-btn:hover,
    .cancel-btn:hover,
    .back-btn:hover,
    .clear-btn:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 20px rgba(0,0,0,0.2);
    }
    /* ここまでcss */
    
    // JavaScript
    let currentInput = null;
    let keypadDisplay = '';
    
    document.querySelectorAll('.keypad-trigger').forEach(input => {
      input.addEventListener('focus', function() {
        closeKeypad();
        currentInput = this;
        keypadDisplay = this.value || '';
        showKeypad();
        updateKeypadTitle();
      });
    });
    
    function showKeypad() {
      document.getElementById('keypadOverlay').style.display = 'block';
      document.getElementById('keypadDisplay').textContent = keypadDisplay || '数字をタップしてください';
      document.body.style.overflow = 'hidden';
    }
    function closeKeypad() {
      document.getElementById('keypadOverlay').style.display = 'none';
      document.body.style.overflow = 'auto';
      currentInput = null;
      keypadDisplay = '';
    }
    function updateKeypadTitle() {
      const titles = {
        'phone': '電話番号入力'
      };
      const title = titles[currentInput.id] || '数字入力';
      document.getElementById('keypadTitle').textContent = title;
    }
    
    document.querySelectorAll('.keypad-btn[data-value]').forEach(btn => {
      btn.addEventListener('click', function() {
        const value = this.dataset.value;
        keypadDisplay += value;
        document.getElementById('keypadDisplay').textContent = keypadDisplay;
      });
    });
    document.getElementById('keypadBackspace').addEventListener('click', function() {
      keypadDisplay = keypadDisplay.slice(0, -1);
      document.getElementById('keypadDisplay').textContent = keypadDisplay || '数字をタップしてください';
    });
    document.getElementById('keypadClear').addEventListener('click', function() {
      keypadDisplay = '';
      document.getElementById('keypadDisplay').textContent = keypadDisplay || '数字をタップしてください';
    });
    document.getElementById('keypadComplete').addEventListener('click', function() {
      if (currentInput) {
        currentInput.value = keypadDisplay;
      }
      closeKeypad();
    });
    document.getElementById('keypadCancel').addEventListener('click', function() {
      closeKeypad();
    });
    document.getElementById('keypadOverlay').addEventListener('click', function(e) {
      if (e.target === this) {
        closeKeypad();
      }
    });

    ローディング画面の設定

    送信ボタンを押した後に一秒間表示させるローディング画面を設定します。
    なお、ローディングは色々紹介されていますので、自分好みのものを探してみてください。

    HTML+css+JavaScriptで実装しますが、JavaScriptのコードは後述の「送信ボタンを押した後の挙動」で。

    <!-- HTML -->
    <div id="loading-wrapper">
      <div id="loading-text">送信中</div>
      <div id="loading-content"></div>
    </div>
    
    /* css */
    #loading-wrapper {
      background: rgb(0 0 0 / .6);
      position: fixed;
      width: 100%;
      height: 100%;
      left: 0;
      top: 0;
      z-index: 999;
      display: none;
    }
    #loading-text {
      display: block;
      position: absolute;
      top: 50%;
      left: 50%;
      color: white;
      width: 100px;
      height: 30px;
      margin: -7px 0 0 -45px;
      text-align: center;
      font-family: 'PT Sans Narrow', sans-serif;
      font-size: 16px;
    }
    #loading-content {
      display: block;
      position: relative;
      left: 50%;
      top: 50%;
      width: 130px;
      height: 130px;
      margin: -65px 0 0 -65px;
      border: 3px solid #F00;
    }
    #loading-content:after {
      content: "";
      position: absolute;
      border: 3px solid #0F0;
      left: 15px;
      right: 15px;
      top: 15px;
      bottom: 15px;
    }
    #loading-content:before {
      content: "";
      position: absolute;
      border: 3px solid #00F;
      left: 5px;
      right: 5px;
      top: 5px;
      bottom: 5px;
    }
    #loading-content {
      border: 3px solid transparent;
      border-top-color: #4D658D;
      border-bottom-color: #4D658D;
      border-radius: 50%;
      -webkit-animation: loader 2s linear infinite;
      -moz-animation: loader 2s linear infinite;
      -o-animation: loader 2s linear infinite;
      animation: loader 2s linear infinite;
    }
    #loading-content:before {
      border: 3px solid transparent;
      border-top-color: #D4CC6A;
      border-bottom-color: #D4CC6A;
      border-radius: 50%;
      -webkit-animation: loader 3s linear infinite;
        -moz-animation: loader 2s linear infinite;
      -o-animation: loader 2s linear infinite;
      animation: loader 3s linear infinite;
    }
    #loading-content:after {
      border: 3px solid transparent;
      border-top-color: #84417C;
      border-bottom-color: #84417C;
      border-radius: 50%;
      -webkit-animation: loader 1.5s linear infinite;
      animation: loader 1.5s linear infinite;
        -moz-animation: loader 2s linear infinite;
      -o-animation: loader 2s linear infinite;
    }
    @-webkit-keyframes loaders {
      0% {
        -webkit-transform: rotate(0deg);
        -ms-transform: rotate(0deg);
        transform: rotate(0deg);
      }
      100% {
        -webkit-transform: rotate(360deg);
        -ms-transform: rotate(360deg);
        transform: rotate(360deg);
      }
    }
    @keyframes loader {
      0% {
        -webkit-transform: rotate(0deg);
        -ms-transform: rotate(0deg);
        transform: rotate(0deg);
      }
      100% {
        -webkit-transform: rotate(360deg);
        -ms-transform: rotate(360deg);
        transform: rotate(360deg);
      }
    }

    モーダルウィンドウの設定

    送信ボタンを押した後に一秒間ローディング画面を表示させて、送信に成功・エラーで表示させるモーダルウィンドウを設定します。

    HTML+css+JavaScriptで実装しますが、JavaScriptのコードはこちらも後述の「送信ボタンを押した後の挙動」で。

    <!-- HTML -->
    <div class="popup-success">
      <div class="popup-inner"> 
        <span class="close">&times;</span>
        <div class="comment1">受け付けました!</div>
        <div class="comment2">お返事までしばらくお待ちください。</div>
      </div>
    </div>
    <div class="popup-error">
      <div class="popup-inner"> 
        <span class="close">&times;</span>
        <div class="comment1">送信に失敗しました!</div>
        <div class="comment2">時間をおいて再度お試しください。</div>
      </div>
    </div>
    
    /* css */
    .popup-success, .popup-error {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgb(0 0 0 / .5);
      justify-content: center;
      align-items: center;
      z-index: 99;
      overflow: hidden;
    }
    .popup-inner {
      position: relative;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      text-align: center;
      width: 90%;
      max-width: 400px;
      height: 50%;
      background: white;
      padding: 10px 0;
      border: 3px solid #997526;
      border-radius: 20px;
    }
    .popup-inner .close {
      position: absolute;
      display: flex;
      align-items: center;
      justify-content: center;
      top: 5px;
      right: 5px;
      height: 30px;
      width: 30px;
      font-size: 18px;
      font-weight: 600;
      background: #ffec47;
      border: 1px solid #006e54;
      border-radius: 50%;
      color: #00a381;
      cursor: pointer;
      z-index: 999;
    }
    .comment1 {
      font-size: 23px;
      font-weight: 600;
      color: #264B99;
      font-family: "Noto Sans JP", sans-serif;
      margin: 10px 0;
    }
    .comment2 {
      font-size: 16px;
      font-weight: 400;
      color: #160c28;
      font-family: "Noto Sans JP", sans-serif;
      margin-top: 10px;
    }

    入力フォームの設定

    その道のプロではないので、適当に作っています。

    HTML

    表示させたい場所に、カスタムHTMLブロックで入れます。前述の吹き出し枠も含めたコードになります。

    実際には、前述で紹介したHTMLコードもくっつけています。

    <div class="form-container">
      <div class="title">お問い合わせフォーム</div>
      <form id="userForm" class="mailform">
        <div class="form-group">
          <label for="name" class="required">お名前</label>
          <input type="text" id="name" class="namewaku" name="name" required placeholder="例:山田 太郎">
          <div class="tooltip">全角で入力してください</div>
        </div>
        
        <div class="form-group">
          <label for="company">会社名</label>
          <input type="text" id="company" class="companywaku" name="company" placeholder="法人の場合はご記入ください">
          <div class="tooltip">例:㈱YAMADA</div>
        </div>    
        
        <div class="form-group">
          <label for="email" class="required">メールアドレス</label>
          <input type="email" id="email" class="emailwaku" name="email" required placeholder="返信先のメールアドレスをご記入ください">
          <div class="tooltip">例:example@email.com</div>
        </div>
        
        <div class="form-group">
          <label for="phone">電話番号</label>
          <input type="text" id="phone" name="phone" class="keypad-trigger" readonly placeholder="タップして入力してください">
        </div>
            
        <div class="form-group">
          <label for="message" class="required">お問い合わせ内容</label>
          <textarea id="message" name="message" required placeholder="お問い合わせ内容をご記入ください"></textarea>
        </div>
        
        <div class="form-group">
         <label for="check" class="check">
            <input id="check" type="checkbox" name="check" required>入力内容を確認しました
        </label>
        </div>
        
        <button type="submit" id="submit" class="submit-btn">送信</button>
      </form>
    </div>

    css

    これまた、その道のプロではないので適当に作っています。

    該当ページだけなので、<style>タグで囲んでカスタムHTMLブロックで入れました。

    本当は、子テーマのstyle.cssに追加するとか、wp_enqueue_scriptsフックを使用して処理するのがベストなんでしょうが、なぜか面倒くさいと思ってしまいます。

    ここら辺が、その道のプロではないです。

    吹き出し枠のcssは含んでいませんが、実際には前述のcssも含んで入れています。

    .form-container {
      width: 98%;
      max-width: 600px;
      margin: 0 auto;
      background: white;
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.3);
      font-family: 'Noto Sans JP', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    }
    .title {
      font-size: 22px;
      font-weight: 500;
      text-align: center;
      color: #160c28;
      margin-bottom: 30px;
    }
    .form-group {
      position: relative;
      margin-bottom: 25px;
    }
    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 600;
      color: #160c28;
    }
    .required {
      color: #160c28;
    }
    .required::after {
      content: "必須";
      color: white;
      font-size: 11px;
      background: #e74c3c;
      padding: 2px 3px;
      margin-left: 5px;
      border-radius: 3px;
    }
    input[type="text"],
    input[type="email"],
    input[type="tel"],
    textarea {
      width: 100%;
      max-width: 100%;
      height: 50px;
      padding: 12px 15px;
      border: 2px solid #ddd;
      border-radius: 6px;
      font-size: 16px;
      transition: border-color 0.3s ease;
      box-sizing: border-box;
      background: #f7fcfe;
    }
    textarea {
      resize: vertical;
      min-height: 200px;
    }
    input[type="text"]:focus,
    input[type="email"]:focus,
    input[type="tel"]:focus,
    textarea:focus {
      outline: none;
      border-color: #3498db;
      box-shadow: 0 0 10px 4px rgb(52 152 219 / .5)
    }

    送信ボタンを押した後の挙動(JavaScript)

    以下のコードで実装しました。recaptchaのサイトキー、PHPファイルのアクセスURLは隠しています。

    なお、サイトキーは公開されても問題ないので、フロントエンド側(HTML・javascript)に置いています。

    1秒間のローディング画面を表示させて、成功したらモーダルウィンドウ(.popup-success)を表示。スパム判定されたらアラート表示。送信に失敗したら失敗のモーダルウィンドウ(.popup-error)を表示。

    該当ページだけなので、これもまた面倒くさくて<script>タグで囲んでカスタムHTMLブロックでページの最後に入れました(WordPress非推奨)。【前述のキーパッドコードも含んで】

    document.querySelector(".mailform").addEventListener("submit", function(event) {
      event.preventDefault();
    
      const loadingWrapper = document.getElementById("loading-wrapper");
      const popupSuccess = document.querySelector(".popup-success");
      const popupError = document.querySelector(".popup-error");
    
      loadingWrapper.style.display = "flex";
    
      setTimeout(() => {
        const formData = new FormData(event.target);
    
        // 必須チェック
        const name = formData.get("name");
        const email = formData.get("email");
        const message = formData.get("message");
    
        if (!name || !email || !message) {
          loadingWrapper.style.display = "none";
          alert("全ての項目を入力してください。");
          return;
        }
    
        grecaptcha.ready(function() {
          grecaptcha.execute('recaptchaのサイトキー', { action: 'submit' }).then(function(token) {
            formData.append("recaptcha", token);
    
            fetch("PHPファイルのアクセスURL", {
              method: "POST",
              body: formData
            })
            .then(response => response.json())
            .then(data => {
              loadingWrapper.style.display = "none";
              if (data.status === "success") {
                popupSuccess.style.display = "flex";
              } else if (data.message === "スパム判定されました。") {
                alert(data.message);
              } else {
                popupError.style.display = "flex";
              }
            })
            .catch(error => {
              loadingWrapper.style.display = "none";
              popupError.style.display = "flex";
            });
          });
        });
      }, 1000);
    });
    
    // モーダルウィンドウを閉じる処理
    document.querySelectorAll('.popup-success .close, .popup-error .close').forEach(el => {
      el.addEventListener('click', () => {
        el.closest('.popup-success, .popup-error').style.display = 'none';
      });
    });
    document.addEventListener('click', (e) => {
      if (e.target.classList.contains('popup-success') || e.target.classList.contains('popup-error')) {
        e.target.style.display = 'none';
      }
    });
    
    // Enterキーでも送信可能
    document.querySelectorAll('.mailform input, .mailform textarea').forEach(input => {
      input.addEventListener('keypress', function(e) {
        if (e.key === 'Enter' && !e.shiftKey) {
          e.preventDefault();
          document.querySelector(".mailform").dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }));
        }
      });
    });

    PHPファイル

    メールを送るためのPHPファイルを作成します。コードは下記のとおり。

    reCAPTCHA のシークレットキーなどは隠しています(×××のところ)。

    このファイルをテーマ直下に置いたのではなく、サーバールート直下に作成しているディレクトリ「PHP」に置いています。

    メールが運営サイドと問い合わせ者に送れられたら 'success'をjsに返し、スパム判定だったらその旨を返し、メール送信に失敗したら'error'を返します。

    問い合わせ者のIPアドレスを取得して運営サイドへ知らせるようにしています。取得しなくても問題ありませんが、スパムや悪質な問い合わせへの対応のために一応取得しておきます。

    なお、取得する情報については、プライバシーポリシーに取得と目的の明示が必須となります。

    <?php
    // WordPress の環境をロード
    require_once $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php';
    session_start();
    
    // ヘッダー設定
    header("Access-Control-Allow-Origin: https://××××××.com/");
    header('Content-Type: application/json; charset=UTF-8');
    header("Access-Control-Allow-Headers: Content-Type");
    date_default_timezone_set('Asia/Tokyo');
    
    // reCAPTCHA のシークレットキー
    define('RECAPTCHA_SECRET_KEY', '××××××');
    
    // 送信元のメールアドレス
    $admin_email = '××××××@××××××.com';
    $from_email = '××××××@××××××.com';
    
    // IPアドレス取得
    function getUserIP() {
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) return $_SERVER['HTTP_CLIENT_IP'];
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) return trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]);
        return $_SERVER['REMOTE_ADDR'];
    }
    $ip_address = getUserIP();
    
    // メールヘッダインジェクション対策
    function clean_input($data) {
        return preg_replace('/[\r\n]+/', '', trim($data));
    }
    
    // フォームデータ取得&サニタイズ
    $name = isset($_POST['name']) ? sanitize_text_field(clean_input($_POST['name'])) : '';
    $company = isset($_POST['company']) ? sanitize_text_field(clean_input($_POST['company'])) : '';
    $email = isset($_POST['email']) ? sanitize_email(clean_input($_POST['email'])) : '';
    $phone = isset($_POST['phone']) ? sanitize_text_field(clean_input($_POST['phone'])) : '';
    $message = isset($_POST['message']) ? sanitize_textarea_field($_POST['message']) : '';
    $recaptcha_response = isset($_POST['recaptcha']) ? $_POST['recaptcha'] : '';
    
    // 送信回数制限(30秒以内の連続送信をブロック)
    if (!isset($_SESSION['last_send'])) {
        $_SESSION['last_send'] = time();
    } else {
        if (time() - $_SESSION['last_send'] < 30) {
            echo json_encode(['status' => 'error', 'message' => '短時間に複数の送信はできません。']);
            exit;
        }
        $_SESSION['last_send'] = time();
    }
    
    // reCAPTCHA 検証
    $recaptcha_data = [
        'secret' => RECAPTCHA_SECRET_KEY,
        'response' => $recaptcha_response,
        'remoteip' => $ip_address
    ];
    
    $options = [
        'http' => [
            'method' => 'POST',
            'header' => 'Content-type: application/x-www-form-urlencoded',
            'content' => http_build_query($recaptcha_data)
        ]
    ];
    $context = stream_context_create($options);
    $recaptcha_result = json_decode(file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $context), true);
    
    // スパム判定
    if ($recaptcha_result['action'] !== 'submit' || $recaptcha_result['score'] < 0.5) {
        echo json_encode(['status' => 'error', 'message' => 'スパム判定されました。']);
        exit;
    }
    
    // 運営サイドへの自動返信メール
    $company_display = $company !== '' ? $company : '未記入';
    $phone_display = $phone !== '' ? $phone : '未記入';
    $admin_subject = "お問い合わせがありました";
    $admin_message = <<<EOM
    $name 様より以下の内容でお問い合わせがありました。
    
    名前: $name
    会社名: $company_display
    メール: $email
    電話番号: $phone_display
    IPアドレス: $ip_address
    
    お問い合わせ内容:
    $message
    EOM;
    
    $headers = [
        "From: ㈱×××× <$from_email>",
        "Reply-To: $email",
        "Content-Type: text/plain; charset=UTF-8"
    ];
    
    // 問い合わせ者への自動返信メール
    $user_subject = "お問い合わせを受け付けました";
    $user_message = <<<EOM
    ※このメールはシステムからの自動返信です
    
    $name 様
    
    この度は、㈱××××へお問い合わせいただきありがとうございます。
    お問い合わせの件につきましては、近日中にお返事いたしますので今しばらくお待ちください。
    
    ------- お問い合わせ内容 -------
    $message
    --------------------------------
    ㈱××××
    TEL:××××
    EOM;
    
    $user_headers = [
        "From: ㈱×××× <$from_email>",
        "Content-Type: text/plain; charset=UTF-8"
    ];
    
    // メール送信
    $admin_mail_sent = wp_mail($admin_email, $admin_subject, $admin_message, $headers);
    $user_mail_sent = wp_mail($email, $user_subject, $user_message, $user_headers);
    
    // 結果
    if ($admin_mail_sent && $user_mail_sent) {
        echo json_encode(['status' => 'success']);
    } else {
        echo json_encode(['status' => 'error']);
    }
    exit;