# Nuxt

> ハイドレーション前はプレーンなフォームとして、後は $fetch で送信する Nuxt のコンポーネント。フォーム ID はランタイム設定に置きます。nuxt generate でも動きます。

Nuxt はフォームをサーバーで描画するので、ブラウザーに届く HTML はすでに動くフォームです。アプリがハイドレーションされるまでは通常の HTML フォームとして送信し、その後はコンポーネントが引き継いで結果をその場に表示します。サーバーでは何も実行しないので、`nuxt generate` でビルドした静的サイトでも動きます。

代わりにシークレットキーを使って Nuxt のサーバールートから送信するには、[Vue](https://ikiform.com/ja/docs/guides/vue.md#nuxt) を参照してください。

## セットアップ

### ステップ 1: フォーム ID をランタイム設定に置く

`nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      ikiformFormId: "frm_xxxxxxxxxxxxxxxx",
    },
  },
});
```

環境ごとに `NUXT_PUBLIC_IKIFORM_FORM_ID` で上書きできます。`nuxt generate` では値がビルド時に埋め込まれるので、ビルド環境で変数を設定してください。

### ステップ 2: コンポーネントを追加する

`app/components/ContactForm.vue`:

```vue
<script setup lang="ts">
type Result =
  | { ok: true; id: string }
  | { ok: false; error: { message: string; fields?: Record<string, string[]> } };

const config = useRuntimeConfig();
const endpoint = `https://ikiform.com/f/${config.public.ikiformFormId}`;

const status = ref<"idle" | "sending" | "sent" | "error">("idle");
const errors = ref<Record<string, string[]>>({});
const message = ref("");

async function onSubmit(event: Event) {
  status.value = "sending";
  const result = await $fetch<Result>(endpoint, {
    method: "POST",
    headers: { Accept: "application/json" },
    body: new FormData(event.currentTarget as HTMLFormElement),
    ignoreResponseError: true,
  }).catch(() => null);
  if (result?.ok) {
    status.value = "sent";
    return;
  }
  errors.value = result?.error.fields ?? {};
  message.value = result?.error.message ?? "Couldn't reach the server. Try again.";
  status.value = "error";
}
</script>

<template>
  <p v-if="status === 'sent'">Thanks, we got it.</p>
  <form v-else :action="endpoint" method="post" @submit.prevent="onSubmit">
    <label>
      Email
      <input name="email" type="email" autocomplete="email" required />
    </label>
    <p v-if="errors.email">{{ errors.email[0] }}</p>
    <label>
      Message
      <textarea name="message" rows="5" />
    </label>
    <input type="hidden" name="_redirect" value="https://example.com/thanks" />
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
    <p v-if="status === 'error' && !errors.email" role="alert">{{ message }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Send" }}</button>
  </form>
</template>
```

- Nuxt 3 では、`app/components/` ではなく `components/` に置きます。
- `ignoreResponseError: true` を付けると `$fetch` が `4xx` で例外を投げなくなり、エラーの情報がデータとして返ります。`catch` に届くのはネットワークエラーだけです。
- `@submit.prevent` はアプリがハイドレーションされて初めて有効になります。それまでは、ブラウザーが `action` に送信して `_redirect` に従います。

### ステップ 3: ページで使う

`app/pages/contact.vue`:

```vue
<template>
  <ContactForm />
</template>
```

このフォールバックにはサーバーレンダリングかプリレンダリングが必要です。`ssr: false` では JavaScript が動くまでページが空なので、JavaScript なしで送信できるフォームがありません。

## 許可ドメイン

本番に加えて、デプロイ先のプレビューのホストを登録します。

| フォームが動く場所 | 許可ドメインに追加するもの |
| --- | --- |
| `nuxt dev` | `localhost`。`http://localhost:3000` に一致します。Ikiform が比較するのはホスト名だけで、ポートは比較しません。 |
| 自分のドメイン | `example.com`（`www.example.com` も含まれます） |
| Vercel | `your-project.vercel.app`。`your-project-git-branch-team.vercel.app` のようなプレビュー URL は一致しないので、テストする間はプレビューの正確なホストを追加してください。 |
| Netlify | `your-site.netlify.app`。デプロイプレビュー（`deploy-preview-12--your-site.netlify.app`）やブランチデプロイ（`branch--your-site.netlify.app`）はそのサブドメインではないので、テストする間はプレビューの正確なホストを追加してください。 |
| Cloudflare Pages | `your-project.pages.dev`。`abc123.your-project.pages.dev` のようなプレビュー URL はそのサブドメインなので、すでに一致します。 |

- `vercel.app`、`netlify.app`、`pages.dev` だけを追加しないでください。他の人のサイトも含め、そのホスト上のすべてのサイトが一致してしまいます。

## テストのチェックリスト

1. 許可ドメインに `localhost` を入れて `nuxt dev` を実行し、送信します。お礼のメッセージがフォームに置き換わるはずです。
2. ブラウザーの開発者ツールで JavaScript をオフにし、再読み込みして送信します。ローカルでは Ikiform のサンクスページに、`https:` のドメインでは `/thanks` に移動するはずです。
3. フィールドを定義した状態で無効なメールアドレスを送信し、メッセージがフィールドの下に表示されることを確認します。
4. `nuxt generate` を実行して出力をデプロイし、プレビューのホストから送信します。

## トラブルシューティング

| 症状 | 原因 | 対処 |
| --- | --- | --- |
| `FetchError: [POST] "https://ikiform.com/f/...": 422` | `ignoreResponseError` がないため、`$fetch` がエラーで例外を投げる | `ignoreResponseError: true` を追加します。 |
| `404 not_found` になり、action が `/f/undefined` で終わっている | サイトのビルド時にフォーム ID が設定されていなかった | ビルド環境で `NUXT_PUBLIC_IKIFORM_FORM_ID` を設定するか、`nuxt.config.ts` でデフォルト値を設定します。 |
| JavaScript なしでは何も起きない | ページが `ssr: false` でクライアントレンダリングされている | サーバーレンダリングを有効にするか、ページをプリレンダリングします。 |
| 送信データが `origin_mismatch` でスパムフィルターの下に届く | 送信したページが、フォームの許可ドメインに含まれていない | 上の表のホストを追加します。**Reject** を選んでいる場合、同じ送信には代わりに `403 origin_not_allowed` が返ります。 |

## 関連ページ

- [Vue](https://ikiform.com/ja/docs/guides/vue.md): 単一ファイルコンポーネント。
- [素の JavaScript](https://ikiform.com/ja/docs/guides/javascript.md): インラインエラー付きの `fetch`。
- [許可ドメイン](https://ikiform.com/ja/docs/spam.md#allowed-domains): オリジンの照合方法と、一致しない場合の動作。
- [エラー](https://ikiform.com/ja/docs/errors.md): すべてのエラーコードと、その原因と対処。
