Next.js + React Hook Form + バニラPHPでお問い合わせメールを送る

Javascript
この記事は約22分で読めます。

この記事は、3ヶ月前に書かれました。

はじめに

Next.js(React)でフロント側のフォーム作成といえば、React Hook Formが定番です。ではメールを送信するバックエンド側はどうでしょうか。

従来のMPA(Multiple Page Application)※1で構築したサイトであれば、入力画面からPHP等で作成し、確認画面、完了画面とセッションで値を保持して、最終的にはsendmailもしくはSMTP接続にてメール送信という流れでした。

Next.js(React)のSPA(Single Page Application)※2で構築されたお問い合わせフォームの事例では、SendGridやEmailJS、MicroCMSと連携する方法がなどが出てきます。Vercelとsendgridの組み合わせも定番ですね。
企業によっては、外部サービスの利用にはセキュリティの関係で安易に採用できないという場合があるでしょう。また、サーバーについてもVercelの利用が出来ず、一般的なレンタルサーバーしか使用できない場合があります。その場合、当然ながらnode.jsが動かせないという悩みがあります。
素直にMPAで構築しておけば幸せになれるのに、どうしてもトレンドのSPAにしたい!という場合もあるでしょう。

今回は、外部サービスを使用せず、バニラPHP※3を用いて自身のサーバー内だけでメール送信まで完結する方法をご紹介します。また、サーバー側でnode.jsが使えない前提で、Next.jsでSG(静的サイト)としてビルドしさくらサーバーにデプロイする方法も簡単に解説します。

※1 HTTP GETが来たらサーバー側でリクエストに応じたHTMLを組み上げてブラウザに返す(クラシックSSR)
※2 単一のHTMLで画面遷移をクライアントサイド側のJSでおこなう
※3 バニラPHPとはフレームワークを使用しない素のPHPのことです。バニラJSと同義語です。

前提環境

  • NextJS 14
  • React 18
  • React Hook Form 7
  • axios 1.6
  • PHP 8.3
  • FreeBSD 13
  • Apache 2.4
Get Started
...

Next.jsとReact Hook Form、その他ライブラリのインストール

今回はバックエンドとの連携を重点に紹介しますので、インストールはさらっと流します。詳しくしりたい場合はドキュメントなどを参照ください。

Next.jsでプロジェクトを作成します。

npx create-next-app@latest

以下のようにしました。
プロジェクト名はnextformとし、srcはYes、App RouterもYes、他はお好みで。

続いてReact Hook Form、非同期にHTTP通信をおこなうライブラリaxiosのインストールをおこないます。作成されたプロジェクト内のディレクトリに入って以下のコマンドを実行します。

npm install react-hook-form
npm install axios

では、一度サーバーを立ちあげて確認します。

npm run dev

http://localhost:3000 にアクセスして以下の画面が表示されればインストールは完了です。

入力画面を作成

/src/page.jsを開き、一旦中身を全て消します。
必要なライブラリをインポートします。
クライアントコンポーネントとして動作しますのでuse clientが必要です。これも最近のNext.jsから必要となりました。

'use client'
import { useForm } from "react-hook-form";
import axios from "axios";
import { useRouter  } from "next/navigation";

入力フォームを作成していきます。
inputを一部抜粋します。{…register}の部分がReact Hook Formの実装となります。直感的に見て分かりやすいと思います。機能は公式サイトをご確認ください。

  <input
    type="text"
    {...register("name", {
      required: "お名前(漢字)を入力してください",
      maxLength: {
        value: 20,
        message: "20文字以下で入力してください",
      },
    })}
  />

未入力など、バリデーションに引っかかった場合には以下のようにエラーメッセージを表示させます。

{errors.name?.message && (
  <p className="error-message">{errors.name?.message}</p>
)}

page.jsの全体像は以下になります。
お名前、メールアドレス、内容の3つのみのシンプルなものにしました。

'use client'
import { useForm } from "react-hook-form";
import axios from "axios";
import { useRouter  } from "next/navigation";

