Zenn
🥶

SSRなNuxt3+Netlify Formsが機能しない問題の回避策

に公開

経緯

普段Flutterアプリの開発をしているkazy_developerです。

ちょっと前にNuxt3 + Tailwind + Netlifyの構成で、勉強がてらWEBサイトを作ったときに
Netlify Formsでつまづいたので、その回避策を備忘録を兼ねて残します。

TL;DR

結論、/public配下にダミー用のform.htmlを設置して、Netlify側にFormsを認識させた上で、nuxt.config.ts/contactをprerenderを有効にした上で/formsのエンドポイントにPOSTすることで正常にNetlify Formsにも届くようになりました。
しかし回避策であってなんかモヤっとするので直したいところでありますが、根本解決厳しそうなので、一旦これでいくことにします。

Netlify Formsとは

https://www.netlify.com/products/forms/
Netlify Formsは、ホスティングサービスのNetlifyが提供する機能の一つで、Webサイトにフォーム機能を簡単に追加できるサービス。

環境

$npx nuxi info
------------------------------
- Operating System: Darwin
- Node Version:     v22.11.0
- Nuxt Version:     3.14.159
- CLI Version:      3.15.0
- Nitro Version:    2.10.4
- Package Manager:  npm@10.9.0
- Builder:          -
- User Config:      default
- Runtime Modules:  @nuxtjs/tailwindcss@6.12.2, @nuxt/eslint@0.6.1, @nuxt/fonts@0.10.2
- Build Modules:    -
------------------------------

お問い合わせフォームの構成

バリデーションも付けたかったので、フォームを作ってuseValidationでチェック、手動でPOSTするという構成にしました。

冗長だったり間違っているところも多々あると思いますが、一旦流れで記載します。

contact.vueを定義する(完成系)

お問合せフォーム用のページとして/pages/contact.vueを作成します。

contact.vueの<template>
<template>
  <div class="flex items-center">
    <div class="container mx-auto">
      <div class="max-w-4xl mx-auto my-10 p-5">
        <div class="text-center">
          <h1 class="my-3 text-3xl font-semibold text-gray-700">
            お問い合わせ
          </h1>
        </div>
        <div v-if="!isSubmited" class="m-7">
          <h2 class="my-3 text-center text-xl font-medium text-gray-700">
            お問い合わせ内容
          </h2>
          <form
            data-netlify="true"
            netlify-honeypot="bot-field"
            name="contact"
            data-netlify-recaptcha="true"
            @submit.prevent="onSubmit"
          >
            <input type="hidden" name="form-name" value="contact" />
            <div class="mb-6">
              <label for="name" class="block mb-2 text-sm text-gray-600">
                お名前
                <span class="text-xs text-red-500">(必須)</span>
              </label>
              <input
                id="name"
                v-model="name"
                type="text"
                name="name"
                placeholder="お名前"
                class="w-full px-3 py-2 placeholder-gray-300 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-indigo-100 focus:border-indigo-300"
              />
              <Error
                v-if="params.name.$dirty && params.name.$anyInvalid"
                :message="params.name.required.$message"
              />
            </div>
            <div class="mb-6">
              <label for="email" class="block mb-2 text-sm text-gray-600">
                メールアドレス
                <span class="text-xs text-red-500">(必須)</span>
              </label>
              <input
                id="email"
                v-model="email"
                type="email"
                name="email"
                placeholder="your@example.com"
                class="w-full px-3 py-2 placeholder-gray-300 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-indigo-100 focus:border-indigo-300"
              />
              <Error
                v-if="params.email.$dirty && params.email.$anyInvalid"
                :message="params.email.required.$message"
              />
            </div>
            <div class="mb-6">
              <label for="phone" class="text-sm text-gray-600">
                電話番号
              </label>
              <input
                id="phone"
                v-model="phone"
                name="phone"
                type="tel"
                inputmode="tel"
                pattern="^(?:\+?[0-9])(?:[0-9-]*[0-9])?$"
                placeholder="000-0000-0000"
                class="w-full px-3 py-2 placeholder-gray-300 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-indigo-100 focus:border-indigo-300"
              />
              <Error
                v-if="params.phone.$dirty && params.phone.$anyInvalid"
                :message="params.phone.validate.$message"
              />
            </div>
            <div class="mb-6">
              <label for="message" class="block mb-2 text-sm text-gray-600">
                内容
                <span class="text-xs text-red-500">(必須)</span>
              </label>
              <textarea
                id="message"
                v-model="message"
                rows="5"
                name="message"
                placeholder="お問い合わせ内容"
                class="w-full px-3 py-2 placeholder-gray-300 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-indigo-100 focus:border-indigo-300"
              />
              <Error
                v-if="params.message.$dirty && params.message.$anyInvalid"
                :message="params.message.required.$message"
              />
            </div>
            <div data-netlify-recaptcha="true" />
            <div class="mb-6">
              <button
                type="submit"
                class="w-full px-3 py-4 font-bold text-white bg-red-900 rounded-md hover:bg-gray-700 focus:bg-gray-700 focus:outline-none"
              >
                送信する
              </button>
            </div>
          </form>
        </div>
        <div v-if="isSubmited" class="p-10 text-gray-700 text-center">
          <p>ありがとうございました!</p>
        </div>
      </div>
    </div>
  </div>
