Bloggerで連絡フォームウィジェットがエラーで送信できなかったのでGoogleフォームを使ってカスタムフォームを実装したはなし。
連絡フォームウィジェットのエラー原因
フォーム送信先のエンドポイントhttps://www.blogger.com/contact-form.do
がオリジンhttps://blog.bigbridge.work
対して、Access-Control-Allow-Origin
許可してくれないためCORSエラーが発生していた。
リクエスト詳細
# 概要
リクエスト 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
ヘッダがない
# レスポンスヘッダ
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
一応リクエストのペイロードにはトークンらしきものは添加されているが、そもそも使用テーマWrite Simpleの公開時期が2016年とかなり古いため現在の要件を満たしていないのかもしれない。使用テーマは変えたくなかったのでGoogleフォームを利用してフォームを実装する。
お問い合わせフォームの要件
お問い合わせに求める要件は次の通り
- お問い合わせを受信出来て、履歴を一元的に管理できること
- フォームをスタリングできること
- reCAPTCHAでボット対策できること
- 入力内容に対してバリデーションできること
- 送信完了後は完了画面にフォワードできること
お問い合わせを受信出来て、履歴を一元的に管理できること
Google formコンソール
お問い合わせのスプレッドシート
お問い合わせはGoogleフォームで受信し、管理する。すべての受信履歴はスプレッドシートにスタックされる。
回答タブでメール通知はONにしておく
フォームをスタリングできること
Googleフォームをサイトにインラインフレームで組み込むと、入力から完了までGoogleフォームまるだしだ。これはいかん。是が非でもサイトのスタイリングに統一したい。
Googleフォームのaction属性とフォームパーツのname属性を拝借して、自前で組んだフォームにそれぞれ充てる。
こんな感じで、GoogleフォームにPOSTが出来る。
(が、送信後Googleフォームの送信完了画面にリダイレクトするため、自前の完了画面に遷移する実装を後述)
<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-V2を設置して、reCAPTCHA検証イベントで送信ボタンを活性状態に切り替える。
フォームの送信タイミングでバリデーションなど追加実装を割り込ませるためにsubmitボタンをbuttonに変更して初期状態はdisabled
。
<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で追加検証のフラグも実装する。
/************************************************
* 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フォームのレスポンスを葬りつつ、ロケーションを送信完了ページに移動する。
<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>
...
/************************************************
* 送信ボタンのコールバック
* - 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エラーである
/************************************************
* メールアドレスのフォーマット検証
* @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 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>
0 Comments:
コメントを投稿