Hugo に Google Form ベースの問い合わせを作った話
Posted on Oct 8, 2019
今年初めに Hugo 使ってとあるウェブサイトつくったんですが、問い合わせページは手抜きで Google Form をそのまま埋め込んで使ってました。ですが、(1)見栄えがよろしくない、(2)Google Analytics のタグが埋め込めない、っていう理由で、HTML ベースに切り替えることに。
ホスティングに Netlify とか使ってれば、簡単なタグを埋め込むだけで Netlify がよしなにやってくれるんですが、今回のウェブサイトは別のホスティングサービス使ってることもあって、別のオプションを試すことに。
POST で直接たたく
ググると一杯事例がでてきますが、Google Form を問い合わせページの「バックエンド」に使う方法としては、Google Form の各項目に埋め込まれたタグを key として設定し、 value に中身を埋め込んで POST すれば、Google Form の UI を使わなくてもデータを投稿できます。
この方法の問題は、POST 成功後に Google Form のページにリダイレクトされてしまうことです。この問題の対策として、「Javascript を使って投稿し、リダイレクトを無効化する」という方法が紹介されていますが、この方法が私は気に入らず。理由は、(1)required タグを埋め込んでも機能しない、(2)ブラウザがやってくれる? email アドレスの sanitization なんかも機能しない、(3)投稿に失敗することを想定していない、(4)Javascript を使わないといけない、(5)reCaptcha とかが使えない、などなど。
Google Cloud Functions
そこで結局、問い合わせページと Google Form の間に Google Cloud Functions を置くことに。問い合わせページから Functions に送り、そっから Google Form に投稿し、帰ってくる結果(200 or 404)に基づいて、成功ないしは失敗ページにリダイレクトさせる、って感じの仕組みにしました。
コードはこんな感じ。
package p
import (
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strings"
)
var googleFormURL = os.Getenv("GOOGLE_FORM_URL")
var successURL = os.Getenv("SUCCESS_URL")
var failURL = os.Getenv("FAIL_URL")
type inquiry struct {
name string
email string
message string
}
func (i *inquiry) sendToGoogleForm() (*http.Response, error) {
// Set up body parameters. Google Form does not accept POST with JSON.
f := url.Values{}
f.Add("entry.XXXXXXXX", i.name)
f.Add("entry.XXXXXXXX", i.email)
f.Add("entry.XXXXXXXX", i.message)
req, err := http.NewRequest("POST", googleFormURL, strings.NewReader(f.Encode()))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
// Send the request and get a response from Slack API.
httpClient := &http.Client{}
return httpClient.Do(req)
}
func newInquiry(r *http.Request) (*inquiry, error) {
// Return error when the request method is not POST.
if r.Method != http.MethodPost {
return nil, errors.New("Invalid Request Method")
}
// Return error when the media type is invalid.
if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
return nil, errors.New("Invalid Content Type")
}
return &inquiry{
name: r.FormValue("name"),
email: r.FormValue("email"),
message: r.FormValue("message"),
}, nil
}
// ContactForm is a function to send a request to Google Form and redirect based on the response.
func ContactForm(w http.ResponseWriter, r *http.Request) {
// Extract parameters from the request.
inq, err := newInquiry(r)
if err != nil {
log.Print("Failed to parse the request from web", err)
http.Redirect(w, r, failURL, http.StatusFound)
return
}
// Send the request and get a response from Google Form.
res, err := inq.sendToGoogleForm()
defer res.Body.Close()
// Redirect to error page when failed.
if err != nil {
log.Print("Could not send a request to Google Form: ", err)
http.Redirect(w, r, failURL, http.StatusFound)
return
}
if res.StatusCode != 200 {
b, _ := ioutil.ReadAll(res.Body)
log.Print("Bad request to Google Form: ", string(b))
http.Redirect(w, r, failURL, http.StatusFound)
return
}
// Redirect to a page that tells it is done.
http.Redirect(w, r, successURL, http.StatusFound)
}
ま、そもそも Cloud Functions をバックエンドにつかうなら、Google Form なんて使わなくてもいいんですが、Google Form から Slack に通知を送る機能を実装してましたし、Google Form を簡易データベース的に使えますし。デプロイしてから1ヶ月くらい経ってますが、今のところ順調に動いてます。
ということで、久々にコーディングの話。
comments powered by Disqus