Nuxt.js + Netlifyで問い合わせフォームを作る方法【自動返信あり】

Nuxt.js + Netlifyで問い合わせフォームを作る方法【自動返信あり】

Nuxt.js + Netlify

この組み合わせはここ数年のトレンドですかね。直近で言うと、東京都が作った新型コロナウィルス対策サイトがこの構成で作成され公開されたと話題になりました。

東京都 新型コロナウイルス感染症対策サイト

https://stopcovid19.metro.tokyo.lg.jp/

このサイトはソースコードもGitHubで公開されており、大変参考になります。

tokyo-metropolitan-gov / covid19

https://github.com/tokyo-metropolitan-gov/covid19

今回はそんなNuxt.js + Netlifyでお問い合わせフォームを作ってみます。

Nuxt.jsって?

Nuxt(ナクスト)はVue.jsをベースにしたJavaScriptフレームワークです。
そもそもVue.jsもフレームワークなのですが、そのまま扱うには自分でWebpackを設定したりルーターを設定したりけっこう手間がかかります。それに比べてNuxtはWebpackの設定もルーターの設定も含め、Webアプリケーションを作るために必要なVue.jsの設定が最初からほとんど組み込まれています。そして1番の違いは、SSR(サーバーサイドレンダリング)がごく簡単に使えることでしょうか。SSRは自前で実装するにはかなりハードルが高い印象ですが、Nuxtであればnodeが動く環境であればすぐ使えます。また、それでもSSRはハードルが高いなっていう場合でも、静的サイトをコマンド一つで簡単に生成できる機能も備わっています。

扱いやすくてすぐに使えるので、ここ数年フロントエンド界隈では人気の高いフレームワークとなっているようです。

Netlifyって?

静的サイト向けの高機能ホスティングサービスです。
これによりいわゆる「サーバーレス開発」が可能になります。
Gitと連携して自動デプロイ、無料SSLを利用可、独自ドメインを当てることも可能です。なんと言っても1番良いところは、Freeプランがあるのでほぼ無料

  • 月100Gまでの通信量無料
  • 月300分までのビルドが無料
  • 独自ドメインの設定が無料
  • SSL無料
  • CDN無料

無料枠が大きいため、かなり大規模なサイトなど特別な事情がない限りはほとんどのサイトが無料で使えると思います。

サーバーがないとメールフォームとかどうすんの?とか思いますが、今回実装するNetlify Formsという機能でメールフォームを機能させることができますのでご安心を。ただしFreeプランだと上限があり100通/月までなので頻繁にお問い合わせがあるサイトは有料に切り替えるか別の方法をご検討ください。

またNetlify Functionsという機能でAWSアカウントなしでLambdaを利用できます。どうしてもサーバー側に機能を追加したい場合はこちらを使うと良いかと思います。(125,000リクエスト/月、実行時間100時間/月)
今回はこちらを使ってNetlify Formsに付いていないお問い合わせフォームの自動返信機能を作ってみたいと思います。

お問い合わせフォームをつくる

今回はNuxtで作ったサイトをNetlifyで静的にビルドして配信します。その中でNetlify Formsでフォームを、Netlify Functionsでフォームの自動返信メールを作る方法をメモ。

毎回デモサイトを作っているのですが、今回はお問い合わせフォームという特性上デモの公開は控えさせていただきます。閲覧数の少ないブログではありますが、自動返信のテストを頻繁にされてもちょっと困るので・・・笑

デモを公開しないため、CSS・デザイン面についてもここで書きません。好みのフレームワークを使ったりして自身で調整してください。

また、長くなるのでNetlifyの登録やGitHubとの連携などについても省略させていただきますのでご了承ください。Nuxt + Netlifyでサイトを公開するまでの手順については下記参考サイトを貼っておきます。

Nuxt.js + Netlifyで爆速構築するサーバーレス開発入門

https://qiita.com/isihigameKoudai/items/e3b136e9964f1d30d73d

Nuxt + Contentful + Netlifyでブログ作成全手順

https://izm51.com/posts/nuxt-contentful-netlify-blog-making-1/

Nuxtでフォームを書く

Netlify Formsを使ってフォームを書くのは非常に簡単。
form要素にnetlify、もしくはdata-netlify="true"という属性と追加するだけです。

<form name="contact" method="POST" netlify>
  <!-- フォームの内容 -->
</form>

または

<form name="contact" method="POST" data-netlify="true">
  <!-- フォームの内容 -->
