【Nuxt.js】お問い合わせフォーム実装にWordPressのContact Form 7で対応する

![nuxt-wp-form.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173978/87e307bd-e3c7-3485-1ea7-b862ef094574.jpeg)

---------------
[2022年6月20日追記]

Contact Form 7 のバージョンアップ（5.6）により、本記事の方法でメールの送信ができなくなりましたので、ご注意ください。
新バージョンに対応した実装方法について、こちらでも調べてみますが、コメントでご教示いただけると幸いです！

---------------


# 今回やること
Nuxt.jsで作成した静的サイトにお問い合わせフォームを実装します。
フォームに入力されたデータをaxiosでWP REST API経由でWordPressのContact Form 7に送り、Contact Form 7からメールを送信します。

なお、今回フロントはNuxtで実装していきますが、Vue-cliでも、Reactでも、ES2018などをBabelで使う場合などでも、だいたい同じ方法で実装できるかと思います。

# WordPress側の設定
WordPressのインストールはここでは省略させていただきます。

## 基本設定 & 必要プラグインのインストール
<img width="1213" alt="Screen Shot 2019-04-11 at 20.59.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173978/1c27be2e-f0aa-d20a-c1e4-a26a464bc166.png">
まずは必要なプラグインをインストールしていきます。
### 1. Contact Form 7
今回主役のお問い合わせフォームのプラグイン。WordPressお問い合わせフォームプラグインの定番中の定番！

### ２. Application Passwords
WP REST APIでアクセスする際の認証に必要なアプリケーションパスワードを生成します。

## アプリケーションパスワードの設定

---------------
[2020年3月10日追記]

Nuxtアプリから、ContactForm7へ送信するだけであれば、
以下のApplication Passwordsの設定と認証の設定は不要とのことです！

@ixkaito さまよりコメントいただきました。
ありがとうございます！

---------------

Application Passwordsのプラグインを入れると、ユーザーの設定ページにApplication Passwordsの設定項目が追加されます。
<img width="1047" alt="Screen Shot 2019-04-11 at 22.02.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173978/ec4fd3e0-e00b-84fc-980e-790a863795a1.png">
任意の認証用ユーザー名を入力して [Add New]をクリック
一度だけパスワードが表示されるので、忘れずにコピーして一旦どこかに保存しておきましょう。


## Contact Form 7 の設定
<img width="1232" alt="Screen Shot 2019-04-11 at 22.59.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173978/684b0e79-f1cd-5b76-e5d3-d127973e265e.png">
ここで、フォームの項目、自動返信メールの設定、送信後のメッセージなどを設定します。
基本的には、普通にWordPressでContact Form 7を設定するのと同じです。
ただし、メールタグの名前は、後々JavaScriptで使うことを考えると、ケバブケースでなくキャメルケースにしておくと良いです。

## WP REST API にアクセスして確認
さて、ここまできたらREST APIでメールが送信できることを確認してみましょう。
HTTPクライアントにはPaw（ https://paw.cloud/ ）を使います。
<img width="1506" alt="Screen Shot 2019-04-11 at 23.14.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173978/b3aedfce-755a-46de-aa7f-207172b2f39e.png">

```http
https://{ドメイン名}/wp-json/contact-form-7/v1/contact-forms/{フォームID}/feedback/
```
BodyにはForm URL-Encodedに設定して、フォームの内容を入力していきます。

<img width="741" alt="Screen Shot 2019-04-11 at 23.20.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173978/269497f1-7ef2-dac2-00b8-314572147268.png">
AuthタブからBasic Authを選択し、先程設定したApplication Passwordsのユーザー名とパスワードをここに入力します。

リクエストを送信して、無事にメールが届けばOKです。



# フロントエンドの設定
今回のプロジェクトファイルはGitHubで公開しています。
https://github.com/hiropy0123/nuxt-contact-form


## Nuxtのセットアップ
create-nuxt-appでプロジェクトを作成します。
https://github.com/nuxt/create-nuxt-app

```bash
$ yarn create nuxt-app nuxt-contact-form
```
```
このように設定してみました。

? Project name nuxt-contact-form
? Project description My extraordinary Nuxt.js project
? Use a custom server framework -> none
? Choose features to install -> Linter / Formatter, Prettier, Axios
? Use a custom UI framework -> bulma
? Use a custom test framework -> none
? Choose rendering mode -> Universal
? Author name -> '名前を入れる'
? Choose a package manager -> yarn
```
### 追加でいくつかパッケージを追加していきます
便利な機能なので追加！

```bash
# Sass loader
$ yarn add -D node-sass sass-loader

# Pug loader
$ yarn add -D pug pug-plain-loader

# @nuxtjs/dotenv
$ yarn add @nuxtjs/dotenv

# lodash
$ yarn add lodash
```

