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とは
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用の最終手段を試すことにしました。
静的なダミー用の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となる。
SSR,SSG,CSRの違い
なので、nuxt.config.ts
で以下のようにお問い合わせフォームのページだけSSGに変更する。
export default defineNuxtConfig({
routeRules: {
"/contact": { prerender: true },
},
},);
これで事前にHTMLが生成されて、Netlify側での要素の認識がされるようになりました。
手動POSTの場合のエンドポイントを変更
多分、contact.vue
だけでフォーム認識されたら/contact
でいけるのだろうと思いますが、
ネットのサンプルを真似たりしてて、ずっと/
や認識されていない/contact
でPOSTして届かなくてハマっていました。
似てる事象か定かではないですが、以下のフォーラムを見てて気付きました。
最後に
何かを直せばあっさり機能するような気もしますが、静的なダミーフォームを作成して一旦は動作するようになったので、時間が取れたら改めて試してみようとは思います。
普段、自分の領域外の開発って色々と躓くところが多いですが、めげずに学習を続けていこうと思います。
Discussion