</form>

どちらでも好きな方でOKです。

またNuxtでNetlify Forms機能を使う場合はhiddenフィールドを埋め込む必要があります。

<form name="contact" method="POST" data-netlify="true">
  <!-- ↓これを追加 -->
  <input type="hidden" name="form-name" value="contact" /> 

  <!-- フォームの内容 -->
</form>

普通のhtmlだったらNetlify側が勝手にこのフィールドを埋め込んでくれるみたいなのですが、Nuxtの場合は外からDOM追加できないのでエラーになります。そのため手動で先に埋め込んでおきます。参考は下記です。

楽勝すぎるはずの netlify form を nuxt generate で頑張って使う

https://qiita.com/yahsan2/items/a70c4c8f617ee9b1f9ff

これを踏まえてpages/contact/index.vueにフォームを作ることにします。
まずはテンプレート部分を書いていきます。

<template>
  <div class="p-contact">
    <form class="p-contact__form" name="contact" method="POST" data-netlify="true" data-netlify-honeypot="bot-field" @submit.prevent="onSubmit" :class="sendingClass">
      <input type="hidden" name="form-name" value="contact">

      <div class="p-contact__item">
        <label for="username">お名前</label>
        <input type="text" id="username" name="username" v-model="username" autocomplete="name">
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__item">
        <label for="katakana">フリガナ</label>
        <input type="text" id="katakana" name="katakana" v-model="katakana">
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__item">
        <label for="useremail">メールアドレス</label>
        <input type="text" id="useremail" name="useremail" v-model="useremail" autocomplete="email">
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__item">
        <label for="message">お問い合わせ内容</label>
        <textarea id="message" name="message" v-model="message"></textarea>
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__item" v-show="false">
        <label for="message">スパムでない場合は空欄</label>
        <input type="text" name="bot-field" v-model="botField"/>
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__submit">
        <button type="submit">送信</button>
      </div>
      <!-- /.p-contact__submit -->
    </form>
    <!-- /.p-contact__form -->
  </div>
</template>

内容は「名前」「フリガナ」「メールアドレス」「お問い合わせ内容」の4項目。フォームはAjaxで送信したいのでformのsubmitイベントを@submit.preventで無効化してonSubmit関数を指定しています。Ajax処理については後ほど。

また、スパム対策にNetlify Formsの機能でハニーポットを設定してみました。

スパム対策にハニーポットを設定

form要素にdata-netlify-honeypot="bot-field"をつけてハニーポット用のフィールドを作ります。ハニーポット用のフィールドに入力があった場合にスパムと判定されフォームが送信されません。このフィールドは人間の場合は見えなくでもいいのでv-show=falseで非表示にしておきます。

<div class="p-contact__item" v-show="false">
  <label for="message">スパムでない場合は空欄</label>
  <input type="text" name="bot-field" v-model="botField"/>
</div>

バリデーションは「VeeValidate」

フォームなのでバリデーションをつけたいですよね。特にNetlifyの場合はサーバーレスでバリデートを裏側で処理できないためほぼ必須です。
いろいろと調べると僕はVeeValidateが使いやすそうで気に入ったので使ってみることにしました。

まずYarnでインストール

yarn add -D vee-validate

プラグインとしてVeeValidateを読み込みます。
plugins/vee-validate.jsを作成して以下のように書きます。

import Vue from 'vue'
import {
  extend,
  ValidationProvider,
  ValidationObserver,
  Validator,
  localize
} from 'vee-validate'
import ja from 'vee-validate/dist/locale/ja.json'
import { required, email, max } from 'vee-validate/dist/rules'

//日本語
localize('ja', ja)

//必須
extend('required', required)
//メールアドレス
extend('email', email)
//文字数上限
extend('max', max)
//カタカナのみ
extend('katakana', {
  message: (field) => { return field + "は全角カタカナのみ使用できます" },
  validate: (value) => { return /^[ァ-ン]+$/.test(value) }
})

Vue.component('validation-provider', ValidationProvider)
Vue.component('validation-observer', ValidationObserver)

こんな感じでextendでバリデーションの条件を追加していきます。
カタカナの判定はデフォルトになかったのでメッセージと判定条件を追加しました。

このプラグインをnuxt.config.jsで読み込みます。

plugins: [
  '~/plugins/vee-validate' //これを追記
],

あとはソースコードをVeeValidateの書き方に変更。

