BloggerにGoogleフォーム×reCAPTCHAでお問い合わせフォームを作成する

Bloggerで連絡フォームウィジェットがエラーで送信できなかったのでGoogleフォームを使ってカスタムフォームを実装したはなし。

[お問い合わせページ](https://blog.bigbridge.work/p/contact.html)

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/39da1156-6221-b3ad-6a60-ce31707abb2a.png)

## 連絡フォームウィジェットのエラー原因

フォーム送信先のエンドポイント`https://www.blogger.com/contact-form.do`がオリジン`https://blog.bigbridge.work`対して、`Access-Control-Allow-Origin`許可してくれないためCORSエラーが発生していた。

![image 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/8beb7047-44b3-d429-556b-cd74d6e0b6aa.png)

![image 4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/951e7cf4-2c08-55e5-3407-143e0d8280df.png)


### リクエスト

```plaintext
# 概要
リクエスト URL:https://www.blogger.com/contact-form.do
リクエスト メソッド:POST
ステータス コード:200 OK
参照ポリシー:strict-origin-when-cross-origin

# リクエストヘッダ
:authority:www.blogger.com
:method:POST
:path:/contact-form.do
:scheme:https
Accept:*/*
Accept-Encoding:gzip, deflate, br, zstd
Accept-Language:ja,en-US;q=0.9,en;q=0.8
Content-Length:170
Content-Type:application/x-www-form-urlencoded;charset=UTF-8
Origin:https://blog.bigbridge.work
Referer:https://blog.bigbridge.work/
Sec-Ch-Ua:"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile:?0
Sec-Ch-Ua-Platform:"macOS"
Sec-Fetch-Dest:empty
Sec-Fetch-Mode:cors
Sec-Fetch-Site:cross-site
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36

# ペイロード部
## フォーム
name: テスト
email: test@bigbridge.work
message: テスト
## トークン
blogID: ******************************
token: AOuZoY6iKedo_5ezKrVuDX5Zx7eK_7l4xg:1711659541683
```

### レスポンス

`Access-Control-Allow-Origin`ヘッダがない

```plaintext
# レスポンスヘッダ
Alt-Svc:h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Content-Encoding:gzip
Content-Length:70
Content-Security-Policy:script-src 'self' *.google.com *.google-analytics.com 'unsafe-inline' 'unsafe-eval' *.gstatic.com *.googlesyndication.com *.blogger.com *.googleapis.com uds.googleusercontent.com https://s.ytimg.com https://i18n-cloud.appspot.com https://www.youtube.com www-onepick-opensocial.googleusercontent.com www-bloggervideo-opensocial.googleusercontent.com www-blogger-opensocial.googleusercontent.com https://www.blogblog.com; report-uri /cspreport
Content-Type:text/javascript; charset=UTF-8
Date:Thu, 28 Mar 2024 20:59:49 GMT
Expires:Mon, 01 Jan 1990 00:00:00 GMT
P3p:CP="This is not a P3P policy! See https://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info."
Pragma:no-cache
Server:GSE
X-Content-Type-Options:nosniff
X-Frame-Options:SAMEORIGIN
X-Xss-Protection:1; mode=block
```