</template>
contact.vueの<script>
<script setup lang="ts">
import { ref } from "vue";
import { useValidation } from "vue-composable";

// フォームのフィールド
const name = ref("");
const email = ref("");
const phone = ref("");
const message = ref("");
const isSubmited = ref(false);

const params = useValidation({
  name: {
    $value: name,
    required: {
      $validator: (value: string | null | undefined): boolean => !!value,
      $message: "お名前を入力してください",
    },
  },
  email: {
    $value: email,
    required: {
      $validator: (value: string | null | undefined): boolean => !!value,
      $message: "メールアドレスを入力してください",
    },
    validate: {
      $validator: (value: string): boolean =>
        /\S+@\S+\.\S+/.test(value) || false,
      $message: "有効なメールアドレスを入力してください",
    },
  },
  phone: {
    $value: phone,
    validate: {
      $validator: (value: string): boolean =>
        value === "" || /^(?:\+?[0-9])(?:[0-9-]*[0-9])?$/.test(value),
      $message: "電話番号の形式が不正です",
    },
  },
  message: {
    $value: message,
    required: {
      $validator: (value: string | null | undefined): boolean => !!value,
      $message: "お問い合わせ内容を入力してください",
    },
  },
});

// エンコード関数
const encode = (data: Record<string, string>) =>
  Object.keys(data)
    .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
    .join("&");

// フォーム送信処理
const onSubmit = async () => {
  if (params.$anyInvalid) {
    params.$touch();
    return;
  }

  await fetch("/form", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: encode({
      "form-name": "contact",
      name: name.value,
      email: email.value,
      phone: phone.value,
      message: message.value,
    }),
  })
    .then((res) => {
      if (res.status === 200) {
        isSubmited.value = true;
        alert("送信が完了しました。");
      }
    })
    .catch((error) => {
      console.error("エラーが発生しました:", error);
      alert("エラーが発生しました。");
    });
};
</script>

UIはこんな感じ。

しかし、これでは機能しない

このままでは送信してもPOSTは成功しているのにForms側には届きません。
(もちろん、Netlify Formsを管理コンソール側でEnableにしてある)

その後、公式ドキュメントやググって出てくる方法を色々試しても改善せず...
ここでかなりの時間が溶けてしまった。辛い。

そして結果的には、
以下の記事に記載にある、SPA用の最終手段を試すことにしました。
https://qiita.com/reopa_sharkun/items/c920be4f52d0826c3bf3

静的なダミー用のHTMLでフォームを公開させる

まずNetlify側にフォームがあることを認識させる必要がありますが、contact.vueでは認識されていないのが今の状態です。
というのも、そもそも

data-netlify-recaptcha="true"

<div data-netlify-recaptcha="true" />

を入れているのに表示されていないのでやっぱりNetlify側に認識されていないように思います。

なので、以下の/public/form.htmlを作成します。

form.html

form.html
<!DOCTYPE html>
<html>
  <body>
    <!-- 完全にダミーフォーム専用 -->
    <form name="contact" netlify netlify-honeypot="bot-field" hidden>
      <input type="text" name="name" />
      <input type="text" name="email" />
      <input type="tel" name="phone" />
      <textarea name="message"></textarea>
      <input type="text" name="bot-field" />
    </form>
  </body>
</html>

上記をデプロイすると、Netlify Formsの画面にフォームが認識されたような表示に変わります。
ということで、contact.vueだけでは認識できていなかったのがさらに濃厚になりました。

追記: 別の方法 prerender: trueを使用する

たしか、Nuxtはデフォルトではユニバーサルレンダリングが設定されていた気がします。
その場合、
以下の記事にも説明があるように初回はSSR、ページ描画後の動的な処理についてはCSRとなる。
https://zenn.dev/ebi_yu/scraps/8e56f463b94eef

SSR,SSG,CSRの違い
https://qiita.com/buttakyou/items/4639cad427e2a336e472

なので、nuxt.config.tsで以下のようにお問い合わせフォームのページだけSSGに変更する。

export default defineNuxtConfig({
    routeRules: {
        "/contact": { prerender: true },
    },
},);

これで事前にHTMLが生成されて、Netlify側での要素の認識がされるようになりました。

手動POSTの場合のエンドポイントを変更

多分、contact.vueだけでフォーム認識されたら/contactでいけるのだろうと思いますが、
ネットのサンプルを真似たりしてて、ずっと/や認識されていない/contactでPOSTして届かなくてハマっていました。

似てる事象か定かではないですが、以下のフォーラムを見てて気付きました。
https://answers.netlify.com/t/nuxt-3-form-not-sending-data-no-errors/92578/9

最後に

何かを直せばあっさり機能するような気もしますが、静的なダミーフォームを作成して一旦は動作するようになったので、時間が取れたら改めて試してみようとは思います。

普段、自分の領域外の開発って色々と躓くところが多いですが、めげずに学習を続けていこうと思います。

Discussion

ログインするとコメントできます