<template>
  <div class="p-contact">
    <validation-observer ref="observer" v-slot="{ invalid, validated }" tag="form" class="p-contact__form" name="contact" method="POST" data-netlify="true" data-netlify-honeypot="bot-field" @submit.prevent="onSubmit" :class="sendingClass">
      <input type="hidden" name="form-name" value="contact">

      <div class="p-contact__item">
        <label for="username">お名前</label>
        <validation-provider v-slot="{ errors }" rules="required|max:100" name="お名前">
          <input type="text" id="username" name="username" v-model="username" autocomplete="name">
          <p v-show="errors.length" class="p-contact__error">{{ errors[0] }}</p>
        </validation-provider>
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__item">
        <label for="katakana">フリガナ</label>
        <validation-provider v-slot="{ errors }" rules="required|katakana" name="フリガナ">
          <input type="text" id="katakana" name="katakana" v-model="katakana">
          <p v-show="errors.length" class="p-contact__error">{{ errors[0] }}</p>
        </validation-provider>
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__item">
        <label for="useremail">メールアドレス</label>
        <validation-provider v-slot="{ errors }" rules="required|email|max:256" name="メールアドレス">
          <input type="text" id="useremail" name="useremail" v-model="useremail" autocomplete="email">
          <p v-show="errors.length" class="p-contact__error">{{ errors[0] }}</p>
        </validation-provider>
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__item">
        <label for="message">お問い合わせ内容</label>
        <validation-provider v-slot="{ errors }" rules="required|max:1000" name="お問い合わせ内容">
          <textarea id="message" name="message" v-model="message"></textarea>
          <p v-show="errors.length" class="p-contact__error">{{ errors[0] }}</p>
        </validation-provider>
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__item" v-show="false">
        <label for="message">スパムでない場合は空欄</label>
        <input type="text" name="bot-field" v-model="botField"/>
      </div>
      <!-- /.p-contact__item -->

      <div class="p-contact__submit">
        <button type="submit" :disabled="invalid || !validated">送信</button>
      </div>
      <!-- /.p-contact__submit -->
    </form>
    <!-- /.p-contact__form -->
  </div>
</template>

エラーメッセージの出し方は調べるといろんな書き方が出てくるのでこの辺りは調べて自分に合ったものを設定してもらえれば良いかと思います。

これでバリデーション機能のついたフォームになりました!

実際のバリデーション動作
実際のバリデーション動作

Netlify Formsの設定

Netlify側で初期設定みたいなものは、なんと「なし」です。
先ほどform要素にnetlify、もしくはdata-netlify="true"という属性と追加しましたが、それをそのままデプロイするだけで利用できます。

Welcomeメール

デプロイしてNetlify がformを認識すると、Welcomeメールが届いてFormsを利用できるようになったと通知されます。

Welcome to Form メール
Welcome to Form メール

通知設定

フォーム送信された時の通知設定はNetlifyの画面から設定できます。
Setting -> Forms -> Form notificationsから右上の「Add noticication」ボタンをクリックすると「Slack」「Webhook」「Eメール」の3つの通知方法を選択できます。

Netlify Forms 通知設定画面
Netlify Forms 通知設定画面

僕はEメールにしましたが、他も簡単そうだし設定が楽でいいなと思います。
IntegromatでWebhook使えばメールもSlackもその他のツールにもまとめて通知できるかも。今度やってみようかな。

Ajaxでフォーム送信する

ここからJavaScriptを書きます。
テンプレートと分けて書いていますが、同じpages/contact/index.vueの中身です。

<script>
  export default {
    data() {
      return {
        username        : '',
        katakana        : '',
        useremail       : '',
        message         : '',
        botField        : '',
        isSubmit        : false,
        isSending       : false,
        isError         : false,
        completeMessage : '',
      }
    },
    computed: {
      sendingClass(){
        return {
          'is-sending'  : this.isSending,
          'is-error'    : this.isError,
          'is-complete' : this.isSubmit
        };
      }
    },
    methods: {
      onSubmit() {
        if(this.isSending){
          return;
        }
        this.isSending = true;
        this.completeMessage = '送信処理中…';
        const params = new URLSearchParams();
        params.append('form-name', 'contact');
        params.append('username', this.username);
        params.append('katakana', this.katakana);
        params.append('useremail', this.useremail);
        params.append('message', this.message);
        if(this.botField){
          params.append('bot-field', this.botField);
        }
        this.$axios
        .$post('/', params)
        .then(() => {
          this.completeMessage = 'お問い合わせを送信しました!';
          this.resetForm();
          this.isSubmit  = true;
        })
        .catch(err => {
          this.completeMessage = 'お問い合わせの送信が失敗しました';
          this.isError   = true;
        })
        .finally(() => {
          this.isSending = false;
        });
      },

      resetForm() {
        this.username        = '';
        this.katakana        = '';
        this.useremail       = '';
        this.message         = '';
        this.isError         = false;
        this.$refs.observer.reset();
      }
    }
  }