export default function Contact(){

  const router = useRouter();

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm()

  const onSubmit = async (data) => {
    try {
      const response = await axios.post(
        process.env.NEXT_PUBLIC_FETCH_URL,
        JSON.stringify(data)
      );
      console.log(response.data);
      router.push('/complete');

    } catch (error) {
      console.log(error);
      router.push('/error');
    }
      
  }

  return (
    <>
    <section className="Contact">
        <form onSubmit={handleSubmit(onSubmit)}>
          <div className="form-item">
            <label>
              <span className="required">必須</span>
              <span className="label">お名前(漢字)</span>
              <input
                type="text"
                {...register("name", {
                  required: "お名前(漢字)を入力してください",
                  maxLength: {
                    value: 20,
                    message: "20文字以下で入力してください",
                  },
                })}
              />
            </label>
            {errors.name?.message && (
              <p className="error-message">{errors.name?.message}</p>
            )}
          </div>

          <div className="form-item">
            <label>
              <span className="required">必須</span>
              <span className="label">メールアドレス</span>
              <input
                type="text"
                {...register("email", {
                  required: "メールアドレスを入力してください",
                  maxLength: {
                    value: 50,
                    message: "50文字以下で入力してください",
                  },
                  pattern: {
                    value:
                      /^[a-zA-Z0-9_.+-]+@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/,
                    message: "正しいメールアドレスを入力してください。",
                  },
                })}
              />
            </label>
            {errors.email?.message && (
              <p className="error-message">{errors.email?.message}</p>
            )}
          </div>

          <div className="form-item">
            <label>
              <span className="required">必須</span>
              <span className="label">コメント</span>
              <textarea
                {...register("comment", {
                  required: "コメントを入力してください",
                  maxLength: {
                    value: 1000,
                    message: "1000文字以下で入力してください",
                  },
                })}
              />
            </label>
            {errors.comment?.message && (
              <p className="error-message">{errors.comment.message}</p>
            )}
          </div>
          <div className="submit-button">
            <button type="submit">送信</button>
          </div>
        </form>
      </section>
	  </>
    )
}

以下、入力フォーム画面になります。適当にCSSを適用しています。TailwindowCSSなりMaterialUIなりで適宜整えてください。私はこれらのCSSフレームワークは嫌いなので使いません。
バリデーションの動作確認をおこないます。未入力のまま送信実行すると、問題なくバリデーションエラーが発生します。

次に、サーバーへ入力内容を投げるロジックです。
非同期送信となるのでasync,awaitを使用します。また、JSONデータに変換して送信するので、JSON.stringify(data)とします。
問題なく送信完了した場合は/completeへ、何等かのエラーが発生した場合は/errorへルーティングさせます。
process.env.NEXT_PUBLIC_FETCH_URLという行がありますが、PHPプログラムへのパスになります。これはのちほど設定します。

  const onSubmit = async (data) => {
    try {
      const response = await axios.post(
        process.env.NEXT_PUBLIC_FETCH_URL,
        JSON.stringify(data)
      );
      console.log(response.data);
      router.push('/complete');

    } catch (error) {
      console.log(error);
      router.push('/error');
    }
      
  }

エラー画面、完了画面を作成

/src/app/の下に/errorと/completeのディレクトリを作成し、それぞれにpage.jsというファイルを作成します。ここは簡単に作成します。
Next.jsの以前のバージョンとはルーティングが変わっています。Page RouterからApp Routerに変わったことにより、ファイルの置き場所が以前と違います。
/appディレクトリの下にフォルダ作成し、配下にpage.jsやpage.tsxを置きます。

export default function Error(){
    return (
    <>
      <section className="Contact">
        <h2 className="t-center">Contact</h2>
        <p className="t-center marginT3em red">送信エラーが発生しました</p>
      </section>	
	  </>
    )
}
export default function Complete(){
    return (
    <>
      <section className="Contact">
        <h2  className="t-center">Contact</h2>
        <p className="t-center marginT3em">お問い合わせありがとうございました<br />数日以内に回答させていただきます</p>
      </section>	
	  </>
    )
}

では、フォームに正しく入力して送信実行してみます。
現時点では、サーバー側のプログラムを作成していませんのでエラーとなります。
以下の画面が表示されればルーティングは問題ありません。エラー画面への遷移もSPAでサクサク動作するので気持ちいですね。

envファイルを準備

プロジェクトのディレクトリ直下に.envファイルを新規作成します。
こちら、何に使用するかというと、送信先のパスを記述します。作成するファイル名により適宜変更して設定してください。

NEXT_PUBLIC_FETCH_URL=https://設置ドメイン名/send/

PHPで送信プログラムを作成する

サーバーサイド側の送信プログラムをPHPで作成します。
送信ロジックについては、最も簡単なsendmailを使用します。より確実な送信はSMTPですが、今回はReactの解説ですので詳細は割愛します。

設置先を/send/index.phpと想定して作成します。

<?php
header("Access-Control-Allow-Origin: *");
header('Content-Type: application/json; charset=UTF-8');


$subject = "ホームページよりお問合せがありました"; //メールタイトル
$to = 'メールアドレス'; //メール送信先


$url = array('https://www.ドメイン名','https://ドメイン名');
$flg = 0;
foreach($url as $myuri){
	if( strncmp($_SERVER['HTTP_REFERER'] , $myuri , strlen($myuri)) == 0 ){
		$flg = 1;
	}
}
	
if($flg != 1){
	header("HTTP/1.1 404 Not Found");
	exit;
}

$json = file_get_contents("php://input");
$contents = json_decode($json, true);

if(isset($contents["name"])) { //氏名があればメール送信処理(簡易的チェック)

	file_put_contents("contact.json", $json, FILE_APPEND);

	$header  = "From: $to";
	$comment  = "お名前|".$contents["name"]."\n";
	$comment .= "かな|".$contents["kana"]."\n";
	$comment .= "会社名|".$contents["company"]."\n";
	$comment .= "メールアドレス|".$contents["email"]."\n";
	$comment .= $contents["comment"]."\n";

	mb_send_mail($to, $subject, $comment, $header);
	$arr["status"] = "sendOk";

}else{
	header("HTTP/1.1 404 Not Found");
	exit;
}

