Phase 3: add themed specs (frontend, SEO, lead capture)

This commit is contained in:
Tom Roth
2026-07-31 07:56:55 +02:00
parent 5c3dcab6bf
commit ecbd9f94e5
5 changed files with 3263 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,680 @@
# 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) |

View File

@@ -42,6 +42,16 @@ This log tracks uncertainties and deferred decisions. Each item should be resolv
- **Rationale**: Carpentry and Mounting share the same installation/assembly search intent. Flooring touch-ups and Wood Refinishing share the same surface/finish search intent and existing image assets.
## OQ-7 — Thank-you page vs. in-page success state after form submission
- **Status**: Open — raised by `38-lead-capture-spec.md`
- **Question**: After a successful form submission, should the site stay on `/contact/` and show an in-page success state (Option A), or redirect to a dedicated `/thank-you/` page (Option B)?
- **Options**:
- **Option A (recommended for v1)**: In-page success state. Form is replaced by `FormFeedback (success)` component on `/contact/`. Simpler. No redirect. No extra page to maintain. Fits v1 since there is no analytics to track conversion events (ADR-005).
- **Option B**: Redirect to `/thank-you/`. Prevents back-button resubmission. Better for conversion tracking when analytics are added later. Requires a separate HTML page with `noindex`.
- **Recommendation**: Option A for v1. Revisit when/if analytics are added.
- **Blocking**: Implementation of `38-lead-capture-spec.md`.
- **Decision needed from**: Diana / project owner.
## OQ-6 — Brand naming and domain
- **Status**: Resolved
- **Resolution**: New public-facing brand name is **"Home Enhancement and Renovation — HER"**.