</script>

axiosを使ってAjaxでフォーム内容を送信します。
form-nameのhiddenフィールドとハニーポッド用のbotFieldを含めるのを忘れずに。

onSubmit() {
  if(this.isSending){
    return;
  }
  this.isSending = true;
  this.completeMessage = '送信処理中…';
  const params = new URLSearchParams();
  params.append('form-name', 'contact');
  params.append('username', this.username);
  params.append('katakana', this.katakana);
  params.append('useremail', this.useremail);
  params.append('message', this.message);
  if(this.botField){
    params.append('bot-field', this.botField);
  }
  this.$axios
  .$post('/', params)//POST先はこのページでOK
  .then(() => {
    this.completeMessage = 'お問い合わせを送信しました!';
    this.resetForm();
    this.isSubmit  = true;
  })
  .catch(err => {
    this.completeMessage = 'お問い合わせの送信が失敗しました';
    this.isError   = true;
  })
  .finally(() => {
    this.isSending = false;
  });
},

また、「送信中」「送信エラー」「送信完了」のフラグを用意しておくことで、二重送信の防止や、それぞれのフラグに合わせた色の変化やメッセージの表示などに使えるのでフラグを作っています。

computed: {
  //フラグによってClassの付け外しをすることで簡単に色などのデザイン変更が可能
  sendingClass(){
    return {
      'is-sending'  : this.isSending,
      'is-error'    : this.isError,
      'is-complete' : this.isSubmit
    };
  }
},

唐突にでてきたaxiosについてはVueではおなじみのAjax用のライブラリですね。使い方の説明は省略しますが、今回はNuxt用に@nuxtjs/axiosを使いました。

このソースコードでは使っていませんが、completeMessageに送信完了メッセージを表示させられるようにしているので、トーストポップアップみたいなUIとうまく組み合わせるなどして送信完了のUIも作れます。

送信完了のために別ページに遷移させる場合はAjaxを使う必要がないので、form要素を下記のように変更すれば良いかと思います。

<validation-observer ref="observer" v-slot="{ invalid, validated }" tag="form" class="p-contact__form" name="contact" method="POST" action="mail-sent" data-netlify="true" data-netlify-honeypot="bot-field"  :class="sendingClass">
 <!-- フォームの内容 -->
</form>
  • @submit.prevent="onSubmit"を削除し通常のForm送信にする
  • action="mail-sent"を追加し、/mail-sentという送信完了ページに遷移

フォーム完成

ここまででフォームは完成!

実際のフォーム送信画面
実際のフォーム送信画面

送信してみるとメール通知もバッチリです。

Netlify メール通知画面
Netlify メール通知画面

通知の文面については本当に必要最低限のもので、タイトル含め変更はできませんが、内容わかるし全然問題ないと思います。許せない方は別の方法を検討しましょう。

自動返信機能を付ける

ここまででフォームとしては完成なのですが、1つ足りない点が。

管理者宛てに通知は来ますが、ユーザーには自動返信機能がないためお問い合わせ完了の通知が送れません。自動返信はなくてもいいっちゃいいけど、個人的には確認のためにあった方が親切かなと思います。

付けましょう。

Netlify Functionsの設定

Formsと同じようにデプロイした時にFunctionsのファイルがあるかどうかを認識するとWelcomeメールが来て使えるようになる仕組みですが、 Functionsの場合は設定ファイルが必要になります。

netlify.tomlというファイルをルートディレクトリ(nuxt.config.jsと同階層)に作成し、以下のように書きます。

[build]
command = "yarn build"
functions = "functions"
publish = "dist"

command = デプロイ時に実行するコマンド
functions = Netlify Functionsのファイルを入れるディレクトリ
publish = 公開ディレクトリ

これでルートディレクトリのfunctions内にファイルを置くことでNetlify Functionsを実行できるようになります。

トリガーを使う

Netlify Functionsは自由に簡易なAPIを作ることもできるのですが、いくつかトリガーも用意されています。今回はsubmission-createdというFormsの送信に合わせて実行できるトリガーを使って自動返信メールを送信する機能を作ってみます。

