# Contact form / lead capture

Every site needs a way for visitors to get in touch. uat-beam.page
provides this through the **email action** — a built-in server-side
endpoint that takes any JSON, formats it, and emails it to the site
owner.

This file is the deep dive on how the email action works, what the
HTML pattern looks like, and the gotchas.

## The single most important thing

**The email action requires a Google account.** If the user is on a
guest account (direct API only), convert them to Google before
building the form — see the main `llm.txt` for how. MCP users are
always on Google, so this is automatic.

## How the email action works

You POST any JSON to:

```
POST https://api.uat-beam.page/actions/<project-slug>/email
```

The endpoint takes whatever JSON you send and emails it to the site
owner's Google email address. No body validation — every field is
included verbatim. Rate limit: **10 emails per project per hour.**

**What the receiver gets:**

- **From:** `noreply@uat-beam.page` (or whatever the platform's SES
  sender is)
- **To:** the project owner's Google email
- **Subject:** the value of the `subject` field in your JSON, or a
  default like `"New message from <slug>.uat-beam.page"`
- **Body:** plain text, every field of your JSON listed as
  `key: value` lines

So if you POST `{"subject": "Quote request", "name": "John", "phone": "07700 900123", "issue": "Boiler leak"}`, the owner gets:

```
Subject: Quote request

subject: Quote request
name: John
phone: 07700 900123
issue: Boiler leak
```

Tell the user this so they know exactly what to expect in their inbox.

## The HTML pattern

A form using the email action with Alpine.js. This handles loading
state, success state, and error state cleanly.

```html
<form x-data="{ sending: false, sent: false, error: false, name: '', email: '', message: '' }"
      @submit.prevent="
        sending = true; error = false;
        fetch('https://api.uat-beam.page/actions/YOUR-SLUG/email', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            subject: 'New enquiry from ' + name,
            name: name,
            email: email,
            message: message
          })
        })
        .then(r => r.ok ? (sent = true) : (error = true))
        .catch(() => error = true)
        .finally(() => sending = false)
      "
      class="bg-white rounded-lg shadow p-8 space-y-5">

  <template x-if="!sent">
    <div class="space-y-5">
      <div>
        <label class="block text-sm font-semibold text-stone-700 mb-1">Your name</label>
        <input x-model="name" type="text" required
               class="w-full border border-stone-300 rounded p-3 focus:border-blue-500 focus:outline-none">
      </div>

      <div>
        <label class="block text-sm font-semibold text-stone-700 mb-1">Email</label>
        <input x-model="email" type="email" required
               class="w-full border border-stone-300 rounded p-3 focus:border-blue-500 focus:outline-none">
      </div>

      <div>
        <label class="block text-sm font-semibold text-stone-700 mb-1">Message</label>
        <textarea x-model="message" rows="5" required
                  class="w-full border border-stone-300 rounded p-3 focus:border-blue-500 focus:outline-none"></textarea>
      </div>

      <template x-if="error">
        <p class="text-red-600 text-sm">Couldn't send. Please try again or call us instead.</p>
      </template>

      <button type="submit" :disabled="sending"
              class="w-full bg-blue-700 text-white py-3 rounded font-semibold hover:bg-blue-800 disabled:opacity-50">
        <span x-show="!sending">Send message</span>
        <span x-show="sending">Sending...</span>
      </button>
    </div>
  </template>

  <template x-if="sent">
    <div class="text-center py-6">
      <h3 class="text-2xl font-bold text-blue-700 mb-2">Message sent</h3>
      <p class="text-stone-600">We'll be in touch soon.</p>
    </div>
  </template>
</form>
```

Replace `YOUR-SLUG` in the `fetch()` URL with the actual project slug.

Add Alpine.js to the page if it isn't already there:

```html
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>
```

## Pre-filling the form from another page

A common pattern: a "Book this course" button on one page links to
the contact form with the course name pre-filled. Use URL query
parameters.

The link on the source page:

```html
<a :href="'/contact?topic=Course+booking&course=' + encodeURIComponent(courseName)"
   class="bg-blue-700 text-white px-4 py-2 rounded">
  Book
</a>
```

The contact page reads the params on init and shows a banner:

```html
<div x-data="{
  params: new URLSearchParams(window.location.search),
  get topic() { return this.params.get('topic') || 'General enquiry'; },
  get prefilled() { return this.params.get('course') || ''; }
}">
  <template x-if="prefilled">
    <div class="bg-amber-50 border border-amber-200 rounded p-3 text-sm text-amber-900 mb-4">
      Booking enquiry for: <strong x-text="prefilled"></strong>
    </div>
  </template>

  <!-- ... form here, include `topic` and `prefilled` in the JSON body ... -->
</div>
```

Then in the `fetch()` body, include `topic: topic` and
`course: prefilled` so the email tells the owner exactly what was
booked.

## When the form is the whole product

For a single-page site where the contact form IS the point (like a
quote-request landing page for a tradesman), put the form front and
centre, above the fold. See `simple-page.md` for a complete example
with form included.

## Variations

- **Multiple form types on one site** — different forms can post to
  the same email action with different `subject` values. The owner
  sees them threaded by subject in their inbox.
- **Honeypot field for spam** — add a hidden `<input name="website">`
  and reject submissions where it's filled in. Bots fill everything;
  humans don't see it.
- **Include the page URL** in the body so the owner knows which page
  the visitor came from: `body: JSON.stringify({ ..., page: window.location.href })`.
