Files
diana/specification/38-lead-capture-spec.md

681 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 38 — Lead Capture Specification
## HER — Home Enhancement and Renovation
**Status**: Phase 3 — implementation-ready
**Implements**: FR-1, FR-2, FR-3, FR-4, FR-5, FR-6, FR-7 | AC-1, AC-6 | ADR-002
**Authority**: This document is the authoritative spec for all form field definitions, validation rules, error states, spam protection, and API integration. `33-frontend-spec.md` §5.6 governs page layout and visual styling; where the two documents overlap, this document takes precedence for behavior and copy.
---
## Table of Contents
1. [Form Fields](#1-form-fields)
2. [Validation Rules](#2-validation-rules)
3. [Form UX States](#3-form-ux-states)
4. [UX Copy](#4-ux-copy)
5. [AWS SES Integration](#5-aws-ses-integration)
6. [Spam Protection](#6-spam-protection)
7. [Accessibility](#7-accessibility)
8. [Mobile Considerations](#8-mobile-considerations)
9. [Thank-You / Confirmation](#9-thank-you--confirmation)
10. [Requirements Traceability](#10-requirements-traceability)
---
## 1. Form Fields
### 1.1 Field Inventory
The form collects five visible required fields plus one hidden honeypot field. All visible fields use the `FormField` component (see `33-frontend-spec.md` §6.7).
| # | Field name | Label | HTML type | `name` attr | `id` attr | `autocomplete` | Required |
|---|---|---|---|---|---|---|---|
| 1 | Full Name | `Full name` | `text` | `name` | `field-name` | `name` | Yes |
| 2 | Service Address | `Service address` | `text` | `address` | `field-address` | `off` | Yes |
| 3 | Phone | `Phone number` | `tel` | `phone` | `field-phone` | `tel` | Yes |
| 4 | Email | `Email address` | `email` | `email` | `field-email` | `email` | Yes |
| 5 | Project Description | `Describe the work` | `textarea` | `description` | `field-description` | `off` | Yes |
| — | Honeypot | *(none — hidden)* | `text` | `website` | `field-website` | `off` | No |
> **Note on `autocomplete="off"` for address**: Browser autofill for `street-address` fills a billing/home address, which may differ from the *service location*. Using `off` prevents misplaced autofill. This is intentional.
### 1.2 Field Details
#### Full Name
- **Label**: `Full name`
- **Placeholder**: `e.g. Jane Smith`
- **Help text**: *(none)*
#### Service Address
- **Label**: `Service address`
- **Placeholder**: `e.g. 123 Market St, San Francisco, CA`
- **Help text** (visible below label, above input): `"Where is the work located? City or full address."`
- **Style**: Help text rendered as `<p class="field-hint">` in DM Sans 400, `--text-sm`, `--col-slate`. Sits between label and input, `--space-1` margin below.
#### Phone Number
- **Label**: `Phone number`
- **Placeholder**: `e.g. (415) 555-0123`
- **Help text**: *(none)*
- **Input mode**: `inputmode="tel"`
#### Email Address
- **Label**: `Email address`
- **Placeholder**: `e.g. jane@example.com`
- **Help text**: *(none)*
- **Input mode**: `inputmode="email"`
#### Project Description
- **Label**: `Describe the work`
- **Placeholder**: `What needs to be fixed, replaced, or improved? Include any relevant details — size, urgency, or special requirements.`
- **Help text**: *(none; placeholder is the primary guide)*
- **Element**: `<textarea>`, resizable vertically only (`resize: vertical`)
- **Min height**: 120px on mobile, 160px on desktop (`md` and above)
#### Honeypot (hidden)
- **Label**: None — never rendered in the visible DOM
- **Name / ID**: `name="website"`, `id="field-website"`
- **Hidden via CSS** (see §6 for exact CSS; do not use `type="hidden"` — bots fill visible fields, not `type="hidden"` ones)
- **Expected value at submission**: empty string
- **Behavior if non-empty**: silently discard on client; Lambda also discards and returns `{ "ok": true }` (see §5)
### 1.3 Field Order
The canonical field order — used as DOM order, mobile stacking order, and keyboard tab order — is:
```
Full Name → Service Address → Phone → Email → Project Description → [Submit]
```
**Desktop layout note**: `33-frontend-spec.md` §5.6 specifies a two-column desktop layout where Name and Phone appear side-by-side in Row 1. This visual grouping is achieved with CSS Grid `order` properties, *not* by reordering the DOM. The DOM order defined above must be preserved for correct tab/focus sequence.
Desktop CSS grid layout (at `md` breakpoint and above):
```
[ Full Name (50%) ] [ Phone (50%) ] ← visual row 1 (CSS order: Name=1, Phone=3)
[ Service Address (100%) ] ← visual row 2 (CSS order: Address=2)
[ Email Address (100%) ] ← visual row 3 (CSS order: Email=4)
[ Project Description (100%) ] ← visual row 4 (CSS order: Description=5)
[ Submit → ] ← visual row 5
```
DOM order remains: Name, Address, Phone, Email, Description — so keyboard navigation flows logically even with visual rearrangement.
### 1.4 Submit Button
| Property | Value |
|---|---|
| Label | `Send My Request` |
| Component | `Button` (primary variant — see `33-frontend-spec.md` §6.1) |
| Mobile width | Full-width (100%) |
| Desktop width | Auto, min-width 200px, right-aligned within form container |
| Disabled state | While submitting: `aria-disabled="true"`, shows spinner (see §3) |
---
## 2. Validation Rules
### 2.1 Validation Strategy
- **Client-side**: HTML5 native attributes (`required`, `minlength`, `maxlength`, `pattern`) plus JavaScript for on-blur validation and submit-time validation. Do not rely on browser default validation UI — suppress with `novalidate` on the `<form>` element and handle all error rendering in JavaScript.
- **Server-side (Lambda)**: Re-validate all fields independently. Never trust client-only validation.
- **Error display**: Inline error messages rendered below each field, inside a `<p role="alert">` element that is always present in the DOM (even when empty). Field errors appear on blur (after first touch) and on submit attempt.
### 2.2 Per-Field Validation Rules
| Field | Required | Min length | Max length | Pattern | Error message |
|---|---|---|---|---|---|
| Full Name | Yes | 2 | 100 | *(none beyond length)* | `"Please enter your name."` |
| Service Address | Yes | 3 | 200 | *(none beyond length)* | `"Please enter the service address or city."` |
| Phone | Yes | — | — | US phone (see §2.3) | `"Please enter a valid phone number."` |
| Email | Yes | — | — | Standard email (see §2.3) | `"Please enter a valid email address."` |
| Project Description | Yes | 10 | 2000 | *(none beyond length)* | `"Please describe your project (at least 10 characters)."` |
| Honeypot | No | — | — | Must be empty | *(no error shown — silent discard)* |
### 2.3 Pattern Definitions
**Phone** (`name="phone"`):
```
^\+?[\d\s\-().]{7,20}$
```
Allows: digits, spaces, hyphens, parentheses, plus sign. Minimum 7 characters (shortest valid phone), maximum 20. Accepts US and international formats:
- `(415) 555-0123`
- `415-555-0123`
- `4155550123`
- `+1 415 555 0123`
**Email** (`name="email"`):
Use the `type="email"` browser built-in pattern, supplemented by JS check that the value contains at least one `@` and at least one `.` after the `@`. Do not write an overly strict regex — the AWS SES delivery failure is the ultimate validator for undeliverable addresses.
### 2.4 Validation Timing
| Trigger | Behavior |
|---|---|
| First focus (untouched field) | No validation; field is in default state |
| Blur (after first touch) | Validate field; show error if invalid |
| Input (after field has been touched and errored) | Re-validate on each keystroke; clear error as soon as valid |
| Submit button click | Validate all fields; show all errors simultaneously; move focus to first invalid field |
| Successful submission | No field validation (form is replaced or disabled) |
### 2.5 Character Counter (Description Field)
Display a live character counter below the `description` textarea showing `N / 2000 characters`. Counter updates on every keystroke.
- **Style**: DM Sans 400, `--text-xs`, `--col-slate`, right-aligned below textarea.
- **Warning state** (≥ 1800 characters): color changes to `#B5622A` (`--col-copper`).
- **Over-limit state** (> 2000 characters): color `#C0392B` (error red), counter reads `2000 / 2000` — the `maxlength` HTML attribute enforces the hard cap and prevents additional input.
---
## 3. Form UX States
The form operates as a client-side state machine with six distinct states.
### 3.1 State Definitions
#### Default
- All fields empty, no errors shown.
- Submit button enabled, label `"Send My Request"`.
- Character counter shows `0 / 2000`.
- Form element: `aria-busy="false"`.
#### Touched / Dirty (Per-Field)
- Validation runs on blur for each field independently.
- If invalid: inline error message appears below the field (`role="alert"`), field border changes to error red (`#C0392B`), `aria-invalid="true"` set on the input.
- If valid: error message cleared, border returns to default (`--col-chalk`), `aria-invalid="false"`.
- Other fields are unaffected.
#### Submitting
- Triggered when user clicks submit and all fields pass client-side validation.
- Submit button: spinner replaces button text, `aria-disabled="true"`, visually disabled (opacity 0.7, `cursor: not-allowed`). Do not use `disabled` attribute — this removes the element from tab order.
- All form fields: `disabled` attribute added (prevents editing during in-flight request).
- Form element: `aria-busy="true"`.
- No duplicate submissions possible while in this state.
- Spinner: 20×20px animated SVG ring, `--col-linen` color on copper button background.
#### Success
- Triggered on HTTP 200 response with `{ "ok": true }`.
- The form element is hidden (`display: none` or removed from DOM).
- The `FormFeedback (success)` component is rendered in its place (see §4.1 for exact copy).
- Focus is moved programmatically to the success message heading (`<h2>` or `<h3>` within the feedback block).
- The success component has `role="status"` and `aria-live="polite"` (see §7).
#### Error — Network / Server
- Triggered on: HTTP non-200 response, network failure, `{ "ok": false }` in response body, or unhandled exception.
- Form fields are re-enabled (remove `disabled`).
- Submit button returns to normal state.
- Form element: `aria-busy="false"`.
- `FormFeedback (error)` banner is rendered above the submit button (see §4.2 for exact copy). The form itself remains visible and filled — user does not lose their input.
- Focus is moved programmatically to the error banner heading.
- The error banner has `role="alert"` and `aria-live="assertive"` (see §7).
#### Partial Error — Field-Level from Server
- Triggered when Lambda returns `{ "ok": false, "error": "..." }` with a field-level message (e.g., a field failed server-side validation).
- The specific field is highlighted with error styling and `aria-invalid="true"`.
- The field-level error message is rendered in that field's error slot.
- The network/server error banner is also shown (degraded trust signal).
- Focus is moved to the first affected field.
### 3.2 State Transition Diagram
```
Default
├─(user types/tabs)──→ Touched/Dirty (per field)
├─(submit, valid)────→ Submitting
│ │
│ ├─(200, ok:true)──→ Success [terminal]
│ │
│ └─(error)─────────→ Error (Network/Server)
│ │
│ └─(user edits & resubmits)──→ Submitting
└─(submit, invalid)──→ Touched/Dirty (all fields) + focus → first error
```
---
## 4. UX Copy
All copy is final and must be used verbatim. Placeholders in brackets (`[phone]`, `[email]`) must be replaced with Diana's actual contact details at implementation.
### 4.1 Page Intro Lead
Rendered as the lead paragraph (`<p>`) in the `/contact/` page hero, below the H1. (See `33-frontend-spec.md` §5.6 Section 2.)
> "Tell Diana about your project. She responds within 12 business days."
Styling: DM Sans 400, `--text-lg`, `--col-ink`.
### 4.2 Success Message (FormFeedback — success variant)
Shown in place of the form after a successful submission. Component: `FormFeedback (success)` (`33-frontend-spec.md` §6.8).
**Heading**:
> "Your request is on its way!"
**Body**:
> "Diana will review your request and get back to you within 12 business days. If your project is urgent, you can also reach her directly at [phone] or [email]."
- `[phone]` renders as a `<a href="tel:[phone]">[phone]</a>` link.
- `[email]` renders as a `<a href="mailto:[email]">[email]</a>` link.
- Both links use `--col-copper` color, `text-decoration: underline`.
Implementation note — contact detail placeholders:
- Phone: `(415) XXX-XXXX` (replace at implementation — see §5.6 §4 "Secondary Contact Info" in `33-frontend-spec.md`)
- Email: `diana@example.com` (replace at implementation)
### 4.3 Network / Server Error Message (FormFeedback — error variant)
Shown as a banner above the submit button while the form remains visible. Component: `FormFeedback (error)` (`33-frontend-spec.md` §6.8).
**Heading**:
> "Something went wrong."
**Body**:
> "We couldn't send your request. Please try again, or contact Diana directly at [phone] or [email]."
- Same link treatment as §4.2.
- Form fields remain filled and editable.
### 4.4 Field-Level Error Copy (reference)
See §2.2 for the authoritative error message text for each field. Reproduced here for convenience:
| Field | Error message |
|---|---|
| Full Name | `"Please enter your name."` |
| Service Address | `"Please enter the service address or city."` |
| Phone | `"Please enter a valid phone number."` |
| Email | `"Please enter a valid email address."` |
| Description | `"Please describe your project (at least 10 characters)."` |
---
## 5. AWS SES Integration
Implements ADR-002. Resolves OQ-3 (endpoint URL and recipient email are deferred to implementation).
### 5.1 Endpoint
| Property | Value |
|---|---|
| URL | TBD — provided at implementation (see ADR-002, OQ-3) |
| Method | `POST` |
| Content-Type | `application/json` |
| Auth | None (public endpoint) |
| CORS | API Gateway must allow the production site origin (e.g. `https://her-home.com`) |
Store the endpoint URL as a build-time environment variable or a configuration constant (e.g. `FORM_ENDPOINT_URL`). Do not hardcode the URL in form markup.
### 5.2 Request Payload
The client sends a single JSON object. All string values are trimmed of leading/trailing whitespace before sending.
```json
{
"name": "string",
"address": "string",
"phone": "string",
"email": "string",
"description": "string",
"honeypot": "string"
}
```
| Field | Type | Notes |
|---|---|---|
| `name` | string | Trimmed, non-empty |
| `address` | string | Trimmed, non-empty |
| `phone` | string | Trimmed, non-empty |
| `email` | string | Trimmed, lowercase, non-empty |
| `description` | string | Trimmed, non-empty, max 2000 chars |
| `honeypot` | string | Always sent; value is `""` for legitimate submissions |
### 5.3 Lambda Responsibilities
The Lambda function is the authoritative gatekeeper and must enforce all rules independently of client validation.
**Processing order**:
1. **Honeypot check**: If `honeypot` field is a non-empty string, log the discarded submission (for monitoring), return HTTP 200 `{ "ok": true }`. Do not signal to the client that the submission was discarded.
2. **Server-side field validation**: Verify all five required fields (`name`, `address`, `phone`, `email`, `description`) are non-empty strings after trimming. If any fail, return HTTP 200 `{ "ok": false, "error": "Missing required fields." }`.
3. **SES send**: Call AWS SES `SendEmail` with the formatted email (see §5.4).
4. **Success response**: On successful SES send, return HTTP 200 `{ "ok": true }`.
5. **SES error**: On SES failure, log the error (CloudWatch), return HTTP 200 `{ "ok": false, "error": "Failed to send email. Please try again." }`.
### 5.4 Email Formatting
**Recipient**: Diana's inbox (address configured in Lambda environment variable `SES_RECIPIENT` — set at implementation)
**Sender**: A verified SES sender identity (e.g. `noreply@her-home.com`) — configured at implementation
**Reply-To**: The submitting user's email address (so Diana can reply directly from her email client)
**Subject line**:
```
New Quote Request from [name]
```
**Plain-text body**:
```
New quote request received via HER website.
Name: [name]
Phone: [phone]
Email: [email]
Address: [address]
Project Description:
[description]
---
Submitted at: [ISO 8601 timestamp in UTC]
```
**HTML email**: Optional enhancement — not required for v1. Plain-text body is sufficient.
### 5.5 Response Handling (Client)
| Scenario | Client action |
|---|---|
| `200 { "ok": true }` | Show success state (§3.1 Success) |
| `200 { "ok": false, "error": "..." }` | Show error banner (§3.1 Error) |
| `429 Too Many Requests` (AWS WAF rate limit) | Show error banner with copy: `"Too many requests. Please wait a moment and try again."` |
| Network error (no response) | Show error banner |
| Any non-200, non-429 HTTP status | Show error banner |
### 5.6 HTTP Status Code Policy
The Lambda **always returns HTTP 200** to the client, with success/failure detail in the JSON body. This policy:
- Prevents browser retry behavior triggered by 5xx responses.
- Keeps error handling logic entirely in JavaScript, not in HTTP status handling.
- Makes CORS error handling simpler.
**Exception**: AWS WAF may return `429 Too Many Requests` before the request reaches Lambda. The client must handle this status code explicitly (see §5.5).
### 5.7 Timeout Handling
- Client-side fetch timeout: **10 seconds**. If no response is received within 10 seconds, abort the request and show the error banner.
- Implementation: use `AbortController` with a 10-second timeout signal passed to `fetch()`.
---
## 6. Spam Protection
### 6.1 Honeypot Field (Primary)
The honeypot is the primary spam protection mechanism in v1. It requires no third-party service and adds no friction for real users.
**Field definition**:
- `name="website"`, `id="field-website"`, `type="text"`
- No `<label>` element rendered in the DOM.
- Never autofocused or tab-accessible.
**CSS hiding** (applied via a utility class, e.g. `.visually-trap`):
```css
.visually-trap {
position: absolute;
opacity: 0;
pointer-events: none;
tabindex: -1; /* set as HTML attribute, not CSS */
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
}
```
> **Do not use `display: none` or `visibility: hidden`** — many bots skip fields with these styles. CSS-based hiding that leaves the element in the normal document flow is more effective.
**Client-side behavior**: Before submitting, check `document.getElementById('field-website').value`. If non-empty, do not call the API — immediately transition to success state (silent discard).
**Server-side behavior**: Lambda checks `honeypot !== ""` → return `{ "ok": true }` without sending email.
### 6.2 AWS WAF Rate Limiting (Recommended)
Configure an AWS WAF Web ACL on the API Gateway with a basic rate-based rule:
| Parameter | Recommended value |
|---|---|
| Rule type | Rate-based |
| Rate limit | 5 requests per IP per 5 minutes |
| Action | Block (returns 429) |
| Scope | API Gateway stage |
This is a **recommendation for implementation**, not a blocking requirement for this spec. The client must handle 429 responses gracefully (see §5.5).
### 6.3 No CAPTCHA (v1)
No CAPTCHA or challenge-based verification is used in v1, per FR-2 (form must be lightweight). If spam volume becomes a problem post-launch, the recommended escalation path is:
1. Tighten AWS WAF rate rules.
2. Add Cloudflare Turnstile (invisible challenge, no user interaction required for most users).
3. As a last resort, add hCaptcha or reCAPTCHA v3.
---
## 7. Accessibility
All form accessibility requirements conform to WCAG 2.1 AA. These requirements supplement `33-frontend-spec.md` §9.
### 7.1 Labels
- Every visible input and textarea has a corresponding `<label>` element.
- Labels are associated via matching `for` (on label) and `id` (on input) attributes.
- Labels are always visible — never hidden, never replaced by placeholder-only patterns.
- Label text is DM Sans 500, `--text-sm`, `--col-graphite` (see `FormField` component).
### 7.2 Error Message Regions
- Each field has a sibling `<p>` element for its error message (e.g. `id="error-name"`), present in the DOM at all times.
- When no error: element is empty (`""`) and has `aria-hidden="true"`.
- When error present: element contains the error text, has `role="alert"`, and `aria-hidden` is removed (or set `"false"`).
- Error messages are injected — not toggled with CSS `display` — to ensure screen reader announcement.
### 7.3 `aria-describedby` on Fields
Each input/textarea carries `aria-describedby` pointing to both the help text element (if present) and the error element, even when empty:
```html
<input
id="field-address"
name="address"
type="text"
aria-describedby="hint-address error-address"
aria-invalid="false"
...
/>
<p id="hint-address" class="field-hint">Where is the work located? City or full address.</p>
<p id="error-address" role="alert" aria-hidden="true"></p>
```
### 7.4 `aria-invalid`
- Default state: `aria-invalid="false"` on all fields.
- On validation failure: `aria-invalid="true"`.
- On validation pass (after error): `aria-invalid="false"` restored.
### 7.5 Submitting State
- Form element: `aria-busy="true"` while request is in-flight.
- Submit button: `aria-disabled="true"` (not `disabled` — button stays focusable and announces its state).
- Button label updated to `"Sending…"` (visible text) while in submitting state.
### 7.6 Success State
- Success message container: `role="status"`, `aria-live="polite"`.
- On render, focus is programmatically moved to the success heading element.
- Success heading must be a heading element (`<h2>` or `<h3>`) — not a `<p>` or `<div>` — so it is discoverable by screen reader heading navigation.
### 7.7 Error Banner State
- Error banner container: `role="alert"`, `aria-live="assertive"`.
- On render, focus is programmatically moved to the error banner heading element.
- The banner must be in the DOM before the error occurs (with empty content), or be inserted immediately before focus is moved — do not rely on `aria-live` alone for focus; use explicit `focus()`.
### 7.8 Focus Management Summary
| State transition | Focus moves to |
|---|---|
| Submit with validation errors | First invalid field's `<input>` or `<textarea>` |
| Submitting → Success | Success message heading |
| Submitting → Error (network/server) | Error banner heading |
| Submitting → Partial field error (server) | First errored field's `<input>` |
### 7.9 Keyboard Navigation
- Tab order follows DOM order: Name → Address → Phone → Email → Description → Submit.
- All interactive elements are reachable via Tab and operable via Enter/Space.
- No keyboard traps.
- Submit button is reachable via Tab and activates on Enter.
---
## 8. Mobile Considerations
All requirements here supplement `33-frontend-spec.md` §3 (breakpoints) and ADR-003 (mobile-first).
### 8.1 Layout
- All fields are full-width (100%) on mobile (below `md` breakpoint).
- Fields stack vertically in DOM order: Name → Address → Phone → Email → Description → Submit.
- Submit button is full-width on mobile.
- Gap between stacked fields: `--space-3` (24px).
### 8.2 `inputmode` Attributes
| Field | `inputmode` value | Effect |
|---|---|---|
| Phone | `tel` | Numeric keypad with `+`, `-`, `(`, `)` on iOS and Android |
| Email | `email` | Keyboard with `@` and `.com` shortcut keys |
| All others | *(not set — default)* | Standard text keyboard |
### 8.3 `autocomplete` Attributes
| Field | `name` attr | `autocomplete` attr |
|---|---|---|
| Full Name | `name` | `name` |
| Phone | `phone` | `tel` |
| Email | `email` | `email` |
| Service Address | `address` | `off` |
| Description | `description` | `off` |
| Honeypot | `website` | `off` |
> **Address `autocomplete="off"` rationale**: Prevents browser/OS autofill from inserting the user's home or billing address into a field intended for the *service location*. A homeowner in Oakland requesting work at a rental in San Francisco must not have Oakland autofilled. See §1.2.
### 8.4 Textarea Sizing
| Breakpoint | Min height |
|---|---|
| Mobile (default) | 120px |
| Desktop (`md` and above) | 160px |
Textarea is resizable vertically only (`resize: vertical`). Max height is not constrained (user may expand freely).
### 8.5 Touch Targets
- All inputs and the submit button have a minimum tap target height of 44px, per `33-frontend-spec.md` §9.3.
- Submit button padding: at minimum `--space-2` (16px) top/bottom, `--space-4` (32px) left/right.
- Field labels tap targets include the input below them (clicking label focuses input via `for`/`id` association).
### 8.6 Viewport and Keyboard Behavior
- The `<meta name="viewport" content="width=device-width, initial-scale=1">` tag must be present (standard; prevents double-tap zoom breaking on iOS).
- On mobile, when a field is focused, the virtual keyboard pushes the viewport up. The form container must not use `overflow: hidden` in a way that clips the active field below the keyboard.
- Do not use `position: fixed` on the form or its container on mobile.
---
## 9. Thank-You / Confirmation
### 9.1 Options
Two approaches are available. The choice is flagged as **Open Question OQ-7** (see below).
#### Option A — In-Page Success State (No Redirect)
After successful submission, the form is hidden and the `FormFeedback (success)` component renders in its place on `/contact/`. No navigation occurs.
**Pros**: Simpler implementation, no extra page to maintain, success copy is already on the page.
**Cons**: If the user hits browser Back and then Forward, they may see the form in its original empty state. Duplicate submission via Back → Re-submit is prevented by client state (form resets to Success state on remount, or history state is used).
#### Option B — Redirect to `/thank-you/`
After `{ "ok": true }`, the client performs `window.location.href = '/thank-you/'`. The `/thank-you/` page renders the success message as a full page.
**Pros**: Eliminates back-button resubmission (form is gone from history); clean URL for conversion tracking if analytics are added later.
**Cons**: Requires a separate HTML page; slightly more complex routing.
**`/thank-you/` page requirements** (if Option B is chosen):
- Full standalone page with nav and footer.
- H1: `"Your request is on its way!"`
- Body copy: same as §4.2 success message.
- `noindex` meta tag: `<meta name="robots" content="noindex, follow">`.
- Included in `sitemap.xml` as `changefreq="never"` with no `<priority>` set — or excluded entirely if the sitemap is filtered to indexable pages only (see `37-seo-content-spec.md`).
- No form on this page.
### 9.2 Open Question — OQ-7
> **OQ-7**: Should successful form submission stay on `/contact/` (Option A — in-page success state) or redirect to `/thank-you/` (Option B — separate page)?
>
> **Recommendation**: Option A for v1, given that there is no analytics in v1 (ADR-005) and the redirect's primary benefit (conversion tracking) does not apply. Revisit if analytics are added later.
>
> **Decision needed from**: Diana / project owner.
> **Blocking**: Implementation of `38-lead-capture-spec.md`.
---
## 10. Requirements Traceability
### 10.1 Functional Requirements
| Requirement | Description | How this spec satisfies it |
|---|---|---|
| **FR-1** | Quote form accessible within 12 taps from any page | Form lives at `/contact/`; nav CTA "Get a Quote" links there from all pages (persistent nav) |
| **FR-2** | Form must be lightweight (no file uploads) | Five text fields only; no file input; no CAPTCHA in v1 (§6.3) |
| **FR-3** | Form collects Name, Address, Phone, Email, Description | All five fields defined in §1.1 with types, labels, and validation |
| **FR-4** | Clear confirmation and next-steps after submission | Success state (§3.1) and success copy (§4.2) specify exact heading + body with timeline and fallback contact |
| **FR-5** | Submissions delivered via existing AWS service | API Gateway + Lambda + SES integration fully specified in §5 |
| **FR-6** | Prominently expose form, email, and phone | Form is the primary channel; email and phone appear in success copy (§4.2) and error copy (§4.3) |
| **FR-7** | Secondary channels (email, phone) in header/footer and post-submission | Phone and email are clickable links in both success and error messages (§4.2, §4.3); header/footer governed by `33-frontend-spec.md` |
### 10.2 Acceptance Criteria
| Criterion | How this spec satisfies it |
|---|---|
| **AC-1** | Visitor can reach the quote form in 12 taps from any page | Fulfilled by persistent nav CTA (FR-1 above); this spec defines what happens after the user arrives at `/contact/` |
| **AC-6** | Secondary contact channels visible in header/footer and post-form confirmation | Success copy (§4.2) and error copy (§4.3) include phone and email as clickable links; requirement is met on form submission regardless of which page section is displayed |
### 10.3 Architecture Decision Records
| ADR | Decision | Relevance to this spec |
|---|---|---|
| **ADR-001** | Static website delivery | Form submission requires a dynamic backend; satisfied by API Gateway + Lambda (no server-side rendering required on the static site) |
| **ADR-002** | Form backend: AWS SES via API Gateway + Lambda | Directly implemented in §5; endpoint URL and SES recipient deferred to implementation per ADR-002 status |
| **ADR-003** | Mobile-first, performance-first design | §8 specifies mobile layout, `inputmode`, `autocomplete`, touch targets; no heavy JS frameworks; fetch API only |
| **ADR-005** | No analytics in v1 | Influences §9 recommendation: in-page success state (Option A) preferred since redirect's conversion-tracking benefit doesn't apply without analytics |
### 10.4 Open Questions (from `99-open-questions.md`)
| OQ | Status | Impact on this spec |
|---|---|---|
| **OQ-3** | Resolved (endpoint URL deferred) | API endpoint URL is a build-time constant (§5.1); recipient email is a Lambda env var (§5.4) — both set at implementation |
| **OQ-7** | New — raised by this spec | Thank-you page vs. in-page success (§9); must be decided before implementation |
### 10.5 Cross-Document References
| Document | Relationship |
|---|---|
| `33-frontend-spec.md` §5.6 | Defines `/contact/` page layout, hero copy, secondary contact section, and desktop two-column grid; this spec governs behavior and copy within the form |
| `33-frontend-spec.md` §6.7 `FormField` | Component used by all visible fields |
| `33-frontend-spec.md` §6.8 `FormFeedback` | Component used for success and error states |
| `33-frontend-spec.md` §6.1 `Button` | Submit button component |
| `33-frontend-spec.md` §9 Accessibility | Form accessibility requirements; §7 of this spec extends them |
| `37-seo-content-spec.md` | `noindex` handling for `/thank-you/` if Option B is chosen |
| `99-open-questions.md` | OQ-3 (resolved), OQ-7 (new — added by this spec) |