[ LANDING & WEB ]
/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).
ACME Agency, <id> and you@example.com mark values that are per-agency — your install fills them with YOUR clients and accounts. If a section references a helper script you don't have yet, it ships with that workflow's install./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
- 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.)
- 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 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)
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.
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.)
- Pull real assets into the project: copy the local
logo.svgif 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):
- Hook headline (above fold) — echoes the ad's promise; specific result + number/timeframe.
- Subheadline — who it's for + mechanism/timeframe.
- CTA + form above the fold — minimal fields (name + phone; email optional). Benefit-led button.
- Pain agitation — name the problem in the prospect's words.
- Mechanism — the named "why this works / why now" (one per page).
- Offer / value stack — components, each with its benefit; lead with the dream outcome.
- Proof — testimonials, numbers, authority (doctor/founder), ratings — near the CTA.
- Risk reversal — "first consult free / no obligation".
- Urgency — real scarcity only.
- FAQ — 4-6, each an objection answered.
- 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-designskill 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:
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
# 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) viashared/website/websites_backup.mjs. Edits without a deploy → commit + push insidewebsites/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.devfirst, 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.mdbefore 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.devfirst. - 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.