docs(02): research phase - marketing site

Research covering Next.js App Router patterns, Nodemailer SMTP contact form,
pure-React testimonials carousel, honeypot spam protection, and React 19
useActionState API for the public marketing homepage phase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chandler Copeland
2026-03-19 14:50:36 -06:00
parent 0167c00462
commit 0cdf3e5f7d

View File

@@ -0,0 +1,583 @@
# Phase 2: Marketing Site - Research
**Researched:** 2026-03-19
**Domain:** Next.js 16 App Router — public marketing page, Nodemailer SMTP contact form, carousel UI
**Confidence:** HIGH
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
**Hero section**
- Layout: Split panel — photo on the left, text content on the right (mirrors the login page feel)
- Tone: Professional and polished — credentials, experience, trust-building (not casual)
- CTA button: "Get in Touch" — smooth-scrolls to the contact form section
- Copy: Claude writes the headline, subheading, and bio (professional Utah real estate agent framing). Wrap text in clearly commented placeholders so Teressa can swap in her own words before launch
**Testimonials**
- Display: Auto-scrolling carousel — rotates every 5 seconds, pauses on hover
- Navigation: Arrow controls and dot indicators so visitors can also advance manually
- Content: Claude writes 45 sample testimonials with realistic Utah buyer/seller scenarios and placeholder full names (e.g., "Sarah Mitchell"). Clearly commented so real ones can be swapped in
- Attribution: Quote + full name only (no city, no photo)
**Contact form**
- Delivery: Email notification to Teressa via Nodemailer + SMTP (no database storage in this phase)
- SMTP credentials: `CONTACT_EMAIL_USER` and `CONTACT_EMAIL_PASS` environment variables — never hardcoded
- Success state: Inline — the form is replaced by a thank-you message ("Thanks! Teressa will be in touch soon.") when submission succeeds
- Spam protection: Honeypot hidden field — bots fill it in, humans never see it; submissions with the honeypot filled are silently discarded
- Validation: Required fields are name, email, phone, and message — validated server-side in the form action
**Page structure**
- Section order (top to bottom): Sticky nav → Hero → Testimonials → Listings placeholder → Contact form → Footer
- Sticky nav content: Teressa's name as wordmark logo (left), anchor links on the right — Home, About, Listings, Contact — smooth-scroll to each section
- Listings placeholder: A tasteful "Listings Coming Soon" section with a brief description — no real data, just visual presence
- Agent portal: No link anywhere on the public site — Teressa navigates to `/agent/login` directly via URL
- Footer: Name + Utah real estate license number (`14196185-SA00` — NOTE: verify the last two characters, may be letters O or zeros) + copyright year + repeated nav links
**Placeholder assets**
- Hero photo: reuse `public/red.jpg` from Phase 1 (already in the project)
- Listings placeholder: use a tasteful stock-style image or a solid brand-colored background with an icon — no real property photos needed
- Background section breaks: subtle texture or light gradient using brand cream/navy — no photography required for non-hero sections
- Icons: use Lucide React (already likely available) for any UI icons (phone, email, map pin for contact section)
### Claude's Discretion
- Exact hero photo crop/focal point and overlay gradient (if any)
- Mobile nav pattern (hamburger menu vs collapsed links)
- Exact card styling, shadow depth, border radius
- Animation timing beyond the 5-second carousel interval
- Exact color weights within the brand palette already established (navy `#1B2B4B`, gold `#C9A84C`, cream `#FAF9F7`)
### Deferred Ideas (OUT OF SCOPE)
- None — discussion stayed within phase scope
</user_constraints>
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| MKTG-01 | Visitor sees a hero section with Teressa's professional photo and warm introductory bio | Split-panel layout pattern, Next.js Image component, `public/red.jpg` already in project |
| MKTG-02 | Visitor sees a "listings coming soon" placeholder section (full WFRMLS integration deferred to v2) | Static section component, brand color background + Lucide icon |
| MKTG-03 | Visitor can submit a contact form with name, email, phone, and message | Next.js Server Action, Nodemailer + SMTP, honeypot pattern, server-side Zod validation |
| MKTG-04 | Visitor sees a testimonials section with client reviews on the homepage | Pure React carousel (useState + useEffect interval), no external carousel library needed |
</phase_requirements>
---
## Summary
This phase builds a single public-facing homepage for Teressa Copeland Homes on top of the Next.js 16 + Tailwind CSS 4 stack established in Phase 1. The page is a single route (`app/page.tsx`) composed of full-width section components: sticky nav, hero, testimonials carousel, listings placeholder, contact form, and footer. No new major dependencies are required — Nodemailer is the only addition for contact form email delivery.
The carousel can be built with zero external libraries using React `useState` / `useEffect` — a 5-second auto-advance with hover-pause, arrow buttons, and dot indicators is straightforward in ~60 lines of client component code. Reaching for a carousel library like Embla or Swiper would add unnecessary bundle weight for a single simple use case.
The contact form uses a Next.js Server Action (no API route needed) with Zod validation already in the project, Nodemailer for SMTP delivery, and a honeypot hidden field for bot protection. No database writes occur in this phase — the form is fire-and-forget email only.
**Primary recommendation:** Build everything as Next.js App Router server components with minimal client component islands (carousel, mobile nav toggle, form state). Keep the public page route at `app/page.tsx` and colocate section components under `app/_components/` or `components/marketing/`.
---
## Standard Stack
### Core (already in project — no new installs for UI)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Next.js | 16.2.0 | App Router, Server Actions, Image optimization | Already installed; Phase 1 foundation |
| React | 19.2.4 | Component model | Already installed |
| Tailwind CSS | ^4 | Utility-first styling | Already installed; brand colors via inline styles or CSS vars |
| Zod | ^4.3.6 | Server-side form validation | Already installed; used in Phase 1 auth |
### New Dependency — Contact Form Email
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| nodemailer | ^6.9.x | SMTP email delivery from Server Action | De facto Node.js SMTP library; well-maintained; zero vendor lock-in |
| @types/nodemailer | ^6.4.x | TypeScript types | Required for TS strict mode |
### Supporting (already available)
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| lucide-react | latest | Icon set (phone, mail, map-pin, chevron, etc.) | Confirmed in CONTEXT.md; check if installed or add |
**Check if lucide-react is already installed:**
```bash
grep lucide /Users/ccopeland/temp/red/teressa-copeland-homes/package.json
```
If absent, add it.
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Hand-rolled carousel | Embla Carousel or Swiper | Overkill for a single 4-5 item testimonials carousel; adds bundle weight |
| Nodemailer | Resend SDK, SendGrid | Adds vendor dependency; Nodemailer works with any SMTP provider already owned by Teressa |
| Server Action form | API Route (`app/api/contact/route.ts`) | Server Actions are simpler, no fetch boilerplate; use progressive enhancement |
**Installation (new packages only):**
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes
npm install nodemailer
npm install --save-dev @types/nodemailer
# If lucide-react not present:
npm install lucide-react
```
---
## Architecture Patterns
### Recommended Project Structure
```
teressa-copeland-homes/
├── app/
│ ├── page.tsx # Public homepage — server component, composes sections
│ ├── layout.tsx # Root layout (already exists from Phase 1)
│ └── _components/ # Marketing page section components (colocated)
│ ├── SiteNav.tsx # Sticky nav — client component (mobile toggle state)
│ ├── HeroSection.tsx # Server component
│ ├── TestimonialsSection.tsx # Client component (carousel state + interval)
│ ├── ListingsPlaceholder.tsx # Server component
│ ├── ContactSection.tsx # Client component (form state, submission)
│ └── SiteFooter.tsx # Server component
├── lib/
│ ├── contact-mailer.ts # Nodemailer transporter + sendContactEmail()
│ └── contact-action.ts # Server Action — validates, calls mailer, returns state
└── public/
└── red.jpg # Hero photo (already exists)
```
**Why `app/_components/`:** Next.js treats files prefixed with `_` as private — they're not treated as routes. Keeps section components colocated with the page without polluting the route tree.
**Alternative:** `components/marketing/` at project root. Either works; pick one and be consistent.
### Pattern 1: Server Action for Contact Form (No API Route)
**What:** Form submits to a Server Action; action validates with Zod, sends email via Nodemailer, returns typed state.
**When to use:** Any form in Next.js App Router that doesn't need a public REST endpoint.
```typescript
// lib/contact-action.ts
'use server'
import { z } from 'zod'
import { sendContactEmail } from './contact-mailer'
const ContactSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Valid email required'),
phone: z.string().min(7, 'Phone is required'),
message: z.string().min(10, 'Message is required'),
// Honeypot — must be empty
website: z.string().max(0),
})
export type ContactState = { status: 'idle' | 'success' | 'error'; message?: string }
export async function submitContact(
_prev: ContactState,
formData: FormData,
): Promise<ContactState> {
const raw = {
name: formData.get('name'),
email: formData.get('email'),
phone: formData.get('phone'),
message: formData.get('message'),
website: formData.get('website'), // honeypot
}
const parsed = ContactSchema.safeParse(raw)
if (!parsed.success) {
return { status: 'error', message: 'Please fill in all required fields.' }
}
// Honeypot filled — silently succeed (don't tell the bot it failed)
if (parsed.data.website !== '') {
return { status: 'success' }
}
try {
await sendContactEmail(parsed.data)
return { status: 'success' }
} catch {
return { status: 'error', message: 'Something went wrong. Please try again.' }
}
}
```
### Pattern 2: Nodemailer SMTP Transporter
**What:** Singleton transporter initialized from env vars; called from Server Action.
**When to use:** Any server-side email send in Node.js/Next.js.
```typescript
// lib/contact-mailer.ts
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: process.env.CONTACT_SMTP_HOST, // e.g., smtp.gmail.com
port: Number(process.env.CONTACT_SMTP_PORT ?? 587),
secure: false, // true for 465, false for 587 (STARTTLS)
auth: {
user: process.env.CONTACT_EMAIL_USER,
pass: process.env.CONTACT_EMAIL_PASS,
},
})
export async function sendContactEmail(data: {
name: string
email: string
phone: string
message: string
}) {
await transporter.sendMail({
from: `"${data.name}" <${process.env.CONTACT_EMAIL_USER}>`,
replyTo: data.email,
to: process.env.CONTACT_EMAIL_USER, // Teressa's inbox
subject: `New contact from ${data.name} — Teressa Copeland Homes`,
text: `Name: ${data.name}\nEmail: ${data.email}\nPhone: ${data.phone}\n\n${data.message}`,
html: `<p><strong>Name:</strong> ${data.name}</p>
<p><strong>Email:</strong> ${data.email}</p>
<p><strong>Phone:</strong> ${data.phone}</p>
<p><strong>Message:</strong><br/>${data.message.replace(/\n/g, '<br/>')}</p>`,
})
}
```
### Pattern 3: Pure React Testimonials Carousel
**What:** Client component with `useState` (activeIndex), `useEffect` (5-second interval), pause on hover.
**When to use:** Simple auto-advance carousel with ≤ 10 items — no library needed.
```typescript
// app/_components/TestimonialsSection.tsx
'use client'
import { useState, useEffect, useRef } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
const TESTIMONIALS = [
// PLACEHOLDER — replace with real client testimonials before launch
{ quote: "Working with Teressa made buying our first home in Salt Lake County seamless...", name: "Sarah Mitchell" },
{ quote: "Teressa sold our Provo home in under a week above asking price...", name: "James & Karen Olsen" },
// add 3-5 total
]
export function TestimonialsSection() {
const [active, setActive] = useState(0)
const [paused, setPaused] = useState(false)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
useEffect(() => {
if (paused) return
intervalRef.current = setInterval(() => {
setActive(i => (i + 1) % TESTIMONIALS.length)
}, 5000)
return () => { if (intervalRef.current) clearInterval(intervalRef.current) }
}, [paused])
const prev = () => setActive(i => (i - 1 + TESTIMONIALS.length) % TESTIMONIALS.length)
const next = () => setActive(i => (i + 1) % TESTIMONIALS.length)
return (
<section
id="about"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* carousel content */}
<blockquote>"{TESTIMONIALS[active].quote}"</blockquote>
<p> {TESTIMONIALS[active].name}</p>
<button onClick={prev}><ChevronLeft /></button>
<button onClick={next}><ChevronRight /></button>
<div>
{TESTIMONIALS.map((_, i) => (
<button key={i} onClick={() => setActive(i)} aria-label={`Testimonial ${i + 1}`}
style={{ opacity: i === active ? 1 : 0.3 }} />
))}
</div>
</section>
)
}
```
### Pattern 4: Smooth-Scroll Anchor Navigation
**What:** Pure CSS + HTML `id` anchors; no JavaScript scroll library needed.
**When to use:** Single-page anchor navigation.
```typescript
// CSS approach — add to globals.css
// html { scroll-behavior: smooth; }
// Nav links
<a href="#hero">Home</a>
<a href="#about">About</a>
<a href="#listings">Listings</a>
<a href="#contact">Contact</a>
// Section targets
<section id="hero"> ... </section>
<section id="about"> ... </section> // testimonials anchors here per section order
<section id="listings"> ... </section>
<section id="contact"> ... </section>
```
### Pattern 5: Contact Form with useActionState (React 19)
**What:** React 19's `useActionState` replaces `useFormState` from react-dom/server. Works with Server Actions in Next.js App Router.
```typescript
'use client'
import { useActionState } from 'react'
import { submitContact, type ContactState } from '@/lib/contact-action'
const initialState: ContactState = { status: 'idle' }
export function ContactForm() {
const [state, action, pending] = useActionState(submitContact, initialState)
if (state.status === 'success') {
return <p>Thanks! Teressa will be in touch soon.</p>
}
return (
<form action={action}>
{/* Honeypot — visually hidden, bots fill it, humans ignore it */}
<input name="website" type="text" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
<input name="name" type="text" required placeholder="Your name" />
<input name="email" type="email" required placeholder="Email address" />
<input name="phone" type="tel" required placeholder="Phone number" />
<textarea name="message" required placeholder="How can Teressa help?" />
{state.status === 'error' && <p role="alert">{state.message}</p>}
<button type="submit" disabled={pending}>
{pending ? 'Sending...' : 'Send Message'}
</button>
</form>
)
}
```
### Anti-Patterns to Avoid
- **Using `useFormState` from `react-dom`:** Removed in React 19. Use `useActionState` from `react` instead.
- **Storing contact form submissions in DB in this phase:** CONTEXT.md locked to email-only delivery. DB storage is out of scope.
- **Importing carousel library (Swiper, Embla):** Adds unnecessary bundle for 4-5 static testimonials. Hand-roll with useState/useEffect.
- **Putting a link to `/agent/login` on the public site:** Explicitly locked out of scope — agent navigates there directly via URL.
- **Hardcoding SMTP credentials:** Always read from `CONTACT_EMAIL_USER` / `CONTACT_EMAIL_PASS` env vars.
- **Using `router.push` for smooth scroll:** Use CSS `scroll-behavior: smooth` + `<a href="#id">` anchors instead.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| SMTP email delivery | Custom fetch to mail API | `nodemailer` | Handles STARTTLS, auth, connection pooling, error codes |
| Form validation | Manual regex/string checks | `zod` (already installed) | Already in project; handles type coercion, error messages |
| Image optimization | Raw `<img>` tags | `next/image` | Automatic WebP conversion, lazy load, responsive srcset |
| Smooth scroll | `window.scrollTo()` JS | CSS `scroll-behavior: smooth` | Native browser feature; no JS required |
**Key insight:** The SMTP connection lifecycle (STARTTLS negotiation, auth failures, timeouts, connection pooling) is non-trivial. Nodemailer handles all of it — never reimplement it.
---
## Common Pitfalls
### Pitfall 1: `useFormState` vs `useActionState` (React 19 breaking change)
**What goes wrong:** Developer uses `useFormState` from `react-dom/server` — module not found or deprecated warning.
**Why it happens:** Most tutorials pre-date React 19. The hook moved.
**How to avoid:** Import `useActionState` from `react` (not `react-dom`). React 19.2.4 is in the project.
**Warning signs:** TypeScript error "useFormState is not exported from react-dom".
### Pitfall 2: Sticky Nav Overlapping Section Anchors
**What goes wrong:** Clicking "Contact" scrolls so the section heading is hidden behind the sticky nav.
**Why it happens:** Browser positions the anchor at `top: 0`, but sticky nav covers it.
**How to avoid:** Add `scroll-margin-top` to each section equal to nav height.
```css
section[id] { scroll-margin-top: 72px; } /* match nav height */
```
### Pitfall 3: Carousel `setInterval` Memory Leak
**What goes wrong:** Multiple intervals accumulate on re-renders; carousel skips or fires multiple times per second.
**Why it happens:** `useEffect` cleanup not returning `clearInterval`.
**How to avoid:** Always clear the previous interval in the `useEffect` cleanup function. Use a `ref` to track the interval ID (shown in Pattern 3 above).
### Pitfall 4: Honeypot Field Autofilled by Browser Password Manager
**What goes wrong:** Browser autofills the honeypot field, causing legitimate submissions to be silently discarded.
**Why it happens:** The hidden field has a name that triggers autofill (e.g., `name="email2"`).
**How to avoid:** Name the honeypot something bots seek but browsers won't autofill (e.g., `name="website"` or `name="url"`). Add `autoComplete="off"` and `tabIndex={-1}`. Use `style={{ display: 'none' }}` rather than `type="hidden"` (bots fill visible-in-DOM fields more reliably).
### Pitfall 5: Nodemailer `from` Address Rejected by SMTP Server
**What goes wrong:** Email silently fails or bounces because `from` address doesn't match authenticated user.
**Why it happens:** Gmail/SMTP providers require `from` to match the authenticated account.
**How to avoid:** Set `from` to `process.env.CONTACT_EMAIL_USER` (Teressa's SMTP account) and use `replyTo` for the visitor's email. The pattern in Pattern 2 above handles this correctly.
### Pitfall 6: Next.js `Image` Component with Local File
**What goes wrong:** `<Image src="/red.jpg" />` throws "Image is missing required 'width' or 'height'" or renders distorted.
**Why it happens:** Next.js Image requires explicit dimensions OR `fill` + positioned parent.
**How to avoid:** For the hero split-panel, use `fill` with `objectFit="cover"` on a positioned container.
```tsx
// Hero photo container
<div style={{ position: 'relative', height: '100%', minHeight: '500px' }}>
<Image src="/red.jpg" alt="Teressa Copeland" fill style={{ objectFit: 'cover', objectPosition: 'center top' }} priority />
</div>
```
---
## Code Examples
### Environment Variables Required
Add to `.env.local` (never commit):
```bash
# Contact form SMTP
CONTACT_EMAIL_USER=teressa@teressacopelandhomes.com
CONTACT_EMAIL_PASS=your_app_password
CONTACT_SMTP_HOST=smtp.gmail.com
CONTACT_SMTP_PORT=587
```
Note: Add `CONTACT_SMTP_HOST` and `CONTACT_SMTP_PORT` in addition to the CONTEXT.md variables — the host/port pair is required by Nodemailer but not mentioned in the user decisions. Use Gmail app password or equivalent.
### Sticky Nav with Mobile Toggle
```tsx
// app/_components/SiteNav.tsx
'use client'
import { useState } from 'react'
import { Menu, X } from 'lucide-react'
const NAV_LINKS = [
{ label: 'Home', href: '#hero' },
{ label: 'About', href: '#about' },
{ label: 'Listings', href: '#listings' },
{ label: 'Contact', href: '#contact' },
]
export function SiteNav() {
const [open, setOpen] = useState(false)
return (
<nav style={{
position: 'sticky', top: 0, zIndex: 50,
backgroundColor: '#1B2B4B', color: '#FAF9F7',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '1rem 2rem' }}>
{/* Wordmark */}
<span style={{ fontWeight: 700, letterSpacing: '0.05em' }}>Teressa Copeland</span>
{/* Desktop nav links */}
<ul className="hidden md:flex gap-8">
{NAV_LINKS.map(l => <li key={l.href}><a href={l.href}>{l.label}</a></li>)}
</ul>
{/* Mobile hamburger */}
<button className="md:hidden" onClick={() => setOpen(o => !o)}>
{open ? <X /> : <Menu />}
</button>
</div>
{/* Mobile drawer */}
{open && (
<ul style={{ backgroundColor: '#1B2B4B', padding: '1rem 2rem' }}>
{NAV_LINKS.map(l => (
<li key={l.href}><a href={l.href} onClick={() => setOpen(false)}>{l.label}</a></li>
))}
</ul>
)}
</nav>
)
}
```
### Footer with License Number
```tsx
// app/_components/SiteFooter.tsx
export function SiteFooter() {
return (
<footer style={{ backgroundColor: '#1B2B4B', color: '#FAF9F7', padding: '2rem' }}>
<p>Teressa Copeland</p>
{/* TODO: Verify license number before launch — last two chars may be "00" (zeros) or "OO" (letters O) */}
<p>Utah Real Estate License: 14196185-SA00</p>
<p>&copy; {new Date().getFullYear()} Teressa Copeland Homes. All rights reserved.</p>
</footer>
)
}
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `useFormState` from `react-dom` | `useActionState` from `react` | React 19 (2024) | Old import throws; use new hook |
| API Routes for form submit | Server Actions with `action={serverAction}` | Next.js 14+ | No `fetch`, no `/api/` endpoint needed |
| `pages/` router | App Router (`app/`) | Next.js 13+ | Project already uses App Router from Phase 1 |
| `<img>` tags | `next/image` | Next.js 10+ | Auto-optimization, lazy loading, responsive |
**Deprecated/outdated:**
- `useFormState` from `react-dom/server`: Removed in React 19. Use `useActionState` from `react`.
- Pages Router API routes for forms: Superseded by Server Actions in App Router.
---
## Open Questions
1. **SMTP provider / Gmail App Password**
- What we know: CONTEXT.md locked `CONTACT_EMAIL_USER` and `CONTACT_EMAIL_PASS` as env vars
- What's unclear: Whether Teressa uses Gmail (app password), Namecheap hosting email, or another provider — affects `host`/`port` config
- Recommendation: Add `CONTACT_SMTP_HOST` and `CONTACT_SMTP_PORT` env vars so the planner does not hardcode a provider. Document Gmail setup as the example (host: smtp.gmail.com, port: 587, secure: false, requires App Password with 2FA enabled).
2. **Lucide React installation status**
- What we know: CONTEXT.md says "use Lucide React (already likely available)" but it does not appear in package.json dependencies
- What's unclear: Whether it was installed but not listed, or needs to be added
- Recommendation: Plan task should `npm install lucide-react` as a safe no-op if already present.
3. **License number last two characters**
- What we know: `14196185-SA00` — user flagged uncertainty whether final chars are `00` (zeros) or `OO` (letters)
- What's unclear: The correct legal license number
- Recommendation: Add a `// TODO: VERIFY LICENSE NUMBER` comment in the footer code. Do not block implementation on this — it's a content swap before launch.
---
## Sources
### Primary (HIGH confidence)
- Next.js 16 App Router — Server Actions, `useActionState`, `next/image` patterns: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
- React 19 `useActionState` API: https://react.dev/reference/react/useActionState
- Nodemailer official docs: https://nodemailer.com/about/
- Project package.json (confirmed versions): Next.js 16.2.0, React 19.2.4, Zod ^4.3.6, Tailwind ^4
### Secondary (MEDIUM confidence)
- CSS `scroll-behavior: smooth` + `scroll-margin-top` MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-margin-top
- Honeypot spam protection pattern — multiple community sources agree on `display:none` + `tabIndex={-1}` + `autoComplete="off"` approach
### Tertiary (LOW confidence — validate before implementation)
- Gmail SMTP settings (host, port, app password requirement) — widely documented but Teressa's actual provider unknown until confirmed
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — package.json confirms exact versions; no guessing
- Architecture: HIGH — Next.js App Router patterns are stable and well-documented
- Carousel pattern: HIGH — pure React useState/useEffect; no library dependency
- Server Action + Nodemailer: HIGH — Next.js official docs confirm pattern; Nodemailer is stable
- SMTP provider specifics: LOW — depends on Teressa's email provider, unknown at research time
**Research date:** 2026-03-19
**Valid until:** 2026-04-19 (30 days — Next.js 16 and Nodemailer are stable; Tailwind 4 and React 19 patterns are settling but unlikely to break)