# Next.js

> Envía desde un componente de cliente del App Router de Next.js, o desde una server action con una clave secreta.

Hay dos buenas formas de enviar desde Next.js. Envía desde el navegador en un componente de cliente, lo que conserva la IP de cada visitante para los límites de frecuencia y funciona con todas las verificaciones antispam. O envía desde una server action, lo que mantiene el formulario funcionando sin JavaScript.

|   | Componente de cliente | Server action |
| --- | --- | --- |
| Funciona sin JavaScript | No | Sí |
| Los límites de frecuencia cuentan | Cada visitante | La IP de tu servidor |
| Verificación humana de Ikiform | Usa `challenge` del SDK | Se omite con una clave secreta |
| Dominios permitidos | Se comparan con el origen del navegador | Se omite con una clave secreta |
| Necesita una clave secreta | No | Sí |

## Instala el SDK

`Terminal`:

```bash
npm install @ikiform/sdk
```

El SDK es opcional. Todo lo de abajo también funciona con `fetch`; consulta [JavaScript y fetch](https://ikiform.com/es/docs/frameworks.md).

## Desde un componente de cliente

`app/contact/contact-form.tsx`:

```tsx
"use client";

import { submit } from "@ikiform/sdk";
import { useState } from "react";

type Status = "idle" | "sending" | "sent" | "error";

export function ContactForm() {
  const [status, setStatus] = useState<Status>("idle");
  const [errors, setErrors] = useState<Record<string, string[]>>({});
  const [message, setMessage] = useState("");

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus("sending");
    const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(event.currentTarget));
    if (result.ok) return setStatus("sent");
    setErrors(result.error.fields ?? {});
    setMessage(result.error.message);
    setStatus("error");
  }

  if (status === "sent") return <p>Thanks, we got it.</p>;

  return (
    <form onSubmit={onSubmit} noValidate>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required aria-invalid={Boolean(errors.email)} />
      {errors.email ? <p>{errors.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {status === "error" && !Object.keys(errors).length ? <p role="alert">{message}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send"}</button>
    </form>
  );
}
```

- `submit` nunca lanza excepciones. Se resuelve como `{ ok: true, id }` o `{ ok: false, error, status, requestId }`.
- `error.fields` relaciona los ids de campo con mensajes cuando el formulario tiene [campos](https://ikiform.com/es/docs/fields.md) definidos.
- Renderiza el componente desde cualquier página de server component, por ejemplo `app/contact/page.tsx`.

### Con la verificación humana de Ikiform

Si la verificación antispam del formulario es **Ikiform**, obtén un token justo antes de cada envío. `challenge` monta un pequeño frame de ikiform.com que permanece invisible salvo que Turnstile necesite un clic.

`contact-form.tsx`:

```tsx
const data = new FormData(event.currentTarget);
const turnstileToken = await challenge("frm_xxxxxxxxxxxxxxxx");
const result = await submit("frm_xxxxxxxxxxxxxxxx", data, { turnstileToken });
```

Impórtalo con `import { challenge, submit } from "@ikiform/sdk";`. Los tokens son de un solo uso, así que llámalo una vez por envío.

## Desde una server action

Crea una clave secreta en el panel, en **API keys**, con el permiso `submissions:write`, y guárdala como variable de entorno. Nunca la expongas al navegador: no le pongas el prefijo `NEXT_PUBLIC_`.

`.env.local`:

```bash
IKIFORM_SECRET_KEY=ik_sk_your_secret_key
```

`app/contact/actions.ts`:

```ts
"use server";

export interface ContactState {
  ok: boolean;
  message: string;
  fields?: Record<string, string[]>;
}

export async function sendContact(_previous: ContactState, formData: FormData): Promise<ContactState> {
  const body = new URLSearchParams();
  for (const [name, value] of formData) {
    if (typeof value === "string" && !name.startsWith("$ACTION")) body.append(name, value);
  }
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
    },
    body,
  });
  const result = await response.json();
  if (result.ok) return { ok: true, message: "Thanks, we got it." };
  return { ok: false, message: result.error.message, fields: result.error.fields };
}
```

`app/contact/contact-form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type ContactState, sendContact } from "./actions";

const initial: ContactState = { ok: false, message: "" };

export function ContactForm() {
  const [state, action, pending] = useActionState(sendContact, initial);

  if (state.ok) return <p>{state.message}</p>;

  return (
    <form action={action}>
      <label htmlFor="email">Email</label>
      <input id="email" name="email" type="email" required />
      {state.fields?.email ? <p>{state.fields.email[0]}</p> : null}
      <label htmlFor="message">Message</label>
      <textarea id="message" name="message" />
      {state.message && !state.fields ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send"}</button>
    </form>
  );
}
```

> Todos los envíos que salen de tu servidor vienen de la dirección IP de tu servidor, así que los límites de frecuencia por visitante de Ikiform cuentan a todos los visitantes juntos. El preset Standard permite 5 envíos por minuto por IP y formulario, algo que un sitio con mucho tráfico puede alcanzar. Envía desde el navegador cuando puedas, o elige el [límite de frecuencia](https://ikiform.com/es/docs/rate-limits.md) Relaxed o Custom para los formularios que pasan por un servidor.

El `submit` del SDK también se puede usar en una server action, pero no envía la cabecera `Authorization`, así que un formulario con dominios permitidos o con la verificación humana de Ikiform rechazaría o marcaría el envío. Usa `fetch` con la clave secreta como se muestra.

## Recibe webhooks en un route handler

Para actuar sobre cada envío en tu app de Next.js, añade un route handler. Consulta [Recibir webhooks](https://ikiform.com/es/docs/guides/webhook-handlers.md#nextjs).
