← back to Professional Directory
agents/api-agent/MOCKUP_VARIANTS.md
243 lines
# Mockup Variants — Per-Org Website Pitch System
Process doc for the 4-direction mockup feature in `server.js`. Backfills the
process markdown that should have been written before the multi-step build
landed (per Steve's `feedback_suggest_process_md.md` rule).
---
## Why this exists
Sales pitch flow: pd-api scores small businesses lacking websites, then
generates a "here's what your site could look like" mockup that's emailed to
the prospect. Until 2026-05-01 the system rendered **one** template per org —
every prospect saw the same design language with only their content swapped
in. Steve's correction: *"Must be unique mockup sites for all. Do not do the
same site over and over."*
Resolution: **4 visually-distinct templates × per-org parameterization** so the
3-direction pitch grid an org sees is unique to them across the catalog.
---
## Templates
| ID | Name | Source | Visual signature | Palette count |
|----|----------------|------------------|------------------------------------------------------------|----|
| **A** | Modern Clinical | hand-built | Sans-serif (Inter), generous whitespace, 3 hero shapes (split / stacked / banner-stripe), Stripe/Notion-clean | 5 |
| **B** | Warm Community | hand-built | Iowan Old Style serif headlines + sans body, earth tones, hero card with circular badge punching out, editorial family-practice | 5 |
| **C** | Premium Wellness | hand-built | Didot serif XXL, dark luxury background, vertical accent line, 2-col service rhythm, concierge/spa aesthetic | 5 |
| **D** | Wellness App | **21st.dev — PulseFit hero** (`mcp__magic__21st_magic_component_inspiration`, query: "healthcare clinic landing hero") — ported from React+framer-motion to server-rendered HTML+CSS keyframe | Icy/soft gradient wash, centered Inter 80px headline, pill buttons, social-proof avatar row, **CSS-keyframe auto-scrolling service-tile carousel** with edge fades | 5 |
| **E** | Editorial-Brutalist | **21st.dev — Hero 03 by aliimam.in** (`mcp__magic__21st_magic_component_inspiration`, query: "brutalist landing hero", similarity 0.105 — only orthogonal direction the re-queries surfaced) — adapted to healthcare with per-type 3-line ALL-CAPS statements, vitals-red heart icon, HIPAA side rail | Helvetica Neue 300 / clamp(54-160px) / ALL-CAPS three-line statement, IBM Plex Mono italic flanking-meta paragraphs, vertical rotated right-edge badge, hairline `<Separator>` between sections, dotted x-ray-cyan grid bg, NO rounded chrome, NO gradients | 5 |
**Total base looks:** 5 templates × 5 palettes = 25 (+ 3 hero shapes for A → 35
combinations within A alone). The 5-up grid (A+B+C+D+E) gives 5⁵ = **3,125
unique 5-tuples** across the catalog — two orgs ever seeing identical 5-design
pitches is effectively impossible across 5K target orgs.
---
## Palette inventories
### Template A — Modern Clinical (`renderPreviewModernClinical`)
| Palette | bg | ink | accent | soft |
|---------|-----|-----|--------|------|
| teal | `#F7F9FB` | `#0E1726` | `#0F766E` | `#E6F2F1` |
| azure | `#FAFAFC` | `#1F2937` | `#2563EB` | `#E8EFFE` |
| sky | `#F8FAFC` | `#0B1220` | `#0EA5E9` | `#E0F2FE` |
| forest | `#F4F8F4` | `#14241D` | `#15803D` | `#DCFCE7` |
| amber | `#FAF8F4` | `#221A0F` | `#B45309` | `#FDE9C9` |
### Template B — Warm Community (`renderPreviewWarmCommunity`)
| Palette | bg | ink | accent | soft | card |
|---------|-----|-----|--------|------|------|
| honey | `#FBF6EF` | `#2A1F12` | `#A16207` | `#F2E2C5` | `#FFFAF1` |
| terracotta | `#F7F2EA` | `#1F1B16` | `#9F2F00` | `#F1D3C2` | `#FFF6EE` |
| olive | `#F4F6F0` | `#1B2014` | `#4D7C0F` | `#DCEBC2` | `#FBFCEF` |
| rose | `#FAF2F4` | `#241419` | `#9D174D` | `#F4D3DD` | `#FFF7F9` |
| lavender | `#F0EFF7` | `#191625` | `#5B21B6` | `#DCD6F5` | `#F8F6FF` |
### Template C — Premium Wellness (`renderPreviewPremiumWellness`)
| Palette | bg | ink | accent | soft | card |
|---------|-----|-----|--------|------|------|
| gold | `#0E0F12` | `#F1ECE0` | `#C9A567` | `#1B1D22` | `#16181D` |
| sage-noir | `#0C1116` | `#E8E4D8` | `#9CA88F` | `#15191F` | `#11161B` |
| copper | `#10110D` | `#EDE8DA` | `#B98C5A` | `#1A1B16` | `#15161F` |
| rose-quartz | `#0F0E10` | `#EFEAE0` | `#D4B5C6` | `#1A1820` | `#161520` |
| eucalyptus | `#0A0F11` | `#E2E7DD` | `#7BAA8B` | `#121819` | `#0F1517` |
### Template D — Wellness App (`renderPreviewWellnessApp`)
| Palette | gradient top | gradient mid | accent | soft |
|---------|--------------|--------------|--------|------|
| icy-blue | `#E8F0FF` | `#F5F9FF` | `#1E3A8A` | `#DBEAFE` |
| mint | `#E8FFF1` | `#F2FFF7` | `#065F46` | `#D1FAE5` |
| peach | `#FFEFE6` | `#FFF7F2` | `#9A3412` | `#FED7AA` |
| lavender | `#F1E8FF` | `#F7F2FF` | `#5B21B6` | `#E9D5FF` |
| soft-amber | `#FFF8E1` | `#FFFCF0` | `#92400E` | `#FEE9B0` |
### Template E — Editorial-Brutalist (`renderPreviewEditorialBrutalist`)
Mood derivation per Paper.design: each palette = a physical scene from a
clinical-paper world. Vitals-red and x-ray cyan recur as the brutalist accents.
| Palette | bg (paper) | ink | meta | accent (vitals) | cool (cyan/dot) | mood |
|---------|------------|-----|------|------------|----------|--------|
| bone | `#F4F1EA` | `#0A0A0A` | `#5C5C5C` | `#D63F2A` | `#7AB8C4` | chart paper at dawn |
| chart | `#F1F4F0` | `#13241D` | `#506257` | `#1B5E20` | `#A8C8B5` | botanical |
| clinical | `#F8F8F4` | `#0F1115` | `#5A5A5A` | `#1A4D8C` | `#B8CCE0` | mineral |
| dawn | `#F2EEE3` | `#1A0F0A` | `#6B4F35` | `#C2410C` | `#FBE3B3` | candlelit |
| radiograph | `#1A1B1F` | `#E8E8E8` | `#888888` | `#FF6B5B` | `#65BAC8` | nocturnal x-ray (only dark variant) |
**Per-type headline mapping** (`BRUTALIST_HEADLINES` const) — each org type
gets a unique 3-line ALL-CAPS statement so the hero doesn't read as generic.
Examples: hospital → `WORLD / CLASS / CARE`, dental_office → `QUIET / ROOMS /
HONEST PRICES`, hospice → `COMFORT / DIGNITY / PRESENCE`. Mid-line gets a
vitals-red heart SVG inserted as a typographic glyph between the first and
remaining words.
**Per-type side-rail badge** (`BRUTALIST_BADGES` const) — vertical rotated text
pinned to the right viewport edge, e.g., hospital → `EMERGENCY READY`,
dental_office → `HSA / FSA`, hospice → `24/7 ON-CALL`.
---
## Parameterization (per-org rotation)
```js
function pickSeed(str, mod) {
let h = 0;
for (let i = 0; i < String(str).length; i++) h = ((h << 5) - h) + str.charCodeAt(i) | 0;
return Math.abs(h) % mod;
}
```
For each template, palette is picked via `pickSeed(o.id + variantTag, palettes.length)`.
Variant tag is `'A' | 'B' | 'C' | 'D'`. Within template A, hero shape uses a
second seed: `pickSeed(o.id + 'A-hero', 3)`. Same orgId → same palettes across
reloads (deterministic), so the 4-design pitch is reproducible.
**Why deterministic instead of random:** prospects who reload the email link a
day later must see the same 4 designs they saw before. Random rotation would
break consistency between the email and the live pitch page.
---
## Routes
| Route | Renders | Notes |
|-------|---------|-------|
| `GET /preview/:orgId` | Original single template (`renderPreview`) | Pre-existing, unchanged. Kept for backward compat with any link still floating around. |
| `GET /preview/:orgId/all` | 4-up iframe grid (`renderPreviewVariantIndex`) | **Default for new pitch flow.** Responsive: 4-up desktop, 2-up tablet, 1-up phone. |
| `GET /preview/:orgId/a` (or `/modern`) | Template A | |
| `GET /preview/:orgId/b` (or `/community`) | Template B | |
| `GET /preview/:orgId/c` (or `/premium`) | Template C | |
| `GET /preview/:orgId/d` (or `/wellness`) | Template D | |
| `GET /preview/:orgId/e` (or `/editorial` / `/brutalist`) | Template E | |
| Other variant tokens | 404 | Validated in handler — Express 5 dropped inline regex like `:variant(a\|b\|c)` |
---
## Where each thing lives in `server.js`
| Function | Role |
|----------|------|
| `pickSeed(str, mod)` | Hash → palette/shape index |
| `renderPreviewModernClinical(o, pros)` | Template A |
| `renderPreviewWarmCommunity(o, pros)` | Template B |
| `renderPreviewPremiumWellness(o, pros)` | Template C |
| `renderPreviewWellnessApp(o, pros)` | Template D — adapted from 21st.dev PulseFit hero |
| `renderPreviewEditorialBrutalist(o, pros)` | Template E — adapted from 21st.dev Hero 03 (aliimam.in) |
| `renderPreviewVariantIndex(o)` | 5-up grid index page (responsive: 5-up→3-up→2-up→1-up) |
| `splitNameAccent(name)` | Helper — safe lead/tail split for B+C `<em>` accent (single-word safe) |
| `ctaHref(phoneTel)` | Helper — phone tel: or fall back to `#contact` anchor (no dead `#`) |
Pitch modal action row (in `renderDashboard`'s inline JS) has direct links to
`/preview/:id/{all,a,b,c,d}` plus the legacy bespoke mockup + the prospect's
current site.
---
## How to add a 5th template
1. **Source.** Either hand-build OR fetch from a design-system MCP:
- `mcp__magic__21st_magic_component_inspiration` — query needs to be specific
("brutalist editorial hero", "monospace dev-tool landing", "magazine
drop-cap layout"); generic queries return same 3 results.
- `mcp__figma__get_design_context` — pass a Figma node URL.
2. **Convert.** If sourced from React+animation lib (framer-motion, etc.),
port to **server-rendered HTML+CSS** — no JS runtime. Keyframe animations
are fine; React/framer-motion hydration is not.
3. **No stock images.** Per `feedback_no_stock_images.md`. Use
abstract gradient tiles, color blocks, or CSS-only illustrations. The
pitch goes to ~5K orgs; stock photos would be identical across all of
them and defeat the uniqueness goal even if the rule didn't exist.
4. **Add to rotation:**
- Define palette inventory (5 entries) at the top of the renderer.
- Use `pickSeed(o.id + 'E', palettes.length)` for the palette pick.
- Export `renderPreviewYourTemplate(o, pros)`.
5. **Wire routes:** add token (e.g. `'e'`) to `VALID` Set in the
`/preview/:orgId/:variant` handler, add to the renderer ternary chain.
6. **Update `/all` index** (`renderPreviewVariantIndex`): change grid to
`repeat(5, 1fr)`, add the new column.
7. **Update pitch modal** (in `renderDashboard`): add the new link to the
action row; update the email body copy from "four concepts" → "five
concepts" + the new variant name.
8. **Update this doc** — palette table, total-look math, route table.
---
## Deploy + restart notes
- pd-api runs as **pm2 `pd-api`** on `127.0.0.1:9874` (loopback only — Mac2)
- Restart: `pm2 restart pd-api` (no SIGINT issues; clean reload).
- pd-api is **not in `cncp-config.json domains[]`** — restarts don't fall
under the `feedback_pm2_deploy_explicit_auth.md` rule. "go"/"y" is
sufficient authorization for restarting it.
- Smoke test after any template change:
```
for v in a b c d all; do curl -s -o /dev/null -w "/$v %{http_code}\n" http://127.0.0.1:9874/preview/19/$v; done
```
- Validate variant fingerprints across multiple orgs (palette rotation):
```
for orgid in 19 20 21 100; do
curl -s http://127.0.0.1:9874/preview/$orgid/a | grep -oE "palette=[a-z-]+" | head -1
done
```
---
## Resolved (this session)
- ✅ **Single-word org names** broke templates B + C (empty leading `<em>` span).
Fixed via `splitNameAccent(name)` helper — falls back to `<em>${tail}</em>`
alone when `lead` is empty.
- ✅ **Null-phone CTA buttons** rendered `href="#"` (dead anchor) on orgs
without phone. Fixed via `ctaHref(phoneTel)` helper — degrades to
`#contact` in-page anchor so the CTA scrolls somewhere meaningful.
- ✅ **Template C contact section missing `id="contact"`** — added so the new
ctaHref fallback actually has a target.
- ✅ **Pitch modal link clutter** — 4 per-variant links collapsed into a
`<details>` disclosure (`individual variants ▾`), keeping `★ /all` as the
primary action.
## Known follow-ups
- **Pre-generate static HTML** for top-N prospects so email-link clicks
bypass pd-api entirely. ~5K orgs × 4 variants × ~6KB = ~120 MB; cache
invalidation needs a signal (e.g., prospect data changed → re-render).
- **More template diversity** — re-query 21st.dev with explicit terms
(brutalist / editorial / monospace) to widen the rotation. Subagent
flagged the first query returned only 3 designs in the
PulseFit-adjacent space.
- **Pull a Figma source.** The Figma MCP wasn't used yet; one direction
pulled directly from a Figma file would round out the 21st.dev port and
honor the literal "use 21stdev components and figma" ask.
- **Email body copy** still says "I'll set up the real domain and put it
live" — pre-existing language not from this round, but worth updating
if pitch flow ever becomes automated (Steve sets up domains, not the
agent).
---
*Last updated: 2026-05-01 — initial backfill, then second pass after rigorous
four-horsemen test added template E (editorial-brutalist), patched P1
single-word + null-phone fixes, collapsed modal link clutter into disclosure.*