Bloggerフォーラムに送信トークンについて仕様変更を示唆するコメントがあった。[Contact us Stopped Working - Blogger Community](https://support.google.com/blogger/thread/199152329/contact-us-stopped-working?hl=en)

一応リクエストのペイロードにはトークンらしきものは添加されているが、そもそも使用テーマ[Write Simple](https://gooyaabitemplates.com/write-simple-blogger-template/)の公開時期が2016年とかなり古いため現在の要件を満たしていないのかもしれない。使用テーマは変えたくなかったのでGoogleフォームを利用してフォームを実装する。

## お問い合わせフォームの要件

お問い合わせに求める要件は次の通り

- お問い合わせを受信出来て、履歴を一元的に管理できること
- フォームをスタリングできること
- reCAPTCHAでボット対策できること
- 入力内容に対してバリデーションできること
- 送信完了後は完了画面にフォワードできること

### お問い合わせを受信出来て、履歴を一元的に管理できること
[Google formコンソール](https://docs.google.com/forms/d/1whXJ_h5K0Siru8DRxMUgusoKpYylYRzsxx4r17KQDnU/edit)
[お問い合わせのスプレッドシート](https://docs.google.com/spreadsheets/d/1bK1lheSwqfvxpASODQDGMv2muoAi2Zj24czf_uVVSjY/edit#gid=1643548005)

お問い合わせはGoogleフォームで受信し、管理する。すべての受信履歴はスプレッドシートにスタックされる。

![](image%208.png)![image 8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/230eaf38-bf98-6b9f-5481-458d04ae6187.png)

回答タブでメール通知はONにしておく

![image 9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/40671754-9ad0-a580-575a-9270e9adaeb6.png)

### フォームをスタリングできること

Googleフォームをサイトにインラインフレームで組み込むと、入力から完了までGoogleフォームまるだしだ。これはいかん。是が非でもサイトのスタイリングに統一したい。

![image 6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/a6d7aed0-9ce0-4498-5e4f-5e4441db3e77.png)

![image 7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/f3ab0d90-5e0d-3bd7-3432-431eebc1e305.png)

Googleフォームのaction属性とフォームパーツのname属性を拝借して、自前で組んだフォームにそれぞれ充てる。

![image 10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/417736/53d77ccc-4443-1d2d-ff5d-9e8342421c72.png)

こんな感じで、GoogleフォームにPOSTが出来る。
(が、送信後Googleフォームの送信完了画面にリダイレクトするため、自前の完了画面に遷移する実装を後述)

```html:html
<form action="https://docs.google.com/forms/u/0/d/e/1FAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/formResponse" method="POST">
    <div>
        <div><span>お名前</span></div>
        <div><input type="text" name="entry.2XXXXXXXX"></div>
    </div>
    <div>
        <div><span>メールアドレス</span></div>
        <div><input type="email" name="entry.3XXXXXXXX"></div>
    </div>
    <div>
        <div><span>お問い合わせ内容</span></div>
        <div><textarea name="entry.1XXXXXXXX" rows="10"></textarea></div>
    </div>
    <div>
		<input type="submit" value="送信">
    </div>
</form>
```

### reCAPTCHAでボット対策できること
[reCAPTCHAコンソール](https://www.google.com/recaptcha/admin/site/698953800)

 reCAPTCHA-V2を設置して、reCAPTCHA検証イベントで送信ボタンを活性状態に切り替える。
フォームの送信タイミングでバリデーションなど追加実装を割り込ませるためにsubmitボタンをbuttonに変更して初期状態は`disabled`。

```html:html
<script src="https://www.google.com/recaptcha/api.js"></script>
<form action="https://docs.google.com/forms/u/0/d/e/1FAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/formResponse" method="POST">
	...
	<!-- それらしく送信ボタンの前にreCAPTCHAを組み込む -->
	<div class="g-recaptcha" data-callback="gValidatedCallback" data-sitekey="6XXXXXXXXXXXXXXX_FXXXXXXXXXXXXXXXXXXXXXX" disabled></div>
	<!-- 送信ボタン 初期状態は非活性-->
    <div>
		<button id="id-contact-form-submit" class="submit" disabled>送信</button>
    </div>
</form>
```

reCAPTCHA検証イベントで送信ボタンを活性状態にスイッチする。`disabled`フラグだけではhtml改ざんが出来るのでJSで追加検証のフラグも実装する。

```javascript:javascript
/************************************************
 * reCAPTCHA検証のコールバック
 * コードがあれば=判定通過で、ボタンのdisableを解除して通過フラグを立てる
 ************************************************/
let gValidated = false;
function gValidatedCallback(code) {
    if(code !== ''){
  		$('#id-contact-form-submit').removeAttr("disabled");
		gValidated = true;
	}
}

/************************************************
 * 送信ボタンのコールバック
 * - reCAPTCHA検証フラグを追加検証する
 ************************************************/
$(document).on('click', '#id-contact-form-submit', function(){  
	try{
        if( !gValidated ) throw new Error('reCAPTCHA検証を完了してください。');
	}catch(error){
    	alert(error.message);
	}
	return false;
});
```

### 送信完了後は完了画面にフォワードできること

GoogleフォームのactionにPOSTするとGoogleフォーム完了画面にリダイレクトしてしまうので、formのtarget属性にダミーの非表示インラインフレームを指定してGoogleフォームのレスポンスを葬りつつ、ロケーションを送信完了ページに移動する。

```html:html
<form id="id-contact-form" action="https://docs.google.com/forms/u/0/d/e/1FAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/formResponse" method="post" target="hidden_iframe">
	...
</form>
<iframe name="hidden_iframe" style="display:none;"></iframe>
```

```javascript:javascript
...
/************************************************
* 送信ボタンのコールバック
* - reCAPTCHA検証フラグを追加検証する
* - Googleフォームに送信する
* - ロケーションを完了画面に移動する
 ************************************************/
$(document).on('click', '#id-contact-form-submit', function(e){
	try{
		// バリデーション
        if( !gValidated ) throw new Error('reCAPTCHA検証を完了してください。');

		// フォーム送信 => レスポンスは"target="hidden_iframe"に葬られる
		$('#id-contact-form').submit();
		// ロケーションを完了画面に移動する
		window.location='/p/been-contact.html';      
	}catch(error){
    	alert(error.message);
	}
	return false;
});
```

### 入力内容に対してバリデーションできること

Googleフォームの送信結果はインラインフレームに葬るため、送信結果を受け取ることが出来ないのでフロントでフォーム入力値のバリデーションを実装する。
※なおajaxによるフォーム送信ではCORSエラーである

```javascript:javascript
/************************************************
 * メールアドレスのフォーマット検証
 * @return bool true: 合格 / false: 不合格
 ************************************************/
function isValidEmail(email) {
  const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return emailRegex.test(email);
}

/************************************************
 * 送信ボタンのコールバック
 * - reCAPTCHA検証フラグを追加検証する
 * - フォーム入力値を検証する
 * - Googleフォームに送信する
 * - ロケーションを完了画面に移動する
 ************************************************/
$(document).on('click', '#id-contact-form-submit', function(e){
    
	try{
		// バリデーション
        if( !gValidated ) throw new Error('reCAPTCHA検証を完了してください。');

		// 必須フォームフィールド
		const nm_name	= 'entry.2XXXXXXXX';
		const nm_email	= 'entry.3XXXXXXXX';
		const nm_body	= 'entry.1XXXXXXXX';

		if( !$(`input[name="${nm_name}"]`).val() ) throw new Error('お名前を入力してください');
		if( !$(`input[name="${nm_email}"]`).val() ) throw new Error('メールアドレスを入力してください');
		if( !isValidEmail( $(`input[name="${nm_email}"]`).val() ) ) throw new Error('メールアドレスのフォーマットが不正です');
		if( !$(`textarea[name="${nm_body}"]`).val() ) throw new Error('お問い合わせ内容を入力してください');
            
		// フォーム送信
		$('#id-contact-form').submit();
		// ロケーション移動
		window.location='/p/been-contact.html';
      
	}catch(error){
    	alert(error.message);
	}
	return false;
});
```

### 結果

要件を満たした最終的なお問い合わせページの実装

```html:html
<script src="https://www.google.com/recaptcha/api.js"></script>
<script>
let gValidated = false;
/************************************************
 * reCAPTCHA検証のコールバック
 * コードがあれば=判定通過で、ボタンのdisableを解除して通過フラグを立てる
 ************************************************/
function gValidatedCallback(code) {
    if(code !== ''){
  		$('#id-contact-form-submit').removeAttr("disabled");
		gValidated = true;
	}
}

/************************************************
 * メールアドレスのフォーマット検証
 * @return bool true: 合格 / false: 不合格
 ************************************************/
function isValidEmail(email) {
  const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return emailRegex.test(email);
}

/************************************************
 * 送信ボタンのコールバック
 * - reCAPTCHA検証フラグを追加検証する
 * - フォーム入力値を検証する
 * - Googleフォームに送信する
 * - ロケーションを完了画面に移動する
 ************************************************/
$(document).on('click', '#id-contact-form-submit', function(e){
	try{      
        if( !gValidated ) throw new Error('reCAPTCHA検証を完了してください。');

		const nm_name = 'entry.2XXXXXXXX';
		const nm_email = 'entry.3XXXXXXXX';
		const nm_body = 'entry.1XXXXXXXX';
		if( !$(`input[name="${nm_name}"]`).val() ) throw new Error('お名前を入力してください');
		if( !$(`input[name="${nm_email}"]`).val() ) throw new Error('メールアドレスを入力してください');
		if( !isValidEmail( $(`input[name="${nm_email}"]`).val() ) ) throw new Error('メールアドレスのフォーマットが不正です');
		if( !$(`textarea[name="${nm_body}"]`).val() ) throw new Error('お問い合わせ内容を入力してください');
      
		$('#id-contact-form').submit();
		window.location='/p/been-contact.html';
	}catch(error){
    	alert(error.message);
	}
	return false;
});
</script>

<style>
  
  .formgroup{}
  .formgroup:not(last-child){
    margin-bottom: 2em;
  }
  .formgroup__label{
    color: #000;
  }
  .formgroup__form > * {
    	width: 100%;
  }
  .formgroup .require{
  		font-size: 0.8em;
    	color: #f00;
  }
  .formgroup .require:before{
    	margin-left: 0.8em;
    	content: '*';
  }
  
  button {
  		text-shadow: none;
  }
    
  .submit{
    	color: #fff;
  		background-color: #000;
  }
  .submit:hover{
    	color: #000;
  		background-color: #fff;
  }
  .submit:disabled{
    	color: #000;
  		background-color: #ccc;
  }
  
  .g-recaptcha{
		margin: 1em 0;
  }

</style>

<p>フォームにお問い合わせ内容を入力して送信してください。</p>

<form id="id-contact-form" action="https://docs.google.com/forms/u/0/d/e/1FAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/formResponse" method="post" target="hidden_iframe">
	<div>
    	<div class="formgroup">
        	<div class="formgroup__label">
          		<span>お名前</span>
          	</div>
        	<div class="formgroup__form">
            	<input type="text" name="entry.2XXXXXXXX" >
          	</div>
    	</div>
    	<div class="formgroup">
        	<div class="formgroup__label">
              	<span>メールアドレス</span>
          	</div>
        	<div class="formgroup__form">
              	<input type="email" name="entry.3XXXXXXXX" >
        	</div>
    	</div>
    	<div class="formgroup">
        	<div class="formgroup__label">
              	<span>お問い合わせ内容</span>
          	</div>
        	<div class="formgroup__form">
              	<textarea name="entry.1XXXXXXXX" rows="10"></textarea>
          	</div>
    	</div>
  	</div>
	<div class="g-recaptcha" data-callback="gValidatedCallback" data-sitekey="6XXXXXXXXXXXXXXX_FXXXXXXXXXXXXXXXXXXXXXX" disabled></div>
	<button id="id-contact-form-submit" class="submit" disabled>送信</button>
</form>
<iframe name="hidden_iframe" style="display:none;"></iframe>
```