NewtとNext.jsを利用して、問い合わせフォームを作成する

最終更新日:

Table of contents

このチュートリアルでは、Newtの Form AppNext.js を利用して、問い合わせフォームを作成する手順を紹介します。

記事内で使用している主なソフトウェアのバージョン

  • Next.js(next): 13.5.6
  • React Hook Form(react-hook-form): 7.47.0
  • React Google Recaptcha V3(react-google-recaptcha-v3): 1.10.1

前提条件

  • Next.jsのプロジェクトを作成済みであること

Next.jsのセットアップについて知りたい場合は、以下のドキュメントをご確認ください。

概要

Next.jsで問い合わせフォームを作成し、Newtで問い合わせデータを確認できるようにします。
はじめに、自動返信・受信通知のついたシンプルなフォームを作成し、その後でバリデーション・リダイレクト・スパム対策の処理を実装しながら、カスタマイズの方法を学習していきます。
※ ここではスタイルについては扱いません。スタイルについて確認したい方は、問い合わせ用のテンプレート newt-starter-nextjs-contact を用意しているので、そちらのコードをご確認ください。

1. Form Appを作成する

はじめに、NewtでForm Appを作成します。

1-1. Form Appの追加

「Appを追加」をクリックして「タイプを選択して追加」を選択します。
form-tutorial1.jpg

「Form App」を選択して「追加」をクリックします。
form-tutorial2.jpg

App名・App UID・Appアイコンを設定して「追加」をクリックします。
form-tutorial3.jpg

これで「Contact」という名前のForm Appが追加されました。
form-tutorial4.jpg

1-2. フォームの作成

次にフォームを作成します。「フォームを作成」ボタンをクリックし、名前を付けてフォームを作成します。
form-tutorial5.jpg

作成後に表示されるエンドポイントは、2-1で利用します。
form-tutorial6.jpg

2. Next.jsでシンプルな問い合わせフォームを作成する

続いて、Next.jsでシンプルな問い合わせフォームを作成しましょう。
ここでは /contact/simple というパスにアクセスした時に、問い合わせフォームを表示するようにします。

2-1. 環境変数の設定

Next.jsには環境変数のビルトインサポートがあり、.env.local を使用して、環境変数をロードできます。Next.jsの環境変数について、詳細は Environment Variables のドキュメントをご確認ください。

ここでは、.env.local ファイルを作成し、2-1で確認したエンドポイントの値を定義します。以下を、実際の値で置き換えて定義してください。

.env.local
1NEXT_PUBLIC_NEWT_FORM_ENDPOINT=https://xxxxx.form.newt.so/v1/xxxxx

上記のように定義しておくと、process.env.NEXT_PUBLIC_NEWT_FORM_ENDPOINT として利用できるようになります。

2-2. HTMLフォームの追加

app/contact/simple/page.tsx を作成し、以下のように記述します。
formaction 属性にはエンドポイントの環境変数を指定します。

pages/contact.tsx
1export default function Page() {
2  return (
3    <>
4      <h1>Contact us</h1>
5      <form action={process.env.NEXT_PUBLIC_NEWT_FORM_ENDPOINT} method="post">
6        <label htmlFor="name">Name</label>
7        <input id="name" name="name" />
8        <label htmlFor="email">Email</label>
9        <input id="email" name="email" type="email" />
10        <label htmlFor="message">Message</label>
11        <textarea id="message" name="message"></textarea>
12        <button type="submit">Submit</button>
13      </form>
14    </>
15  )
16}

各属性について説明します。

label要素のhtmlFor属性と、input要素・textarea要素のid属性
label要素for 属性は、JSXでは htmlFor として記述します。input要素textarea要素id 属性と一致するように設定してください。
※ 詳細は、Reactの アクセシブルなフォーム のドキュメントをご確認ください。

input要素・textarea要素のname属性
input要素・textarea要素のnameで設定された値をもとに、投稿データのスキーマが決まります。例えば上記の例では、nameemailmessage フィールドを持ったスキーマが作成されます。
詳細については、フィールドの仕様 のドキュメントをご確認ください。

input要素のtype属性
お好みの入力形式に応じて、inputの型 を選択してください。デフォルトでは text となります。

