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

最終更新日:

Table of contents

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

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

  • Astro(astro): 4.4.0
  • @astrojs/react(@astrojs/react): 3.0.10
  • React Hook Form(react-hook-form): 7.50.1

前提条件

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

概要

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

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

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. Astroでシンプルな問い合わせフォームを作成する

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

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

src/pages/contact.astro を作成し、以下のように記述します。
formaction 属性に設定している https://xxxxxx.form.newt.so/v1/xxxxxx の値については、1-2で確認したエンドポイントの値に置き換えてください。

src/pages/contact.astro
1---
2import Layout from '../layouts/Layout.astro'
3---
4
5<Layout
6  title="Newt・Astroフォーム"
7  description="NewtとAstroを利用した問い合わせフォームです"
8>
9  <div>
10    <h1>Contact us</h1>
11    <form action="https://xxxxxx.form.newt.so/v1/xxxxxx" method="post">
12      <label for="name">
13        Name
14        <input id="name" name="name" />
15      </label>
16      <label for="email">
17        Email
18        <input id="email" name="email" type="email" />
19      </label>
20      <label for="message">
21        Message
22        <textarea id="message" name="message"></textarea>
23      </label>
24      <button type="submit">Submit</button>
25    </form>
26  </div>
27</Layout>

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

label要素のfor属性と、input要素・textarea要素のid属性
label要素for 属性は、input要素textarea要素id 属性と一致するように設定します。

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

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

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

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

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

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

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

form-tutorial7.jpg

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

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

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

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

form-tutorial8.jpg

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

2-4. 挙動の確認

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

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

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

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

JavaScriptを利用したバリデーションを行わない場合、3-2のステップを飛ばして構いません。またステップ4からは、JavaScriptを利用していない3-1までのコードを元に、スパム対策とカスタムリダイレクトの実装を行います。

ここではReact用のインテグレーションを追加し、React Hook Form を利用したバリデーションを行います。

3-2-1. React用インテグレーションの追加

はじめに astro add コマンドを利用して、React用のインテグレーションを追加します。
詳細はAstroの @astrojs/react のドキュメントをご確認ください。
以下のどれかのコマンドを実行します。

npx astro add react
# or
yarn astro add react

コマンドを実行すると、以下の質問を聞かれるので、お好きな設定を選びましょう。

  • @astrojs/react, @types/react-dom, @types/react, react-dom, react をインストールするか(ここでは yes を選択)
  • astro.config.mjsの変更を許可するか(ここでは yes を選択)
  • tsconfig.jsonの変更を許可するか(ここでは yes を選択)

yarnを利用した場合、以下のように表示されます。

$ yarn astro add react
✔ Resolving packages...

  Astro will run the following command:
  If you skip this step, you can always run it yourself later
 
 ╭─────────────────────────────────────────────────────────╮
 │ yarn add @astrojs/react@^3.0.10 @types/react@^18.2.57 @types/react-dom@^18.2.19 react@^18.2.0 react-dom@^18.2.0  │
 ╰─────────────────────────────────────────────────────────╯

✔ Continue? … yes
✔ Installing dependencies...

  Astro will make the following changes to your config file:

 ╭ astro.config.mjs ──────────────╮
 │ import { defineConfig } from 'astro/config';  │
 │                                               │
 │ import react from "@astrojs/react";           │
 │                                               │
 │ // https://astro.build/config                 │
 │ export default defineConfig({                 │
 │   integrations: [react()]                     │
 │ });                                           │
 ╰───────────────────────╯

✔ Continue? … yes

   success  Added the following integration to your project:
  - @astrojs/react

  Astro will make the following changes to your tsconfig.json:

 ╭ tsconfig.json ─────────────╮
 │ {                                       │
 │   "extends": "astro/tsconfigs/strict",  │
 │   "compilerOptions": {                  │
 │     "jsx": "react-jsx",                 │
 │     "jsxImportSource": "react"          │
 │   }                                     │
 │ }                                       │
 ╰────────────────────╯

✔ Continue? … yes

   success  Successfully updated TypeScript settings
✨  Done in 49.82s.

3-2-2. React Hook Formの追加

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

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

3-2-3. ContactFormコンポーネントの追加

続いて、ContactFormコンポーネントを追加します。
以下のように src/components/ContactForm.tsx を追加します。
fetch の リソースとして設定されている https://xxxxxx.form.newt.so/v1/xxxxxx の値については、1-2で確認したエンドポイントの値に置き換えてください。

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

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

  • 入力値の型として、FormValues を定義し、useForm にジェネリクスとして渡します
  • useFormmode という引数を渡しています。これはどのタイミングでバリデーションを行うか制御しています。ここでは onChange を指定し、変更のたびにバリデーションを実行します
  • 入力されたフォームのデータは handleSubmit 関数で data として利用できます
  • FormData を利用して、送信するデータ formData を作成します
  • フェッチ API を利用して、1-2で確認したエンドポイントにデータを送ります。この時、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)}

3-2-4. 作成したContactFormコンポーネントの利用

最後に src/pages/contact.astro からContactFormコンポーネントをインポートして利用します。

src/pages/contact.astro
1---
2import ContactForm from '../components/ContactForm'
3import Layout from '../layouts/Layout.astro'
4---
5
6<Layout
7  title="Newt・Astroフォーム"
8  description="NewtとAstroを利用した問い合わせフォームです"
9>
10  <ContactForm client:load />
11</Layout>

ContactFormコンポーネントをハイドレートするために clientディレクティブ を利用しています。client:load を設定することで、ページロード時に即座にハイドレートが実行されます。
これでバリデーションの設定ができました。以下のことを確認します。

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

astro-form4.png

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

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

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

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

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

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

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

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