## お問い合わせページの作成
`pages`ディレクトリにお問い合わせページを作成します。
フォーム部分はコンポーネントとして、別ファイルにします。
また、今回は簡単なバリデーション機能と、確認画面（モーダル表示）と送信完了画面（モーダル）を作成します。
モーダル部分も別コンポーネントで作成します。

```vue:pages/contact.vue
<template lang="pug">
  section.l-section 
    .l-container 
      contact-form(ref="form", @open-modal="openModal()")
      transition(name="modal", v-cloak)
        form-modal(
          v-if="isModal", 
          @close-modal="closeModal()", 
          @clear-data="clear()", 
          @submit="submit()",
          :data-response="responseData"
        )
</template>

<script>
import ContactForm from '@/components/ContactForm'
import FormModal from '@/components/FormModal'

export default {
  components: {
    ContactForm,
    FormModal
  },
  data() {
    return {
      isModal: false,
      responseData: null
    }
  },
  methods: {
    openModal() {
      this.isModal = true
    },
    closeModal() {
      this.isModal = false
    },
    clear() {
      this.$refs.form.reset()
    },
    submit() {
      const formData = this.convertJsontoUrlencoded(this.$store.state.formData)
      const USER = process.env.USER
      const PASSWORD = process.env.APPLICATION_PASSWORD
      // Base64に変換
      const TOKEN = window.btoa(`${USER}:${PASSWORD}`)
      const axiosConfig = {
        headers: {
          'Authorization': `Basic ${TOKEN}`,
          'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
        }
      }

      this.$axios
        .post('/contact-form-7/v1/contact-forms/5/feedback/', formData)
        .then(response => {
          console.log(response)
        })
        .catch(error => {
          console.log(error)
        })
    },
    // Conver to JSON Object to application/x-www-form-urlencoded
    convertJsontoUrlencoded(obj) {
      let str = [];
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            str.push(encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]))
        }
      }
      return str.join("&");
    }
  }
}
</script>
```

```vue:components/ContactForm.vue
<template lang="pug">
  div.contact-form
    .form-content 
      label.required 名前
      input(
        v-model="formData.yourName"
        type="text" 
        class="form-control" 
        name="yourName" 
        required 
        autocomplete="name"
        @blur="touched.yourName = true"
      )
      .input-hint 
        span.error(v-show="touched.yourName && !formData.yourName") 必須項目です

    .form-content 
      label.required メールアドレス
      input(
        v-model="formData.yourEmail"
        type="text" 
        class="form-control" 
        name="yourEmail" 
        required 
        autocomplete="email"
        @blur="touched.yourEmail = true"
      )
      .input-hint 
        span.error(v-show="touched.yourEmail && !formData.yourEmail") 必須項目です。
        span.error(v-show="touched.yourEmail && !validEmail(formData.yourEmail)") メールアドレスを正しく入力してください。

    .form-content 
      label 題名
      input(
        v-model="formData.subject"
        type="text" 
        class="form-control" 
        name="subject" 
      )

    .form-content 
      label お問い合わせ内容
      textarea(
        v-model="formData.message"
        class="form-control" 
        name="message" 
        rows="8" 
      )
    
    button(:disabled="hasError", @click="confirm") 確認画面へ
  
</template>

<script>
import _ from 'lodash'

const initialState = () => {
  return {
    formData: {
      yourName: '',
      yourEmail: '',
      subject: '',
      message: ''
    },
    touched: {
      yourName: false,
      yourEmail: false
    },
    valid: {
      yourName: false,
      yourEmail: false
    },
    isModal: false
  }
}

export default {
  data() {
    return initialState()
  },
  computed: {
    hasError() {
      return !this.validateForm()
    }
  },
  methods: {
    // バリデーション
    validEmail(email) {
      const RegExp = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
      return RegExp.test(email)
    },
    validateForm() {
      // 入力をトライしたかどうか
      const array = _.map(this.touched, item => {
        return item
      })
      const allTouched = array.every(value => {
        return value === true
      })

      if (this.formData.yourName) {
        this.valid.yourName = true
      } else {
        this.valid.yourName = false
      }
      if (this.formData.yourEmail) {
        this.valid.yourEmail = true
      } else {
        this.valid.yourEmail = false
      }

      // 入力されているかどうか
      const array2 = _.map(this.valid, item => {
        return item
      })
      const allValid = array2.every(value => {
        return value === true
      })

      return allTouched && allValid
    },
    // Vuexに保存
    storeForm() {
      const form = this.formData
      this.$store.commit('setFormData', form)
    },
    // 確認ページ
    confirm() {
      this.storeForm()
      this.open()
    },
    // モーダルを開く
    open() {
      this.$emit('open-modal')
    },
    // モーダルを閉じる
    close() {
      this.$emit('close-modal')
    },

    // dataを初期値に戻す
    reset() {
      Object.assign(this.$data, initialState())
    }
  }
}
</script>
```