これで http://localhost:3000/contact/simple にアクセスすると、以下のような問い合わせフォームが作成できました。
※ スタイルについてはお好みで設定してください
nextjs-form1.png

2-3. 自動返信メールの設定

自動返信メール の設定をします。自動返信メールを設定することで、投稿を送信したエンドユーザーに対して、設定した内容のメールを自動で送信できます。

※ Newtは自動返信メールの送信先メールアドレスを検出するために email フィールドを使います。input要素のname属性に email を指定し、エンドユーザーのメールアドレスが入力されるようにしてください。

「App設定」から該当のフォームをクリックし、「メール設定」を選択します。
「自動返信メールを有効にする」にチェックを入れ、件名と本文を入力し、「保存」をクリックします。

form-tutorial7.jpg

自動返信メールでは {submission.name} のように記述することで、メールの件名や本文に、投稿データの内容を埋め込めます。
2-2で、input要素・textarea要素のname属性に nameemailmessage を指定したので、ここでは {submission.name}{submission.email}{submission.message} を利用できます。

2-4. 受信通知メールの設定

受信通知メール の設定をします。受信通知メールを設定することで、エンドユーザーから新しいメッセージを受信した際に、メールで通知を受け取れます。

「App設定」から該当のフォームをクリックし、「メール設定」を選択します。
「受信通知メールを有効にする」にチェックを入れ、通知を受け取るメールアドレス・件名・本文を設定し、「保存」をクリックします。

form-tutorial8.jpg

受信通知メールでも、自動返信メールと同様に、投稿データの内容を埋め込めます。

2-5. 挙動の確認

これで、シンプルな問い合わせフォームを作成できました。
実際に動かして、挙動を確認してみましょう。

まず、開発サーバーを立ち上げて、問い合わせフォームを表示します。ここでは http://localhost:3000/contact/simple にアクセスします。
適当な内容を入力して送信すればよいですが、設定したEmail(name属性に email を指定した入力値)に自動返信メールが届くので、ご自身のメールアドレスを入力するよう気をつけてください。
nextjs-form2.png

送信が成功すると、以下の画面が表示されます。
form-tutorial9.png

管理画面では、以下のようにデータが確認できます。
form-tutorial10.jpg

次に自動返信メールを確認します。以下のようなメールが飛んでいれば成功です。
form-tutorial11.png

最後に受信通知メールを確認します。以下のようなメールが飛んでいれば成功です。
form-tutorial12.png

これでシンプルなフォームの作成・自動返信メールの設定・受信通知メールの設定ができました。ここからはフォームのカスタマイズをしながら、より高度なフォームの作成方法を学んでいきましょう。

3. バリデーションを追加する

クライアントサイドのバリデーションについて、以下の順で紹介します。

  • input要素の属性を利用したバリデーション
  • JavaScriptを利用したバリデーション(React Hook Formを利用したバリデーション)

3-1. input要素の属性を利用したバリデーション

HTML5のフォームバリデーションを利用することで、JavaScriptなしでバリデーションを実行できます。よく使われるものについては HTML5のフォームバリデーション をご確認ください。

例えば、name を必須にしたい場合は、以下のように required を指定します。

<input id="name" name="name" required />

もし name を指定せず、送信を実行した場合は、以下のように表示されます。
nextjs-form3.png

3-2. JavaScriptを利用したバリデーション

HTMLネイティブのフォームバリデーションでは足りない場合、JavaScriptを利用することで、より柔軟なバリデーションを実行できます。
ここでは、バリデーションのタイミングを変え、さらにエラーメッセージをカスタマイズしてみましょう。

JavaScriptを利用してフォームを送信する場合(Acceptヘッダーでapplication/jsonを指定してレスポンスをJSONデータとして受け取る場合)、ご自身で送信後の表示を制御していただくことになります。
デフォルトのThanksページ・Errorページへのリダイレクトや カスタムリダイレクト は行われなくなりますので、ご注意ください。

ここでは React Hook Form を利用してバリデーションを行います。まずは react-hook-form をインストールしましょう。

npm install react-hook-form
# or
yarn add react-hook-form