使い方は簡単。先ほどnetlify.tomlで設定したfunctionsディレクトリを作成し、その中にsubmission-created.jsという名前のファイルを作ります。これでこのファイルの中に書いた処理がFormsの送信時に実行されます。

あとはnodemailerというnodeでメール送信できるモジュールを使って自動返信処理を書いていきます。

まずYarnで必要なモジュールをインストール

yarn add -D nodemailer dotenv

そして処理内容を書きます。

functions/submission-created.js

require('dotenv').config();

const nodemailer = require('nodemailer');

exports.handler = function(event, context, callback) {
  const { username, katakana, useremail, message } = JSON.parse(event.body).payload.data;

  // OAuth認証情報
  const auth = {
    type         : 'OAuth2',
    user         : process.env.OAUTH_USER,
    clientId     : process.env.OAUTH_CLIENT_ID,
    clientSecret : process.env.OAUTH_CLIENT_SECRET,
    refreshToken : process.env.OAUTH_REFRESH_TOKEN
  };


  // トランスポート
  const transport = {
    service : 'gmail',
    auth    : auth
  };

  let transporter = nodemailer.createTransport(transport);

  const url = 'https://notes-sharesl.netlify.app/';

  let mailOptions = {
    from    : `notes by SHARESL <info@sharesl.net>`,
    to      : `${useremail}`,
    subject : '【notes by SHARESL】お問い合わせありがとうございます',
    text    : `${username} 様\n\nお問い合わせありがとうございます。\n以下の内容でフォームを送信いたしました。\n数日中に追って担当者よりメールにて回答をお送りいたします。\n今しばらくお待ちください。\n\n------ 送信内容 ------\n【お名前】\n${username}\n\n【フリガナ】\n${katakana}\n\n【メールアドレス】\n${useremail}\n\n【お問い合わせ内容】\n${message}\n\n--------------------\nnotes by SHARESL\n${url}\n`,
  };

  transporter.sendMail(mailOptions, function(error, info) {
    if (error) {
      callback(error);
    } else {
      callback(null, {
        statusCode: 200,
        body: 'ok',
      });
    }
  });
};

送信にはメールサーバーが必要なのでG SuiteアカウントのGmailを使いました。SMTP接続はGoogleのセキュリティ上では良くないとのことでOAuth認証を使うことにしました。その内容については下記を参考にしました。

Node.jsでGmailからメールをOAuth認証で送信する方法

https://qiita.com/markey/items/f0a2d5215ec428442673

nodemailerは初めて使ったのですが、Netlify上での使い方を探すとちょうど同じことをやっている方の記事があったのでこちらを参考にしました。

Netlify functionを使ってフォームが送信されたときに送信したユーザーのメールアドレスに自動返信する。

https://qiita.com/snemesk/items/355951ee36b5e5cdf0c7

環境変数の設定方法

process.env.OAUTH_USERなどの環境変数を使っていますが、こちらはNetlify上で設定することができます。clientId、clientSecret、refreshTokenは流出すると危険なため直書きせずに環境変数に入れてしまいます。

Netlify上の環境変数の設定は、
Setting -> Build & deploy -> Environment -> Environment variables から追加できます。

netlify.tomlに書く方法でも設定できるみたいですが、Gitに上がってしまうのでこの方法を使うことはないと思います。

自動返信メールの完成!

うまく自動返信できるようになりました。

自動返信メール内容

まとめ

今回はNetlifyでお問い合わせフォームを作る方法についてメモしました。
ポートフォリオとかこのブログぐらいの小規模サイトであれば全然問題なく使えそうな感じ。

なんといってもNetlifyもNuxtも使いやすいのがいいですね。
ビルド環境を新しくしたり今まで使ってたものを更新したりとかって、自分で環境作ってたら必要だけど、Nuxtだとそんなの必要ないですし。Netlifyだとサーバーのメンテも必要ないですし、静的だからセキュリティ面も安心。おまけに表示速度も高速。全部揃ってるこの構成なら無駄な時間をほとんどかけずに制作に集中できますよね。どんどん使っていきたいと思います。

CMSとの連携はContentfulが良く話題になってて使い勝手は悪くなさそうだけど、どこまで自由にカスタマイズできるんだろうか?

このブログはWordPressで今からContentfulに移行っていうのはちょっと難しそう。時間がある時にREST APIと連携して試してみようかなーと思います。