## 確認画面と送信完了後のメッセージをモーダルで作成
```vue:FormModal.vue
<template lang="pug">
  .contact-confirm 
    .overlay
    slot
      .modal 
        h2(:class="myStatus") {{ dataResponse ? dataResponse.message : '入力内容をご確認ください。' }}
        .confirm(v-if="!dataResponse")
          dl 
            dt 名前
            dd {{ getFormData.yourName }}
          dl 
            dt メールアドレス
            dd {{ getFormData.yourEmail }}
          dl 
            dt 題名
            dd {{ getFormData.subject }}
          dl 
            dt お問い合わせ内容
            dd(v-html="getFormData.message")

          .btn-submit(@click="send") 送信
          .btn-return(@click="close") 修正する

        .response(v-if="dataResponse")
          .response-body(v-html="dataResponse.body")
          .btn-return(@click="close") 閉じる
</template>


<script>
import { mapGetters } from 'vuex'

export default {
  props: {
    dataResponse: {
      type: Object,
      required: false,
      default: null
    }
  },
  computed: {
    myStatus() {
      if (this.dataResponse !== null) {
        return this.dataResponse.status
      } else {
        return ''
      }
    },
    ...mapGetters(['getFormData'])
  },
  methods: {
    close() {
      this.$store.dispatch('resetForm')
      this.$emit('close-modal')
    },
    clear() {
      this.$emit('clear-data')
    },
    send() {
      this.clear()
      this.$emit('submit')
    }
  }
}
</script>
```


## 重要なポイント！
REST APIからフォームを送信するときの重要なポイントは2つあります。
１．　フォームで送信するデータをJSON形式から、X-WWW-FROM-URLENCODEDの形式に変換する
２．　headerにAuthorizationの情報を含める

### axiosの設定
先ほど、取得したApplication PasswordsとWordPressのAPIのURLをenvファイルに登録します。

```:.env
WP_REST_API_BASE_URL=https://ドメイン名/wp-json
WPUSER=ユーザー名
APPLICATION_PASSWORD="パスワード"
```
この`.env`ファイルはプロジェクトの一番上のディレクトリに置いて、`.gitignore`に登録して公開されないようにします。

```js:nuxt.config.js
require('dotenv').config() // 一番上にenvファイルを読み込む設定を追加

export default {
  // 諸々省略
  // ~
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/dotenv'
  ],
  // 環境変数の読み込み
  env: {
    WP_REST_API_BASE_URL: process.env.WP_REST_API_BASE_URL,
    WPUSER: process.env.WPUSER,
    APPLICATION_PASSWORD: process.env.APPLICATION_PASSWORD
  },
  /*
   ** Axios module configuration
   */
  axios: {
    baseURL: process.env.WP_REST_API_BASE_URL // baseURLの設定
  },
  // ~
}
```


### フォームのデータを `application/json` から `application/x-www-form-urlencoded` に変換する
```js
convertJsontoUrlencoded(obj) {
  let str = [];
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      str.push(encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]))
    }
  }
  return str.join("&");
}
```
### Authorizationの設定
```js
submit() {
  const formData = this.convertJsontoUrlencoded(this.$store.state.formData)
  const USER = process.env.WPUSER
  const PASSWORD = process.env.APPLICATION_PASSWORD
  // Base64に変換
  const TOKEN = window.btoa(`${USER}:${PASSWORD}`)
  const axiosConfig = {
    headers: {
      'Authorization': `Basic ${TOKEN}`,
      'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
    }
  }

  this.$axios
    .post('/contact-form-7/v1/contact-forms/5/feedback/', formData, axiosConfig)
    .then(response => {
      console.log(response)
      this.responseData = response.data
    })
    .catch(error => {
      console.log(error)
    })
}
```
submitメソッドの中で、`WPUSER`と`APPLICATION_PASSWORD`を呼び出して、TOKENを作成しています。
`window.btoa()` は文字列をBase64にエンコードしてくれます。


## 送信確認

実際に送受信の確認をしてみます。
正常に送信されると、Contact Form 7で設定した送信完了のメッセージが、表示されます。
送信されるメール本文の内容やメールの送信先は、Contact Form 7で設定できます。


# まとめ
WordPress Contact Form 7 は利用している方も多いと思うのですが、フロントエンドをNuxtにすることで、フォーム入力の表現や確認画面の表現が自由にできるのがいいですね！