# Forms

Any HTML form on a Lobsite site can collect messages: point its `action` at `/_lobsite/forms/<name>` and deploy. There's no backend to write, no script to add and no key to paste. Submissions land in the inbox on your dashboard and in your email.

```html
<form action="/_lobsite/forms/contact" method="POST">
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>
  <button>Send</button>
</form>
```

## How forms work

- **The action names the form.** `/_lobsite/forms/contact` is the form `contact`. Names are lowercase letters, digits, `-` and `_`. Use one name per purpose (`contact`, `newsletter`, `quote`); several pages can share one form.
- **Forms are found at deploy time.** The deploy reads the HTML files and records each form that posts to `/_lobsite/forms/`. The form has to be in an HTML file in the deployed folder: a form that only exists after JavaScript renders it isn't found (see [Frameworks](#frameworks)).
- **Fields are stored as you name them.** Every field with a `name` is kept, as text. A field named `email` is used as the Reply-To of the notification email and as the title in the inbox. File uploads aren't supported.
- **Spam protection is added for you.** When the page is served, a hidden honeypot field and a signed timestamp are added to the form. Submissions that fill the honeypot or come back within 2 seconds go to the Spam tab instead of the inbox, and too many in a row from one visitor are refused.
- **After sending**, the visitor sees a thank-you page with a link back, or the page you set with `redirect`.
- **Forms work on production URLs.** Preview deploys don't accept submissions, and a site deployed without an account accepts them once it's claimed.

## Contact form

A complete page with a name, an email and a message. The labels and the `autocomplete` values make it easy to fill in; the `required` attributes stop an empty submission before it's sent.

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Contact</title>
</head>
<body>
  <h1>Get in touch</h1>
  <form action="/_lobsite/forms/contact" method="POST">
    <label>Name
      <input name="name" autocomplete="name" required>
    </label>
    <label>Email
      <input name="email" type="email" autocomplete="email" required>
    </label>
    <label>Message
      <textarea name="message" rows="5" required></textarea>
    </label>
    <button type="submit">Send</button>
  </form>
</body>
</html>
```

To check the same fields on the server and send visitors to your own thank-you page, add the form to `lobsite.toml` and create `thanks.html`:

```toml
[forms.contact]
required = ["name", "email", "message"]
types    = { email = "email" }
redirect = "/thanks"
```

## Newsletter signup

One field is enough. A second form on the same site gets its own name, so signups and messages stay apart in the inbox and in exports.

```html
<form action="/_lobsite/forms/newsletter" method="POST">
  <label for="nl-email">Get updates by email</label>
  <input id="nl-email" name="email" type="email" autocomplete="email" required>
  <input type="hidden" name="_redirect" value="/subscribed">
  <button>Subscribe</button>
</form>
```

`_redirect` is a hidden field that picks the page shown after sending. It must be a path or a URL on the same site; `redirect` in `lobsite.toml` takes precedence.

## Quote request

Selects, radio buttons and checkboxes work as usual. Checkboxes with the same name are stored as one comma-separated value.

```html
<form action="/_lobsite/forms/quote" method="POST">
  <input name="name" autocomplete="name" required>
  <input name="email" type="email" autocomplete="email" required>
  <input name="phone" type="tel" autocomplete="tel">
  <select name="budget">
    <option>Under $1,000</option>
    <option>$1,000 to $5,000</option>
    <option>Over $5,000</option>
  </select>
  <fieldset>
    <legend>Services</legend>
    <label><input type="checkbox" name="services" value="Design"> Design</label>
    <label><input type="checkbox" name="services" value="Development"> Development</label>
    <label><input type="checkbox" name="services" value="Hosting"> Hosting</label>
  </fieldset>
  <textarea name="details" required></textarea>
  <button>Request a quote</button>
</form>
```

## Sending without leaving the page

To show a message in place instead of loading a new page, send the form with `fetch`. Build the body with `new FormData(form)` so the hidden spam fields go along, and ask for JSON with the `Accept` header:

```html
<form id="contact" action="/_lobsite/forms/contact" method="POST">
  <input name="email" type="email" required>
  <textarea name="message" required></textarea>
  <button>Send</button>
  <p id="contact-status" role="status"></p>
</form>
<script>
  const form = document.getElementById("contact");
  const status = document.getElementById("contact-status");
  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    status.textContent = "Sending…";
    const res = await fetch(form.action, {
      method: "POST",
      body: new FormData(form),
      headers: { Accept: "application/json" },
    });
    const answer = await res.json();
    if (answer.ok) {
      form.reset();
      status.textContent = "Thanks! We'll be in touch.";
    } else {
      status.textContent = answer.errors ? Object.entries(answer.errors).map(([field, why]) => `${field}: ${why}`).join(", ") : "Something went wrong. Please try again.";
    }
  });
</script>
```

The answer is `{"ok": true}`, or `{"ok": false, "errors": {"email": "Required"}}` for fields that didn't pass, or `{"ok": false, "error": "rate_limited"}` (also `too_large`, `not_found`, `forms_off`). Without the script, the same form still works as a plain form.

## Frameworks

React, Vue, Svelte, Astro and the rest work when the form is in the built HTML, which is what prerendering and static export produce (Astro and Eleventy by default, Next.js with `output: "export"`, SvelteKit with `prerender`, Nuxt with `nuxt generate`). Keep the `action` and `method` attributes on the `<form>` element itself.

In a single-page app, the form has to be in the built HTML and stay as served: a form that exists only in JavaScript isn't recorded and its submissions are refused, and a form the browser re-renders loses the spam fields, so its messages land in Spam.

```jsx
// React: a plain form element, prerendered into the HTML.
export function Contact() {
  return (
    <form action="/_lobsite/forms/contact" method="POST">
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button>Send</button>
    </form>
  );
}
```

## Settings

Each form can have a section in `lobsite.toml`. All keys are optional; the settings travel with the deploy.

```toml
[forms.contact]
required  = ["email", "message"]                 # fields that can't be empty
types     = { email = "email", phone = "tel" }   # text, email, number, url or tel
redirect  = "/thanks"                            # the page shown after sending
notify    = ["you@example.com"]                  # who gets an email for each message
webhook   = "https://example.com/hooks/contact"  # POSTs each submission as JSON
```

A submission that fails `required` or `types` isn't stored; the visitor sees what to fix and a link back. On the free plan, `notify` can only be your account email and `webhook` isn't available.

## Reading submissions

- The inbox on your dashboard shows every message, with a Spam tab for what the checks caught. Reply opens your mail app.
- Each message is emailed to your account address, with Reply-To set to the sender.
- `lobsite forms ls` lists your forms; `lobsite forms export contact --csv` downloads one form's submissions.

On the free plan, forms run in limited mode: a monthly quota per site, notifications to your account email only, and no webhooks. The paid plan adds webhooks, any verified notification address and no monthly cap. See [Limits](https://lobsite.com/docs#limits).

