zsty.us

Before / After · Case Study

Palisades 1st Choice — WordPress brochure to a fire-rebuild operating surface

  • Local Pro Services
  • Real Estate

1st Choice Design & Development (CSLB #576474, est. 1989) had a 5-page Hostinger WordPress + Elementor brochure for their post-Palisades-fire rebuild service. One Fluent Forms contact form. No blog, no project gallery, no service-area landings, no client portal. Every 'Get Your Free Consultation' button dumped to the same generic form. Rebuilt on Next 16 + Tailwind v4 + Drizzle + Neon + R2 with a real admin dashboard, a homeowner client portal (timeline + documents + budget + photo updates), 20 SEO insight posts, a partner drone-upload pipeline, an Anthropic-drafted outreach queue running on a 4h Vercel Cron, and a Twilio inbound-SMS reply matcher on +1 213 526 0198 — without changing the visual language a fire-trauma family already recognized.

Same navy-and-periwinkle brochure on the outside. A typed Postgres app with an outreach agent and a homeowner portal on the inside.
🔒palisades1stchoice.vercel.app
jackiej.events — modern site

Receipts — measured

Numbers that moved.

5+
Routes
was 5+
0
DB tables
was 0
0
Blog posts
was 0
0
Background agents
was 0
  • 5 pages → 27+ routes (mirror pages, 3 city SEO landings, /free-consultation, /projects gallery, 20 SSG insight posts, admin, client portal)
  • WordPress 6.9.4 + Elementor + Fluent Forms (Hostinger) → Next 16 + Drizzle + Neon Postgres + R2 (Vercel)
  • 1 generic contact form → typed forms + DB-backed leads + outreach agent + outreach approval queue
  • 0 client-facing portal → /client/[slug] with status timeline, doc vault, budget tracker, message thread, drone gallery

The pixel diff

Overview, then the walkthrough.

Left: everything the legacy site shipped — frozen in place. Right: an auto-scrolling tour of the rebuild, from hero to booking. No slider to fight with.

Before — the overview
🔒palisades1stchoice.com
Before — the overview

This is everything the legacy site had to offer above the fold. No pricing, no calendar, no booking — the funnel ended at a mailto.

After — scroll through what shipped
🔒palisades1stchoice.vercel.app
After — scroll through what shipped

Auto-scrolls through the modern site so you can see the booking surface, the package grid, the pricing table, and the calendar inline — without leaving this page.

The four beats

Problem · Insight · Build · Outcome.

Scroll past the legacy frame. The four beats land in order. At the end, the modern site fades into the same window.

🔒palisades1stchoice.com
Legacy
01 · Problem

A WordPress brochure was costing them every lead.

Every 'Get Your Free Consultation' button dumped into a single Fluent Forms endpoint. Phone wasn't tel:. Email wasn't mailto:. No blog meant zero SEO surface. No client portal meant every status update was a phone call. Families in trauma needed structure; the site offered a contact form.

02 · Insight

Keep the brand. Replace the plumbing.

The navy-and-periwinkle visual language was already right for a contractor talking to families who lost everything. We kept Poppins, kept the drone heroes, kept the calm corporate voice. What needed to change was every system underneath — forms, content, ops, comms — and a homeowner portal that didn't exist.

03 · Build

Six phases, six weeks.

Phase 1 discovery + scaffold. Phase 2 preview deploy. Phase 2.5 wiring + CRUD. Phase 3 GitHub origin + DB-first projects + DNS/GBP playbooks. Phase 4 real auth + 20 blog posts + outreach agent skeleton. Phase 5 admin CRUD + client portal foundation. Phase 6 the rest: outreach queue, doc vault, progress messages, budget tracking, campaigns, mailers, chatbot, Vercel cron, Twilio webhook.

04 · Outcome

A working operating surface, not a brochure.

27+ routes, 13 DB tables, 6 background agents, 20 SSG insight posts, three city SEO landings, a Twilio number with inbound webhook, a Claude outreach drafter, an admin dashboard that replaces /wp-admin, a homeowner portal that replaces phone calls. Same hero. Different machine underneath.

🔒palisades1stchoice.vercel.app
Modern

Architecture

One chain replaced by another. Receipts above.

Old stack
  • WordPress 6.9.4
  • Elementor Pro
  • Fluent Forms
  • WPR Addons
  • Hostinger Reach
  • Hostinger hosting
New stack
  • Next 16
  • Tailwind v4
  • Drizzle + Neon
  • Cloudflare R2
  • Anthropic
  • Twilio
  • SES
  • Vercel + Cron

What changed

Grouped by what kind of system shipped.

Each claim ships with concrete evidence — env vars, table names, cadence chips. No marketing fluff.

Design

Same visual language, much more surface area

We kept the navy-and-periwinkle palette, the Poppins typography, the drone-captured aerial heroes, and the corporate-calm voice — these were already correct for a contractor talking to fire-trauma families. The rebuild adds a typed forms layer, a 6-step '01–06' phase timeline on /services, distinct /free-consultation and /contact forms (the old site dumped both into the same Fluent Forms endpoint), three city-specific SEO landings, and 20 SSG insight posts. Hero copy is preserved verbatim where it worked.

Before
5 pages, one form, no blog, every CTA hit the same generic contact endpoint.
After
27+ routes, typed Zod-validated forms, dedicated free-consultation flow, /projects gallery, /insights with 20 long-tail posts, three city landings (/service-areas/pacific-palisades, /altadena, /malibu).
Agent backbone

Outreach drafter on a 4-hour Vercel Cron

Every 4 hours the cron hits GET /api/agents/outreach (auto-injected Bearer CRON_SECRET). It pulls new leads that have no outreach_attempts yet, calls Anthropic claude-sonnet-4-5 with a system prompt that anchors voice ('corporate, calm, competent — never WALK-INS WELCOME energy, never imitate insurance or FEMA'), drafts personalized subject + body per channel (email or SMS), and writes the draft to outreach_attempts with status='queued'. Brendon reviews at /admin/outreach and approves with one click — which dispatches via the verified-identity SMTP path (email) or the Twilio raw API (SMS).

Before
No outreach — only the family who happened to find the contact form ever heard back.
After
Every new lead gets a Claude-drafted intro queued for human approval within 4 hours.
  • Schedule: 0 */4 * * * in vercel.json
  • Model: claude-sonnet-4-5
  • Throttle on send: 250ms between recipients
  • Bounce + complaint path: SES Configuration Set with SNS topics → Stalwart suppression list (Hetzner) — currently transitioning per 2026-05-28 audit
Agent backbone

Anthropic ChatWidget with RAG over the 20 insights posts

Floating widget at bottom-right of every public page (hidden on /admin/* and /client/*). POST /api/chat does keyword-overlap RAG over the posts table — lowercased ≥4-char terms, regex match on title or body_md, top 4 hits with their excerpts get injected into the system prompt. Streams Anthropic Messages API SSE through plain text/plain to the browser. System prompt anchors voice + always closes with 'Want a 30-min call? Book at /free-consultation — no charge, no obligation.'

  • Source: src/app/api/chat/route.ts + src/components/ChatWidget.tsx
  • Stop list: this, that, with, from, what, when, where, which, there, …
  • Render hook: usePathname() hides widget on /admin* and /client*
Retention

Homeowner client portal — timeline · docs · budget · messages

Each family gets /client/[slug] gated to project.ownerUserId. They see: an ordered status timeline (lead → design → permitting → insurance coordination → construction → final inspection → delivered), a document vault (insurance dec pages, permits, contracts — private R2 with presigned-GET, 5-min URLs), a budget tracker (received / paid / remaining with category breakdowns — the #1 question every family asks), a message thread with photo attachments (R2 public bucket), and a drone footage gallery. Internal-only entries flagged via visibility='internal' don't render to clients. 11 new DB tables backed by Drizzle + Neon.

  • 13 tables total in src/db/schema.ts including project_documents, project_messages, project_budget_entries
  • Budget = headline cards (Received / Paid / Remaining + % spent) + entries table with kind badges (RECEIVED green / PAID red / COMMITMENT)
  • Document vault: 100 MB cap, content-type whitelist, scoped key prefix documents/<projectId>/<random>-<safeName>
Real-time push

Twilio inbound-SMS reply matcher on +1 213 526 0198

Inbound SMS to the Palisades subaccount number POSTs to /api/webhooks/twilio. We normalize the From phone (strip everything but digits + +), join outreach_attempts ↔ leads on digit-matching phone, mark the most-recent sent SMS attempt as status='replied', persist replyBody + replyReceivedAt, return empty TwiML 200. The admin /outreach review page surfaces replies inline under the original draft.

  • Twilio subaccount AC0373…30208 (under Life Candles LLC parent)
  • Emergency address registered (87 N Raymond Ave Suite 526, Pasadena CA 91103) — avoids $75/emergency-call fee
  • X-Twilio-Signature validation: TODO marker in the route handler
Design

Mailers + EDDM print pipeline (no Cmd+P required)

/admin/mailers renders three print-ready EDDM flyer variants (Palisades / Altadena / Malibu) via Puppeteer + qrcode-generator. Each variant has a UTM-tagged QR code that opens /free-consultation?utm_source=eddm&utm_campaign={variant}. Click 'Print' → PDF download → ship to USPS EDDM bulk-mail vendor. Saves 10–15 minutes per print run versus rebuilding the Adobe artwork each campaign.

While the owner sleeps.

Autonomous surfaces

The agent backbone keeps the brand earning after hours. The owner approves; the system runs.

  • Outreach drafter

    every 4 hours (Vercel Cron)

    Pulls new leads, drafts personalized intros via Claude, queues for human approval. Brendon reviews + sends from /admin/outreach.

  • Inbound SMS reply matcher

    on each inbound SMS

    Matches replies on +1 213 526 0198 back to the original outreach_attempt by phone digits. Surfaces in /admin/outreach next to the draft that triggered it.

  • Status-change client notifier

    on each status change

    When Brendon flips a project from permitting → construction (etc.), an email goes to the linked owner_user_id with a templated update + deep link to /client/[slug].

  • New-message client notifier

    on each message post

    When staff posts a photo update + caption to a project, the owner gets an email summarizing the post + link to the timeline.

  • Drone-upload partner pipeline

    on each partner upload

    HMAC-token gated /drone-uploads/[token] route. Partner uploads raw .mp4/.heic to R2 via presigned PUT; DB row written via /api/drone-uploads/complete. Auto-processing webhook to Open Claw team (contract pending).

  • Public chatbot RAG

    on each user message

    Floating widget on every public page streams Claude answers grounded in the 20 SSG insight posts. Anonymous mode; OG Life subscription gate deferred until that ships.

← All rebuilds

Palisades 1st Choice — WordPress brochure to a fire-rebuild operating surface — zsty.us