【Astro】Contact FormにValidationとreCAPTCHA v3を追加する
初稿:
- 9 min read -
![img of 【Astro】Contact FormにValidationとreCAPTCHA v3を追加する](/_astro/astro-contact-form.Dj4LKLv7_Zcz3r4.webp)
記事概要
- 先日のBloggerからAstroへ移行した記事の別途詳細
※参考 - Blog移行記事
10年以上の期間お世話になったGoogle Bloggerに別れを告げ、この度AstroでBlogサイトを構築し移行した。Astroは静的サイトを手軽に開発できる軽量フレームワーク。無料のテンプレートをベースにカスタマイズを行った。それなりの作業ボリュームとなったので、詳細は別記事に分け、今回は移行作業全体をまとめる。
目的
- Astroで構築したWebサイトにContact Formを設置する
- React用のパッケージ「React Hook Form」でValidationを行う
- AstroはReactのコンポーネントも使える(Astroアイランド)
- reCAPTCHA v3を導入しスパム対策を行う
- reCAPTCHAのサーバサイド検証をNewtのForm Appというサービスで行う
用語説明
Astro とは?
astroは、ブログやマーケティング、eコマースなど、コンテンツ駆動のウェブサイトを作成するためのウェブフレームワークです。astroは、新しいフロントエンドアーキテクチャを開拓し、他のフレームワークと比較してjavascriptのオーバーヘッドと複雑さを低減することで知られています。高速でseoに優れたウェブサイトが必要なら、astroが最適です。 — astro公式docs より引用をDeepLで翻訳
reCAPTCHA とは?
reCAPTCHA は、スパムや不正行為からサイトを保護する無料のサービスです。高度なリスク分析手法を使用して、人間と bot を区別します。 — reCAPTCHA | Google for Developersより引用
React Hook Form とは?
- React用のValidation機能を提供するプラグイン
パフォーマンス、柔軟性、拡張性に優れたフォームと、使いやすいバリデーション。 — React Hook Formより引用 をDeepLで翻訳
Newtとは?
- 2021年創業のスタートアップ企業
- ヘッドレスCMS、フォーム構築サービスを提供している
- 無料のEntryプランのスペックは以下のとおり
- フォーム作成数:無制限
- 月間メッセージ数:30
- 自動返信メール
- CSVダウンロード
- スパム対策
- Form App料金プラン | ヘッドレスCMS「Newt」
コンテンツ管理の新しいスタンダード | ヘッドレスCMS「Newt」
Newtは、APIベースでコンテンツ管理を行うことができるヘッドレスCMSです。最新のウェブ環境に対応したCMSでコンテンツとチームのパフォーマンスを最大化します。
作業環境
- OS - Ubuntu-22.04LTS on WSL2
- Node.js - v20.14.0
- pnpm - v9.4.0
- Astro - v4.11.3
- TailwindCSS - v3.4.4
作業手順
- reCAPTCHA v3を登録
- NewtにForm Appを追加
- react、react-hook-formをインストール
- Reactコンポーネント(Contact Form)の実装
- Contactページを作成
作業詳細
reCAPTCHA v3を登録
- reCAPTCHAのドキュメント
- Googleアカウントが必要
- 以下URLにアクセスし、登録を行う
- 入力する項目は以下のとおり
項目 | 説明 |
---|---|
ラベル | 識別しやすい名前 |
reCAPTCHA タイプ | v3 推奨 |
ドメイン | サイトのドメイン |
Google Cloud Platform | プロジェクトを指定 |
- 注意事項
- ローカルでテストする場合はドメインにlocalhost等も追加しておく
- ここでの情報はGoogle Cloud Platform(以降、GCP)のプロジェクトに保存される
- あらかじめGCPのプロジェクトを用意する、あるいはこの画面で新規作成する
- 送信ボタンをクリックし、表示されたreCAPTCHA のキーを保存しておく
- サイトキー: Astroのcomponentで使用する
- シークレットキー: NewtのForm Appで使用する
- 画面遷移してしまった場合は「歯車アイコン → reCAPTCHAのキー」で確認できる
以上でreCAPTCHAの登録作業は完了。
NewtにForm Appを追加
- Newt Form Appのドキュメント
Form App 登録
- 次のURLにアクセスし、アカウントを登録する
- 次のドキュメントの「Step 1」に従いForm Appとフォームを追加し、エンドポイントのURLを保存しておく
- サイドバーの先ほど登録したForm Appの右端「…」をクリックし、App設定を開く
- サイドバーのフォームをクリックし、スパム対策の有効にする、reCAPTCHAシークレットキーを入力
- 画面右上の「保存」をクリック
受信通知メールの設定
- 問い合わせを受信する設定を行う
- 同じ画面の「メール設定」を開き以下を設定する
- 受信通知メールを有効にする: をオンにする
- 通知を受け取るメールアドレス: 受信したいメールアドレスを入力
- 件名: メールの件名
- 本文: 下記コードブロックを参照
- {submission.xxxxxx} でフォームから送信された値を使用できる
- 画面右上の「保存」をクリックする
以下のお問い合わせを受信しました。
■お名前
{submission.name}
■メールアドレス
{submission.email}
■お問い合わせ内容
{submission.message}
以上でNewtにForm Appを追加する作業は完了。
react、react-hook-formをインストール
react インストール
- 以下コマンドでパッケージをインストールする
pnpm astro add react
- 複数の関連パッケージの追加を聞かれるのでyes
✔ Resolving packages...
23:00:01
Astro will run the following command:
If you skip this step, you can always run it yourself later
╭───────────────────────────────────────────────────────╮
│ pnpm add @astrojs/react@^3.6.0 @types/react@^18.3.3 @types/react-dom@^18.3.0 react@^18.3.1 react-dom@^18.3.1 │
╰────────────────────────────────────────────────────────╯
✔ Continue?
- astro.config.mjsの自動変更を聞かれるのでyes
Astro will make the following changes to your config file:
╭ astro.config.mjs ───────────────────────╮
│ import { defineConfig } from 'astro/config'; │
│ import mdx from '@astrojs/mdx'; │
│ import sitemap from '@astrojs/sitemap'; │
│ import tailwind from '@astrojs/tailwind'; │
│ import { never } from 'astro/zod'; │
│ │
│ import react from "@astrojs/react"; │
│ │
? Continue?
- tsconfig.jsonの自動更新を聞かれるのでyes
Astro will make the following changes to your tsconfig.json:
╭ tsconfig.json ───────────────────────╮
│ { │
│ "extends": "astro/tsconfigs/strict", │
│ "compilerOptions": { │
│ "jsx": "react-jsx", │
│ "jsxImportSource": "react" │
? Continue?
- 完了
success Successfully updated TypeScript settings
react-hook-form インストール
- 以下コマンドでreact-hook-formをインストール
pnpm add react-hook-form
- 完了
Packages: +1
+
Progress: resolved 1613, reused 1538, downloaded 1, added 1, done
dependencies:
+ react-hook-form 7.52.0
Done in 4.1s
Reactコンポーネント(Contact Form)の実装
- 仕様は以下のとおり
- reCAPTCHのサイトキーと、Newt Form Appのエンドポイントをpropsで受ける
- react-hook-formのValidationを埋め込んだフォームをレンダリングする
- ユーザが送信を実行すると、フォームデータとreCAPTCHAのtokenをNewt Form AppにPOSTする
- Newt側でreCAPTCHAサーバと認証処理を行う
- レスポンスがOkなら/thanks/ページ、エラーなら/error/ページへ遷移する
import { useForm } from 'react-hook-form'
type FormValues = {
name: string
email: string
message: string
}
interface FormProps {
formUrl: string
siteKey: string
}
export default function ContactForm(props: FormProps) {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormValues>({ mode: 'onChange' })
const onSubmit = handleSubmit(async (data) => {
grecaptcha.ready(() => {
grecaptcha.execute(props.siteKey, { action: 'submit' }).then(async (token) => {
const formData = new FormData()
Object.entries(data).forEach(([key, value]) => {
formData.append(key, value)
})
formData.append('googleReCaptchaToken', token)
try {
const response = await fetch(props.formUrl, {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json'
}
})
if (response.ok) {
location.href = '/thanks/'
} else {
location.href = '/error/'
}
} catch (err) {
location.href = '/error/'
}
})
})
})
return (
<form onSubmit={onSubmit}>
<div className='mb-6'>
<label htmlFor='name' className='block text-sm font-medium mb-2'>
Name
<span className='text-red-800 mx-1 text-sm'>*</span>
{errors?.name && (
<span id='error-name-required' aria-live='assertive' className='ml-4 text-red-700'>
{errors.name.message}
</span>
)}
</label>
<input
id='name'
className='p-4 block w-full text-md rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-slate-900 '
{...register('name', { required: '※Name は必須項目です。' })}
aria-describedby='error-name-required'
placeholder='名前'
/>
</div>
<div className='mb-6'>
<label htmlFor='email' className='block text-sm font-medium mb-2'>
Email
<span className='text-red-800 mx-1 text-sm'>*</span>
{errors?.email && (
<span id='error-email-required' aria-live='assertive' className='ml-4 text-red-700'>
{errors.email.message}
</span>
)}
</label>
<input
id='email'
className='p-4 md:p-4 block w-full text-md rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-slate-900'
{...register('email', { required: '※Emailは必須項目です。' })}
placeholder='[email protected]'
/>
</div>
<div className='mb-8'>
<label htmlFor='message' className='block text-sm font-medium mb-2'>
Message
<span className='text-red-800 mx-1 text-sm'>*</span>
{errors?.message && (
<span id='error-message-required' aria-live='assertive' className='ml-4 text-red-700'>
{errors.message.message}
</span>
)}
</label>
<textarea
id='message'
rows={5}
className='p-4 md:p-4 block w-full text-md rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-slate-900'
{...register('message', { required: '※Message は必須項目です。' })}
placeholder='メッセージ'
></textarea>
</div>
<div className='flex justify-center items-center mb-4'>
<button type='submit' className='primary-btn mx-auto w-full'>
送信
</button>
</div>
</form>
)
}
Contactページを作成
- siteKeyはreCAPTCHA作成後に表示されたサイトキーを使用
- formUrlはNewtのForm App作成後に表示されたエンドポイントを使用
---
import BaseLayout from '@/layouts/BaseLayout'
import ContactForm from 'src/components/ui/ContactForm'
const title = 'お問い合わせ'
const description = 'このブログに関する問い合わせフォームのページです。'
const siteKey: string = import.meta.env.VITE_NEWT_KEY
const id: string = import.meta.env.VITE_NEWT_ID
const formUrl = `https://neputa-note.form.newt.so/v1/${id}`
---
<BaseLayout title={title} description={description}>
<div class='mx-auto w-full max-w-3xl'>
<div
class='mx-auto flex flex-col rounded-lg border border-gray-200 bg-gray-100 p-4 shadow backdrop-blur dark:border-gray-700 dark:bg-slate-900 sm:p-6 lg:p-8'
>
<ContactForm siteKey={siteKey} formUrl={formUrl} client:load />
</div>
</div>
</BaseLayout>
<script is:inline src=`https://www.google.com/recaptcha/api.js?render=${siteKey}&hl=ja`></script>
以上で実装は完了。