続いて、以下のように app/contact/react-hook-form/page.tsx を作成します。

app/contact/react-hook-form/page.tsx
1'use client'
2import { useForm } from 'react-hook-form'
3
4type FormValues = {
5  name: string
6  email: string
7  message: string
8}
9
10export default function Page() {
11  const {
12    register,
13    handleSubmit,
14    formState: { errors },
15  } = useForm<FormValues>({ mode: 'onChange' })
16
17  const onSubmit = handleSubmit(async (data) => {
18    const formData = new FormData()
19    Object.entries(data).forEach(([key, value]) => {
20      formData.append(key, value)
21    })
22
23    await fetch(process.env.NEXT_PUBLIC_NEWT_FORM_ENDPOINT ?? '', {
24      method: 'POST',
25      body: formData,
26      headers: {
27        Accept: 'application/json',
28      },
29    })
30  })
31
32  return (
33    <>
34      <h1>Contact us</h1>
35      <form onSubmit={onSubmit}>
36        <label htmlFor="name">Name*</label>
37        <input
38          id="name"
39          {...register('name', { required: 'Name is required' })}
40          aria-describedby="error-name-required"
41        />
42        {errors?.name && (
43          <span id="error-name-required" aria-live="assertive">
44            {errors.name.message}
45          </span>
46        )}
47        <label htmlFor="email">Email</label>
48        <input id="email" {...register('email')} />
49        <label htmlFor="message">Message</label>
50        <textarea id="message" {...register('message')}></textarea>
51        <button type="submit">Submit</button>
52      </form>
53    </>
54  )
55}

以下のことを行っています。

  • 入力値の型として、FormValues を定義し、useForm にジェネリクスとして渡します
  • useFormmode という引数を渡しています。これはどのタイミングでバリデーションを行うか制御しています。ここでは onChange を指定し、変更のたびにバリデーションを実行します
  • 入力されたフォームのデータは handleSubmit 関数で data として利用できます
  • FormData を利用して、送信するデータ formData を作成します
  • フェッチ API を利用して、Form Appのエンドポイントにデータを送ります。この時、Acceptヘッダーに 'application/json' を指定します

また、以下の部分で name がない場合に Name is required というエラーメッセージを出しています。
register メソッドを利用して、様々なバリデーションが設定できるので、ぜひお好きなバリデーションをお試しください。

1<label htmlFor="name">Name*</label>
2<input
3  id="name"
4  {...register('name', { required: 'Name is required' })}
5  aria-describedby="error-name-required"
6/>
7{errors?.name && (
8  <span id="error-name-required" aria-live="assertive">
9    {errors.name.message}
10  </span>
11)}

http://localhost:3000/contact/react-hook-form にアクセスして、以下のことを確認します。

  • 入力変更があったタイミングでバリデーションが実行されること
  • name が入力されていない場合に Name is required というエラーメッセージが出ること

nextjs-form4.png

4. リダイレクトを追加する

次に、3-2で作成したフォームにリダイレクトを追加してみましょう。
送信が成功した場合のページと、失敗した場合のページを用意して、送信結果に応じてリダイレクトを行います。

4-1. 送信成功ページ・送信失敗ページを用意する

送信成功ページとして、以下を用意します。
app/thanks/page.tsx を作成します。

app/thanks/page.tsx
1'use client'
2import { useRouter } from 'next/navigation'
3
4export default function Thanks() {
5  const router = useRouter()
6
7  return (
8    <>
9      <h1>Thank you!</h1>
10      <div>
11        <button type="button" onClick={() => router.back()}>
12          Back to Previous Page
13        </button>
14      </div>
15    </>
16  )
17}

送信失敗ページとして、以下を用意します。
app/error/page.tsx を作成します。

app/error/page.tsx
1'use client'
2import { useRouter } from 'next/navigation'
3
4export default function Error() {
5  const router = useRouter()
6
7  return (
8    <>
9      <h1>Error!</h1>
10      <div>
11        <button type="button" onClick={() => router.back()}>
12          Back to Previous Page
13        </button>
14      </div>
15    </>
16  )
17}

4-2. リダイレクトを設定する

app/contact/react-hook-form/page.tsx を以下のように修正します。

