Next.js App router + Nodemailer/EmailJS + React Hook Formでお問い合わせフォーム作成
はじめに
Next.jsを使用して既にプロジェクトが構築されていることを前提に、お問い合わせコンポーネントを作成します。
SendGridは現在、個人アカウントからの利用を受け付けていない、EmailJSは以下の理由から本番環境での使用を見送り、最終的にはプロジェクトによって異なりますが、nodemailerとtayori(無料アカウント)を採用しました。
Nodemailer
Node.jsアプリケーションから電子メールを送信するためのモジュール
Varcelでデプロイした場合にGmailを用いて本番環境でメールを送る時サーバーが地理的に別の場所にあるなどの問題により、メールがブロックされる可能性があるとのことです。
私もローカルでは問題ありませんでしたが、デプロイ後に送信できませんでした。
追記
/api
以下の動的処理は、LambdaとAPI Gateway
を使用してS3とCloudFrontでデプロイした際に問題なく動作しました!
また、メール送信時にはSlackに通知が来るようにしました。
本番環境での使用を見送った理由
EmailJS
クライアントサイドで動作するJavaScriptライブラリであり、APIキーやその他の構成データをクライアントサイドのコードに含める必要があります。これにより、ブラウザのNetworkタブ > PayloadからAPIキーなどが確認できてしまいます。
nodemailer
Node.jsアプリケーションから電子メールを送信するために広く使用されるモジュールで、SMTPサーバーを通じて直接メールを送信することができます。
SMTPサーバー
SMTPサーバー(Simple Mail Transfer Protocol Server)は、電子メールを送受信するための標準的なプロトコルを用いたサーバーです。
アプリパスワードの作成
まずは2段階プロセスを有効にしてください。
私はアプリから2段階プロセスを有効にしました。
右上のアイコンを選択し、Googleアカウントを管理
を選択してください。
セキュリティタブから2段階認証プロセス
を選択して表示される手順に沿って進めてください。
2段階プロセスを有効になったら下記にアクセスしてアプリパスワードの作成してください。
アプリ名を入力して、作成
を押してください。
アプリパスワードが作成されました。スペースが入っていると思いますが、これは除いて使用してください。
Nodemailerのインストール
npm install nodemailer
typescriptの場合は、NodemailerのためのTypeScript型定義ファイルもインストールしてください。
npm install @types/nodemailer
環境変数の設定
GMAIL_ACCOUNT=""
GMAIL_PASSWORD=""
APIルート設定
このままでは、ブラウザのNetworkタブからアプリパスワードが確認できてしまうので、APIルート設定してそこからHTTPリクエストを送るようにします。
詳しくは下記を参考にしてください。
mkdir src/app/api/contact && touch src/app/api/contact/route.ts
今回はテンプレート内容をhtml
で記述していますが、text
を使用した場合はテキストで記述することができます。
import { NextRequest, NextResponse } from 'next/server';
import nodemailer from 'nodemailer';
export async function POST(req: NextRequest) {
try {
const { data } = await req.json();
const transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 587,
auth: {
user: process.env.GMAIL_ACCOUNT,
pass: process.env.GMAIL_PASSWORD,
},
});
const mailOptions = {
from: process.env.GMAIL_ACCOUNT,
to: data.email,
subject: 'お問い合わせありがとうございます',
html: `
<p>${data.name} 様</p>
<p>この度は、お問い合わせいただきありがとうございます。</p>
<p>以下の内容でお問い合わせを受け付けました。</p>
<div style="padding: 12px; border-left: 4px solid #d0d0d0; font-style: italic;">
<p>お問い合わせ種別:</p>
<p>${data.type}</p>
<p>お問い合わせ内容:</p>
<p>${data.content}</p>
</div>
<p>お問い合わせいただいた内容につきまして、確認次第ご返信させていただきます。</p>
<p>何卒よろしくお願い申し上げます。</p>
`,
};
const res = await transporter.sendMail(mailOptions);
return NextResponse.json(res, { status: 200 });
} catch (error) {
throw error;
}
}
React-hook-formのインストール
npm install react-hook-form
import axios from 'axios';
import { SubmitHandler, useForm } from 'react-hook-form';
interface FormData {
name: string;
email: string;
type: string;
content: string;
}
export function Contact() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
const onSubmit: SubmitHandler<FormData> = async (data: FormData) => {
try {
await axios.post('/api/contact', {
data,
});
alert('お問い合わせが送信されました。');
} catch (err) {
console.log('FAILED...', err);
alert('送信に失敗しました。');
}
};
return (
<div style={{ maxWidth: '500px', margin: 'auto', padding: '50px' }}>
<h2 style={{ fontSize: '24px', fontWeight: 'bold' }}>お問い合わせ</h2>
<p>下記のフォームにご記入ください。</p>
<form
onSubmit={handleSubmit(onSubmit)}
style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}
>
<label htmlFor='name'>お名前</label>
<input
id='name'
{...register('name', { required: true })}
type='text'
placeholder=''
style={{
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
border: '1px solid #ccc',
}}
/>
{errors.name && <span style={{ color: 'red' }}>名前は必須です。</span>}
<label htmlFor='email'>メールアドレス</label>
<input
id='email'
{...register('email', { required: true, pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: "invalid email address"
} })}
type='email'
placeholder=''
style={{
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
border: '1px solid #ccc',
}}
/>
{errors.email && (
<span style={{ color: 'red' }}>
有効なメールアドレスを入力してください。
</span>
)}
<label htmlFor='type'>お問い合わせ種別</label>
<select
id='type'
{...register('type', { required: true })}
style={{
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
border: '1px solid #ccc',
}}
>
<option value=''>以下から選択してください</option>
<option value='CMS構築'>CMS構築</option>
<option value='WEBサイト作成'>WEBサイト作成</option>
<option value='その他'>その他</option>
</select>
{errors.type && (
<span style={{ color: 'red' }}>お問い合わせの種別は必須です。</span>
)}
<label htmlFor='content'>お問い合わせ内容</label>
<textarea
id='content'
{...register('content', { required: true })}
style={{
minHeight: '150px',
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
border: '1px solid #ccc',
}}
placeholder=''
/>
{errors.content && (
<span style={{ color: 'red' }}>お問い合わせ内容は必須です。</span>
)}
<button
type='submit'
style={{
width: '100%',
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
backgroundColor: '#007BFF',
color: 'white',
border: 'none',
cursor: 'pointer',
}}
>
送信
</button>
</form>
</div>
);
}
EmailJS
EmailJSはクライアントサイドで動作し、サーバー側の設定を必要としないため、特にNext.jsのようなサーバーサイドレンダリングを行うフレームワークと組み合わせると効果的です。
無料プランでは、機能が限定的ですが、月200件のメール送信が可能なので今回はこちらを利用します。
EmailJSライブラリをインストール
必要なライブラリをインストールしてください。
npm install --save @emailjs/browser
メール設定
Add New Service
を選択してください。
今回はGmailアカウントからメールを送信したいのでGmailを選択します。
Connect Account
で使用したいアカウントを選択し連携後、Create Service
を押してください。
Create New Template
を選択してメールのテンプレートを作成します。
Edit Content
からCode Editor
を選択してください。
<p>{{name}} 様</p>
<p>この度は、お問い合わせいただきありがとうございます。</p>
<p>以下の内容でお問い合わせを受け付けました。</p>
<div style="padding: 12px; border-left: 4px solid #d0d0d0; font-style: italic;">
<p>お問い合わせ種別:</p>
<p>{{type}}</p>
<p>お問い合わせ内容:</p>
<p>{{content}}</p>
</div>
<p>お問い合わせいただいた内容につきまして、確認次第ご返信させていただきます。</p>
<p>何卒よろしくお願い申し上げます。</p>
<br/>
<br/>
<p>※このメールはシステムから自動送信されています。</p>
<p>※返信不要のメールですが、ご不明点等がございましたらexample@gmail.comまでご連絡ください。</p>
入力してSave
してください。
import emailjs from '@emailjs/browser';
import { SubmitHandler, useForm } from 'react-hook-form';
interface FormData {
name: string;
email: string;
type: string;
content: string;
}
export function Contact() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
const onSubmit: SubmitHandler<FormData> = async (data: FormData) => {
const userID = process.env.NEXT_PUBLIC_EMAILJS_PUBLIC_KEY || '';
const serviceID = process.env.NEXT_PUBLIC_EMAILJS_SERVICE_ID || '';
const templateID = process.env.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID || '';
const params = {
name: data.name,
email: data.email,
type: data.type,
content: data.content,
};
try {
await emailjs.send(serviceID, templateID, params, userID);
alert('お問い合わせが送信されました。');
} catch (err) {
console.log('FAILED...', err);
alert('送信に失敗しました。');
}
};
return (
<div style={{ maxWidth: '500px', margin: 'auto', padding: '50px' }}>
<h2 style={{ fontSize: '24px', fontWeight: 'bold' }}>お問い合わせ</h2>
<p>下記のフォームにご記入ください。</p>
<form
onSubmit={handleSubmit(onSubmit)}
style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}
>
<label htmlFor='name'>お名前</label>
<input
id='name'
{...register('name', { required: true })}
type='text'
placeholder=''
style={{
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
border: '1px solid #ccc',
}}
/>
{errors.name && <span style={{ color: 'red' }}>名前は必須です。</span>}
<label htmlFor='email'>メールアドレス</label>
<input
id='email'
{...register('email', { required: true, pattern: /^\S+@\S+$/i })}
type='email'
placeholder=''
style={{
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
border: '1px solid #ccc',
}}
/>
{errors.email && (
<span style={{ color: 'red' }}>
有効なメールアドレスを入力してください。
</span>
)}
<label htmlFor='type'>お問い合わせ種別</label>
<select
id='type'
{...register('type', { required: true })}
style={{
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
border: '1px solid #ccc',
}}
>
<option value=''>以下から選択してください</option>
<option value='CMS構築'>CMS構築</option>
<option value='WEBサイト作成'>WEBサイト作成</option>
<option value='その他'>その他</option>
</select>
{errors.type && (
<span style={{ color: 'red' }}>お問い合わせの種別は必須です。</span>
)}
<label htmlFor='content'>お問い合わせ内容</label>
<textarea
id='content'
{...register('content', { required: true })}
style={{
minHeight: '150px',
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
border: '1px solid #ccc',
}}
placeholder=''
/>
{errors.content && (
<span style={{ color: 'red' }}>お問い合わせ内容は必須です。</span>
)}
<button
type='submit'
style={{
width: '100%',
padding: '10px',
fontSize: '16px',
borderRadius: '5px',
backgroundColor: '#007BFF',
color: 'white',
border: 'none',
cursor: 'pointer',
}}
>
送信
</button>
</form>
</div>
);
}
下記のようにブラウザのNetworkタブ > PayloadからAPIキーなどが確認できてしまいます。
終わりに
何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉
Discussion