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