app/contact/react-hook-form/page.tsx
'use client'
import { useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'

type FormValues = {
  name: string
  email: string
  message: string
}

export default function Page() {
  const router = useRouter()
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({ mode: 'onChange' })

  const onSubmit = handleSubmit(async (data) => {
    const formData = new FormData()
    Object.entries(data).forEach(([key, value]) => {
      formData.append(key, value)
    })

    await fetch(process.env.NEXT_PUBLIC_NEWT_FORM_ENDPOINT ?? '', {
      method: 'POST',
      body: formData,
      headers: {
        Accept: 'application/json',
      },
    })
    try {
      const response = await fetch(
        process.env.NEXT_PUBLIC_NEWT_FORM_ENDPOINT ?? '',
        {
          method: 'POST',
          body: formData,
          headers: {
            Accept: 'application/json',
          },
        },
      )

      if (response.ok) {
        router.push('/thanks')
      } else {
        router.push('/error')
      }
    } catch (err) {
      router.push('/error')
    }
  })

  return (
    (省略)
  )
}

送信が成功した場合(response.ok がtrueの場合)は /thanks にリダイレクトし、送信成功ページを表示しています。
送信が失敗した場合(response.ok がfalseの場合、またはエラーが発生した場合)は /error にリダイレクトし、送信失敗ページを表示しています。

これでリダイレクトを追加することができました。

5. スパム対策を実装する

最後に、スパム対策 として Google reCAPTCHA v3 の設置方法について説明します。

5-1. サイトキーとシークレットキーの取得

Google reCAPTHAのコンソール にアクセスし、新しいサイトを登録します。

reCAPTCHAタイプは reCAPTCHA v3 を選択します。
ここではローカル環境でのテストを行うので、ドメインには localhost を追加します。本番環境で設定したい場合は、reCAPTCHAを導入したいサイトのドメインを設定してください。
form-tutorial13.jpg

全ての項目を入力して「送信」をクリックすると、サイトキーとシークレットキーが表示されます。
form-tutorial14.jpg

5-2. シークレットキーの登録

Newtの管理画面にアクセスし、「App設定」を開きます。該当のフォームをクリックし、「フォーム設定」を選択します。
「スパム対策」のセクションより、「Google reCAPTCHA v3を有効にする」にチェックを入れ、5-1で取得したシークレットキーを貼り付けて保存します。
spam-filtering1.png

5-3. 環境変数の設定

5-1で確認したサイトキーを NEXT_PUBLIC_RECAPTCHA_SITE_KEY として環境変数に追加します。

.env.local
NEXT_PUBLIC_NEWT_FORM_ENDPOINT=https://xxxxx.form.newt.so/v1/xxxxx
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=xxxxxxxxxxxxxxx

5-4. Next.jsでの設定

最後にNext.jsでの設定を行います。ここでは React Google Recaptcha V3 を用いて、設定します。まずはReact Google Recaptcha V3をインストールしましょう。

npm install react-google-recaptcha-v3
# or
yarn add react-google-recaptcha-v3

React Google Recaptcha V3を利用するためには、該当のコンポーネントを GoogleReCaptchaProvider でラップする必要があるので app/contact/recaptcha/layout.tsx を作成します。
※ ここでは app/contact/recaptcha/page.tsx にフォームを作成するとします。

以下のように作成します。
GoogleReCaptchaProvider に設定できるオプションの詳細は React Google Recaptcha V3 をご確認ください。

app/contact/recaptcha/layout.tsx
1'use client'
2import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'
3
4export default function Layout({ children }: { children: React.ReactNode }) {
5  return (
6    <GoogleReCaptchaProvider
7      reCaptchaKey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? ''}
8      language="ja"
9    >
10      {children}
11    </GoogleReCaptchaProvider>
12  )
13}

ここでは languageja を設定しているため、問い合わせフォームの右下に表示されるreCAPTCHAのバッジが日本語になります。他の言語で設定したい方は、Googleの Language Codes のドキュメントを参考に設定してください。
form-tutorial15.jpg

最後に送信データに googleReCaptchaToken を追加します。
app/contact/recaptcha/page.tsx を、以下のように作成します。