4-3. @types/grecaptchaのインストール

後述の処理で必要になるため、@types/grecaptcha をインストールしておきます。

npm install --save-dev @types/grecaptcha
# or
yarn add -D @types/grecaptcha

4-4. Astroでの設定

最後にAstroでの設定を行います。
3-1で作成した src/pages/contact.astro を以下のように修正します。

また、以下の箇所については、正しい値に置き換えるよう注意してください。

  • https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key で指定してる reCAPTCHA_site_key には、4-1で取得したサイトキーを入力してください
  • grecaptcha.execute で指定してる reCAPTCHA_site_key には、4-1で取得したサイトキーを入力してください
  • sendRequest メソッドで送信している https://xxxxxx.form.newt.so/v1/xxxxxx は、1-2で確認したエンドポイントの値に置き換えてください
src/pages/cotact.astro
1---
2import Layout from '../layouts/Layout.astro'
3---
4
5<Layout
6  title="Newt・Astroフォーム"
7  description="NewtとAstroを利用した問い合わせフォームです"
8>
9  <div>
10    <h1>Contact us</h1>
11    <form id="form">
12      <label for="name">
13        Name
14        <input id="name" name="name" />
15      </label>
16      <label for="email">
17        Email
18        <input id="email" name="email" type="email" />
19      </label>
20      <label for="message">
21        Message
22        <textarea id="message" name="message"></textarea>
23      </label>
24      <textarea id="message" name="message"></textarea>
25      <button type="submit">Submit</button>
26    </form>
27  </div>
28</Layout>
29
30<script
31  is:inline
32  src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key&hl=ja"
33></script>
34<script>
35  const form = document.getElementById('form')
36  form?.addEventListener('submit', submitFormData)
37
38  function submitFormData(event: Event) {
39    event.preventDefault()
40    grecaptcha.ready(() => {
41      grecaptcha
42        .execute('reCAPTCHA_site_key', { action: 'submit' })
43        .then(async (token) => {
44          const target = event.target as typeof event.target & {
45            name: { value: string }
46            email: { value: string }
47            message: { value: string }
48          }
49
50          const data = {
51            name: target.name.value,
52            email: target.email.value,
53            message: target.message.value,
54            googleReCaptchaToken: token,
55          }
56
57          const formData = new FormData()
58          Object.entries(data).forEach(([key, value]) => {
59            formData.append(key, value)
60          })
61
62          await sendRequest(formData)
63        })
64    })
65  }
66
67  async function sendRequest(formData: FormData) {
68    return await fetch('https://xxxxxx.form.newt.so/v1/xxxxxx', {
69      method: 'POST',
70      body: formData,
71      headers: {
72        Accept: 'application/json',
73      },
74    })
75  }
76</script>

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

  • サイトキーを使用して、reCAPTCHAのJavaScript APIを読み込みます。外部スクリプトであるため、is:inline ディレクティブを含めます
  • reCAPTCHAのJavaScript APIの読み込みでは hl クエリに ja を設定しているため、問い合わせフォームの右下に表示されるreCAPTCHAのバッジが日本語になります。他の言語で設定したい方は、Googleの Language Codes のドキュメントを参考に設定してください
  • googleReCaptchaToken というフィールド名でトークンを送信します(他の名前で送信した場合、正しく動作しないので注意してください)

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

form-tutorial15.jpg

もし、バッジを非表示にしたい場合、.grecaptcha-badge { visibility: hidden; } を適用すると非表示になりますが、この操作はGoogle公式のガイドに従った場合のみ許可されます。
詳細は、Googleの reCAPTCHA バッジを非表示にします。どうすればよいですか? のドキュメントをご確認ください。

5. リダイレクトをカスタマイズする

最後にフォーム送信後のリダイレクトをカスタマイズします。
送信が成功した場合のページと、失敗した場合のページを用意して、送信結果に応じてリダイレクトしてみましょう。

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

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

pages/thanks.astro
1---
2import Layout from '../layouts/Layout.astro'
3---
4
5<Layout title="Thank you" description="問い合わせの送信が成功しました">
6  <div>
7    <h1>Thank you!</h1>
8    <div>
9      <a href="/contact">Back to Previous Page</a>
10    </div>
11  </div>
12</Layout>

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

page/error.astro
1---
2import Layout from '../layouts/Layout.astro'
3---
4
5<Layout title="Error" description="問い合わせの送信が失敗しました">
6  <div class="Result">
7    <h1>Error!</h1>
8    <div>
9      <a href="/contact">Back to Previous Page</a>
10    </div>
11  </div>
12</Layout>

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

page/contact.astro を以下のように修正します。
fetch の リソースとして設定されている https://xxxxxx.form.newt.so/v1/xxxxxx の値については、1-2で確認したエンドポイントの値に置き換えてください。

page/contact.astro
(省略)

  function submitFormData(event: Event) {
    event.preventDefault()
    grecaptcha.ready(() => {
      grecaptcha
        .execute('reCAPTCHA_site_key', { action: 'submit' })
          action: 'submit',
        })
        .then(async (token) => {
          const target = event.target as typeof event.target & {
            name: { value: string }
            email: { value: string }
            message: { value: string }
          }

          const data = {
            name: target.name.value,
            email: target.email.value,
            message: target.message.value,
            googleReCaptchaToken: token,
          }

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

          await sendRequest(formData)
          try {
            const response = await sendRequest(formData)
            if (response.ok) {
              location.href = '/thanks'
            } else {
              location.href = '/error'
            }
          } catch (err) {
            location.href = '/error'
          }
        })
    })
  }
(省略)

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

これでリダイレクトをカスタマイズすることができました。

以上で、すべてのステップが終了となります。

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

NewtMade in Newt