# /landing

> Build a brand-accurate, direct-response landing page for a ACME Agency client on the Paradox Landing System (Cloudflare Pages + shared Supabase + edge A/B + the admin console).


# /landing — brand-accurate direct-response landers on the Paradox Landing System

Build client landing pages in our own stack (NOT Lovable, NOT the `/website` SiteGround
flow). Output is a **direct-response landing page** — one offer, one CTA, built to convert
paid traffic — deployed to **Cloudflare Pages**, capturing leads through the shared
**Supabase + `submit-lead`** pipeline, A/B-tested at the edge, and managed from the
**console** (`your-domain.example`). System lives in `shared/landing/`
(read `shared/landing/README.md`).

This is the BUILD step. The resolver, `submit-lead`, deploy tooling, and console already
exist — this skill makes the *pages themselves* good: on-brand and direct-response.

---

## The two hard rules

1. **Brand FIRST — never invent colors.** Read the client's real brand before designing.
   (Incident 2026-06-07: a lander was built in generic teal when ACME Agency's brand is amber
   gold #D59C44 — because the brand doc wasn't read. Don't repeat.)
2. **Direct-response, not a brand website.** One offer, one job, one CTA, no nav bar.
   Lead with the offer. Message-match the ad. Specific > pretty.

---

## Step 0 — Apply learned rules (read FIRST, every build)

Read **[`.claude/skills/landing/LEARNED_RULES.md`](LEARNED_RULES.md)** and satisfy every
rule before you write any markup. Each rule is a mistake a human already had to fix on a
past lander (mobile cramming, iOS blank boxes, hero copy that ran long, conflict markers
shipped live, lead-forwarding dropped on redeploy). Applying them up front is the whole
point of the self-improvement loop — the file grows as the team edits real landers
(`shared/self_improve/engine.mjs` proposes new rules from `audit_log` edits; a human
approves them in). If a rule ever conflicts with an explicit client brief, the brief wins.

---

## Step 1 — Resolve the client + brand (do this before any design)

1. `ACME Agency/clients/clients.json` → the client entry: `drive_folder_id`,
   `reference_assets` (logo/doctor/photo Drive URLs), `website`, `ghl_location_id`,
   `ghl_api_key_env`, `slack_channel`, `vertical`.
2. `ACME Agency/clients/<Client>/brand-dna.md` — the canonical brand doc: exact hex
   colors, fonts, logo, photography direction, voice, **and the real offer/USP**. Read it
   fully. (Also check the client's Drive folder for a cached `brand-dna.md`.)
3. Pull real assets into the project: copy the local `logo.svg` if present; download key
   photos (`curl -sL "https://drive.google.com/uc?export=download&id=<id>" -o ...`),
   resize big ones with sharp. Use the client's OWN logo + photos, not stock.

**If NO brand exists** for the client → define it the `/static-ad-generator` Phase-1 way:
scrape the live site with Firecrawl + read a Playwright screenshot with vision for the
EXACT hex colors/fonts, inspect their Drive designs, write `brand-dna.md` locally + to
Drive. Confirm the palette with the user before building. (`/reference-ads` is the
higher-fidelity path when the client has strong existing designs.)

## Step 2 — Lock the offer + copy (direct-response)

Identify the **offer** (from `brand-dna.md` "Product/Service Details" or the user's brief).
If the user supplied copy → use it verbatim (HR/BS: keep š/ć/č/ž/đ + emojis, see
`ACME Agency/CLAUDE.md`). If not → write it (or call the `copywriter` agent) to the
**DR blueprint** below. Apply the Hormozi value equation to make the offer a no-brainer:
**Value = (Dream outcome × Perceived likelihood) ÷ (Time delay × Effort)** — maximize the
top (result + proof + guarantee), minimize the bottom (speed + done-for-you).

**DR landing-page section order (the blueprint):**
1. Hook headline (above fold) — echoes the ad's promise; specific result + number/timeframe.
2. Subheadline — who it's for + mechanism/timeframe.
3. **CTA + form above the fold** — minimal fields (name + phone; email optional). Benefit-led button.
4. Pain agitation — name the problem in the prospect's words.
5. Mechanism — the named "why this works / why now" (one per page).
6. Offer / value stack — components, each with its benefit; lead with the dream outcome.
7. Proof — testimonials, numbers, authority (doctor/founder), ratings — near the CTA.
8. Risk reversal — "first consult free / no obligation".
9. Urgency — real scarcity only.
10. FAQ — 4-6, each an objection answered.
11. Repeat CTA band.

**DR rules:** one CTA repeated, **no nav bar / no outbound links**, lead with the offer,
message-match the traffic source, specificity over fluff, emotion-first then proof,
conversational HR/BS (no AI-tells, no em-dashes, read-aloud test).

## Step 3 — Design within the brand

- Query the design engine for structure/feel:
  `node .claude/skills/ui-ux-pro-max/scripts/search.mjs "landing page <vertical> <tone>" --domain landing`
- Invoke the `frontend-design` skill while writing the code — but **constrain palette,
  fonts, and logo to the client's `brand-dna.md`** (brand fidelity beats novelty here).
- Project layout under `websites/<client>-<offer>/`: `assets/styles.css`, `assets/form.js`
  (posts to `/api/submit-lead` with `variant_path` + tracking), `assets/img/`, `v1/index.html`
  (control), `v2/index.html` (variant), root `index.html` (= control, fail-safe), `functions/`
  (synced from `shared/landing/functions/` by the deploy script).
- **Brand fonts + colors are mandatory, not optional.** Embed the client's REAL fonts
  (probe their live site for the actual `@font-face` files + computed `font-family`, download
  the `.ttf/.woff2` into `assets/fonts/`, declare + preload them, point `--serif`/`--sans` at
  them) and use their EXACT palette. Never ship a free look-alike serif — the client catches it
  on sight. See `LEARNED_RULES.md` `[brand]`.
- **Variants differ on ONE variable** (usually the hero hook) — single-variable test.
- **Screenshot-ACME Agencyw before shipping:** deploy to the test URL, then Playwright-screenshot
  `/v1` (and `/v2`) and read the PNG with vision; fix until it's genuinely on-brand. Don't guess.
- **Mobile gate (REQUIRED before deploy):** run `node shared/landing/scripts/mobile_audit.mjs
  websites/<dir>` — it audits every `vN/` at 390px (no horizontal scroll/overflow, tap targets
  ≥44px, text ≥13px, section padding). It exits non-zero on any hard failure; fix until CLEAN.
  Every new variant must pass it too. This is the standing "pixel-perfect mobile" standard.

## Step 4 — Deploy + register

**Check `agency-os_SANDBOX` first** — it decides whether you deploy directly or enqueue.

### Sandboxed colleague session (`agency-os_SANDBOX=1`) — NEVER a proposal
You can't push or create CF/Supabase resources from a sandbox, so hand the deploy to the
privileged runner (cron as faris). For a **brand-new lander**, enqueue a new-client request —
it seeds Supabase + creates the CF Pages project + deploys + pushes, **live in ~1 min**:
```bash
node shared/landing/scripts/<id>.mjs \
  --slug <kebab> --name "<Display Name>" --project <cf-project> \
  --site-dir websites/<dir> --page-name "<A/B test name>" \
  --v1-name "Control" --v2-name "<variant hook>" [--split 50] [--ghl-webhook <url>]
```
It embeds every file under `--site-dir`, so the runner gets your exact source. **Do NOT file an
improvement proposal for a landing build** — this path exists precisely to remove that wait.
(Changing an EXISTING lander is `/landing-edit`, not this skill.)

### Direct (Faris / non-sandbox) — seed then deploy
```bash
# new client: create the Supabase clients/pages/variants rows (generic, idempotent)
node shared/landing/scripts/seed_client.mjs \
  --slug <slug> --name "<Name>" --root-domain <cf-project>.pages.dev \
  --page-name "<A/B test name>" --v1-name "Control" --v2-name "<variant hook>"
# then deploy: functions + pages, CF env (Supabase + CLIENT_SLUG [+ GHL]), writes live_url, pushes source
node shared/landing/scripts/deploy_client.mjs --site websites/<dir> --project <cf-project> --client-slug <slug> [--ghl-webhook <url> | --ghl-key-env GHL_<CLIENT>_API_KEY]
```
- **Source backup is automatic:** the deploy auto-commits + pushes the `websites/` umbrella repo (`your-org/agency-websites`, your server mirror) via `shared/website/websites_backup.mjs`. Edits without a deploy → commit + push inside `websites/` yourself.
- An extra test on an existing client can also be created from the **console** ("Novi test").
- **GHL forwarding:** prefer the client's GHL **inbound-webhook URL** (`--ghl-webhook` →
  `GHL_WEBHOOK_URL` binding, or paste it in the console's client view). API-key fallback exists.
  Leave it off during smoke tests so you never write to a client's live CRM uninvited.
- **Domain:** deploy to `<project>.pages.dev` first, verify, then attach the client's domain
  in Cloudflare Pages + have them add the CNAME.
- **Zero the stats before handoff (mandatory):** your build/smoke testing (curl hits, test
  form submits, screenshot loads) all count as visits/leads. Before the client sees the
  dashboard or real ads go live, wipe them so the client starts at zero — only real traffic
  counts: `node shared/landing/scripts/reset_stats.mjs <slug>` (or the console's Reset button).
  A new client must NEVER carry our test data. (Incident 2026-06-09: ACME Agency showed 109 test
  visits in the console on first view.)

## When to trigger

- "build a landing page / lander / funnel page for <client>"
- "napravi landing / novi lander za <klijent>"
- A new client that needs a lead-gen landing page (instead of Lovable)
- "build a variant of <client>'s lander" (then register the A/B test in the console)

## Guardrails

- **Read `brand-dna.md` before designing.** No generic palettes. If missing, define brand first + confirm.
- **It's a direct-response lander, not a website** — one offer, one CTA, no nav.
- **Never deploy a client's real domain without explicit instruction** — test on `*.pages.dev` first.
- **HR/BS copy:** diacritics intact, no em-dashes, no machine-translation feel.
- **Don't write to a client's live GHL** during testing — keep GHL forwarding off until the user OKs the webhook URL.
- **Zero the stats before handoff** — wipe smoke-test visits/leads (`node shared/landing/scripts/reset_stats.mjs <slug>`). A client must start at zero; only real traffic counts.