app/contact/recaptcha/page.tsx
1'use client'
2import { useRouter } from 'next/navigation'
3import { useGoogleReCaptcha } from 'react-google-recaptcha-v3' // 1
4import { useForm } from 'react-hook-form'
5
6type FormValues = {
7  name: string
8  email: string
9  message: string
10  googleReCaptchaToken: string
11}
12
13export default function Page() {
14  const router = useRouter()
15  const {
16    register,
17    handleSubmit,
18    formState: { errors },
19  } = useForm<FormValues>({ mode: 'onChange' })
20  const { executeRecaptcha } = useGoogleReCaptcha()
21
22  const onSubmit = handleSubmit(async (data) => {
23    if (!executeRecaptcha) return // 2
24    const token = await executeRecaptcha('submit')
25    data.googleReCaptchaToken = token // 3
26
27    const formData = new FormData()
28    Object.entries(data).forEach(([key, value]) => {
29      formData.append(key, value)
30    })
31
32    try {
33      const response = await fetch(
34        process.env.NEXT_PUBLIC_NEWT_FORM_ENDPOINT ?? '',
35        {
36          method: 'POST',
37          body: formData,
38          headers: {
39            Accept: 'application/json',
40          },
41        },
42      )
43
44      if (response.ok) {
45        router.push('/thanks')
46      } else {
47        router.push('/error')
48      }
49    } catch (err) {
50      router.push('/error')
51    }
52  })
53
54  return (
55    <>
56      <h1>Contact us</h1>
57      <form onSubmit={onSubmit}>
58        <label htmlFor="name">Name*</label>
59        <input
60          id="name"
61          {...register('name', { required: 'Name is required' })}
62          aria-describedby="error-name-required"
63        />
64        {errors?.name && (
65          <span id="error-name-required" aria-live="assertive">
66            {errors.name.message}
67          </span>
68        )}
69        <label htmlFor="email">Email</label>
70        <input id="email" {...register('email')} />
71        <label htmlFor="message">Message</label>
72        <textarea id="message" {...register('message')}></textarea>
73        <button type="submit">Submit</button>
74      </form>
75    </>
76  )
77}

以下のことを行っています。

  1. useGoogleReCaptcha をインポートします
  2. executeRecaptcha のnullチェックをし、利用可能か確認します
  3. googleReCaptchaToken というフィールド名でトークンを送信します

これでGoogle reCAPTCHA v3によるスパム対策が実装できました。

もし、バッジを非表示にしたい場合、Googleの reCAPTCHA バッジを非表示にします。どうすればよいですか? のドキュメントをご確認ください。

5-5. エラーが発生してしまう場合

reCAPTCHAを利用したフォームでエラーが発生してしまう場合は、まずはどのようなエラーが発生しているかレスポンスを確認しましょう。
以下のようなログを追加すると、返却されているデータを確認できます。

const res = await response.json()
console.log(res)

正常にフォームが送信されている場合は以下のようなデータが返却されます。

{
  "status": 200,
  "success": true
}

一方、エラーが発生した場合は、エラー内容が返却されます。
例えば、googleReCaptchaToken というフィールド名でトークンが送信されていない場合、以下のようなエラーが返却されます。

{
  "status": 400,
  "code": "InvalidGoogleRecaptchaToken"
  "message": "The google reCAPTHCHA token is invalid."
}

エラー内容の一覧は Error types に記載がありますので、どの code が表示されているか確認して、問題の原因を確認してみてください。

よくある問題としては、以下のようなものがあります。ご注意ください。

  • 管理画面でシークレットキーが登録されていない(InvalidGoogleRecaptchaSecret)
  • googleReCaptchaToken というフィールド名で、トークンが送信されていない(InvalidGoogleRecaptchaToken)
  • トークンの作成から2分以上経過し、期限が切れたトークンを送信している(InvalidGoogleRecaptchaToken)
  • Google reCAPTHAのコンソールに、対象のドメインを追加していない(SpamDetected)

次のステップ

Form Appでは、他にもさまざまな機能があるので、興味のある方はぜひ以下のドキュメントもご覧ください。

NewtMade in Newt