print json_encode($arr, JSON_PRETTY_PRINT);

戻り値をJsonで返すので、headerを以下のように指定します。

header("Access-Control-Allow-Origin: *");
header('Content-Type: application/json; charset=UTF-8');

簡易的に直アクセスを防止します。リファラが自ドメインでない場合は404エラーを吐きます。Next.js側ではエラーをキャッチしますので、エラー画面へルーティングされます。
許可ドメインは配列に羅列することで複数のドメインを対象とすることができます。

//リファラをチェックして直アクセスを排除(簡易的)
$url = array('https://www.ドメイン名','https://ドメイン名','http://ドメイン名');
$flg = 0;
foreach($url as $myuri){
	if( strncmp($_SERVER['HTTP_REFERER'] , $myuri , strlen($myuri)) == 0 ){
		$flg = 1;
	}
}

//リファラ不正の場合は404エラー	
if($flg != 1){
	header("HTTP/1.1 404 Not Found");
	exit;
}

Next.jsから送られてきたJsonデータを取得します。取得したJsonデータをデコードして連想配列にぶちこみます。

$json = file_get_contents("php://input");
$contents = json_decode($json, true);

簡易的に入力チェックを行います。
今回はお名前が空でなければ送信処理を実行しますが、実務ではもっと深いところまでチェックします。

if(isset($contents["name"])) {

メールが管理者に届かない場合もあるため、念のためサーバーにもログを保存します。サンプルではjsonデータのままで保存していますが、カンマ区切りのCSVなど適宜変更してください。
実務ではセキュリティを考慮し、.htaccessなどで該当ファイルの外部からのアクセスをブロックしてください。

file_put_contents("contact.json", $json, FILE_APPEND);

メール文面の生成と送信実行処理です。
こちらは簡易的な送信方法になります。実際にはヘッダにreply-toやらenvelope-fromやら、いろいろくっつけて送信します。スパム判定にならないように注意しましょう。
配列$arrにNext.jsに返す文字列を入れています。

$header  = "From: $to";
$comment  = "お名前|".$contents["name"]."\n";
$comment .= "かな|".$contents["kana"]."\n";
$comment .= "会社名|".$contents["company"]."\n";
$comment .= "メールアドレス|".$contents["email"]."\n";
$comment .= $contents["comment"]."\n";

mb_send_mail($to, $subject, $comment, $header);
$arr["status"] = "sendOk";

送信が問題なければ、$arr配列をJsonデータに変換して出力します。

print json_encode($arr, JSON_PRETTY_PRINT);

Next.js側では、戻り値のデータを以下のように確認できます。値により処理を振り分けてもよいかもしれません。

console.log(response.data);

Next.jsでビルドしてデプロイ

楽しいビルドのお時間です。
今回はSG(静的サイト)としてビルドし、さくらサーバーにアップロードしてみたいと思います。
まずはnext.config.jsを以下のように設定します。肝心なのはoutput: 'export'です。
これを入れることにより、/outに静的HTMLとして書き出されます。

/** @type {import('next').NextConfig} */
const nextConfig = {
    output: 'export',
    trailingSlash: true
};

export default nextConfig;
Deploying: Static Exports | Next.js
...

あと、私はpackage.jsonのbuildを以下のようにしました。

  "scripts": {
    "dev": "next dev",
    "build": "cross-env NODE_ENV=production next build",
    "start": "next start",
    "lint": "next lint"
  },

では、一度Nextサーバーを停止してから以下のビルドコマンドを叩きます。

npm run build

ビルドが無事完了しました。
/outディレクリ配下に静的サイトとしてファイル一式が書き出されています。これをまるごとサーバーにアップロードします。
注意が必要なのはサブディレクトリに設置する想定でビルドしていないため、ルートディレクトリに設置してテストをおこなってください。サブディレクトリに設置する場合は、また別の設定が必要です。

では、早速送信テストをこなってみます。

以下のように無事にメールが届きました。

サーバーのログファイルにも無事に保存されました。

{"name":"てすとたろう","email":"メールアドレス","comment":"これはテスト送信ですよ\nこれはテスト送信ですよ\nこれはテスト送信ですよ"}

まとめ

Next.jsは進化が早く、破壊的バージョンアップがあった場合は同じ手法が通用しません。今回紹介した方法は現時点での最新版Next.jsを使用しています。
バックエンド側のPHP界隈については、わりとゆっくりとした更新頻度なので、さほど変わらないと思います。ただ、紹介したPHPプログラムはかなり簡易的なものですので、実務でメール送信をおこなうにはRFCに準拠した形でヘッダをもっと詳細に構築する必要があります。一行に入る文字数も決まっていますので、長い文面を入力された場合の対策も必要です。
さらに、二受送信防止なども必要になってきます。