[ LANDING & WEB ]
/landing-edit
Recognize and apply ANY change to a live landing page on the Paradox Landing System (Cloudflare Pages + shared Supabase + edge A/B) and deploy it in ~1 minute — no command to type, no approval, no Faris in the loop, and
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-edit — the single path for editing a live lander (no proposal, no approval gate)
Anyone on the team changes a client's landing page from their own Claude Code and it goes live in ~1 minute — without routing through Faris and without ever becoming an improvement proposal. This is the lightweight EDIT path that the heavyweight /landing build skill and the read-only console never provided, and it is the structural reason copy/CSS tweaks used to pile up on Faris's desk (or sit in the 17:30 improver queue).
Auto-trigger — you do not wait for the command
If someone (in any session, sandboxed or not) asks to change something on a client's already deployed landing page, this skill is the path — invoke it yourself, you do not need the user to type /landing-edit. "Promijeni CTA na ACME Agency landingu", "fix the spacing on the ACME Agency header", "dodaj /hvala stranicu", "make the testimonials a slider" → all of this is /landing-edit.
Never write an improvement proposal for landing-page work. Landing edits are explicitly exempt from the sandbox "write a proposal when you can't edit" rule. The deploy creds live on the privileged runner, not in the sandbox — so a sandboxed session ENQUEUES the change (Step 4a) and the runner deploys it in ~1 min. A proposal would just make a live edit wait a day for the improver, which is exactly the failure mode this skill exists to kill.
Scope — any change to an EXISTING lander
✅ In scope (apply + deploy):
- Copy: headlines, subheads, body, CTA text, phone, prices, testimonials, FAQ, meta tags.
- Images: swap an existing image, add a new one.
- CSS / layout / styling: spacing, alignment, colors-within-brand, responsive fixes, show/hide.
- Structure: add / remove / reorder a section, add a sub-page (e.g.
/v1/hvala/), small markup changes. - Small JS behavior: e.g. redirect-on-submit, toggle, smooth-scroll — keep it scoped and tested.
↪ Hand to /landing instead (a BUILD, not an edit — and /landing also deploys directly, never a proposal):
- A brand-new client landing page from scratch.
- A brand-new A/B variant (a whole new
/vNpage added to a running test). - A full redesign / re-brand (new color system, new font stack, new visual language).
When unsure, prefer editing here — the bar for kicking to /landing is "this is a new page/variant or a ground-up redesign," not "this is bigger than a copy swap."
Step 0 — Resolve the client, the live page, and the test state
node shared/landing/scripts/landing_resolve.mjs --client "<name|slug>" # human summary
node shared/landing/scripts/landing_resolve.mjs --client "<name|slug>" --json # for parsing
Reads Supabase (same source of truth as the console) and returns:
slug,live_url,custom_domain,cf_project(derived from live_url),ghl_webhook_url— REQUIRED for the redeploy (see the footgun below),site_dir— the CF deploy root (the dir that holdsv1//v2/; for dual-build clients
like ACME Agency-cfo this is websites/<dir>/landing-deploy, NOT the parent) — deploy with --site = this — plus matched_dir (the client dir where the editable src/ lives) + ranked candidates,
- every page + variant with its
target_path, which is thecontrol, andmid_test.
Stop and tell the user if:
- No client matches, or the match is ambiguous (re-run with the exact slug).
site_diris NOT FOUND → thewebsites/repo isn't present/current on this machine.
Pull it first (your server mirror your-org/agency-websites); editing a stale copy is exactly the drift that puts source out of sync with live. Do not guess a dir.
- The resolver returns no
cf_project(the client is a SiteGround-only site, not on this
system) → use /website deploy instead.
Step 1 — Pick the right file(s)
A variant's target_path maps to a file under the resolver's site_dir (the deploy root): /v1 → <site_dir>/v1/index.html, /v2 → <site_dir>/v2/index.html. For most clients site_dir == the client dir. For dual-build clients (e.g. ACME Agency-cfo) the resolver returns the self-contained CF build dir websites/<dir>/landing-deploy as site_dir, and the editable source dir as matched_dir. Those deploy files are self-contained (CSS inlined, images as data-URIs, a bespoke submit-lead <script>). For those:
- Edit
<site_dir>/v1/index.html(the deployed file) AND mirror the same change into the editable
source <matched_dir>/src/… — text/CSS swaps via unique-string Edit; do NOT touch the inlined image data-URIs or the platform submit-lead script.
- The root
index.html(when present) is the fail-safe copy of the control — if you edit the
control, edit the root mirror too.
Default target = the control variant (control_path). If the user names a variant, edit that one.
Step 2 — ⚠ A/B safety check (do not skip)
If the resolver flags mid_test: true for the page you're about to touch:
- Editing a variant mid-test changes what live traffic sees and **contaminates the running
experiment**. Surface this before proceeding — name the test, ask whether to (a) edit the control only, (b) edit the variant anyway, or (c) wait.
- Editing the control mid-test still shifts the baseline — warn, then proceed if confirmed.
- Either way the edit is logged to
audit_log(Step 4) so the test record shows it.
No live test (mid_test: false) → just proceed.
Step 3 — Make the edit
Edit the HTML/CSS/JS in websites/<dir>/… directly. Rules:
- Apply the learned rules first. Skim
.claude/skills/landing/LEARNED_RULES.md
— the same mistakes (mobile cramming, iOS blank boxes, conflict markers, long hero copy) must not be reintroduced by an edit. Your edit summary (logged to audit_log) is also the signal the self-improvement engine learns the next rule from, so write it specifically ("fix steps cramming on mobile", not "css fix").
- Croatian/Bosnian copy: keep all š/ć/č/ž/đ diacritics and emojis, no em-dashes, no
machine-translation feel — read it aloud (see ACME Agency/CLAUDE.md). Use the copywriter agent for anything more than a literal swap the user dictated.
- Verbatim when dictated: exact text the user gave → use it exactly.
- CSS / layout: stay within the brand tokens already in the stylesheet (don't invent a new
palette/font — that's a /landing rebrand). For a visual/layout change, run the screenshot self-ACME Agencyw loop before deploying if the site supports it (node shared/website/screenshot_loop.mjs --site <slug>), then eyeball the PNGs.
- New section / sub-page: match the existing structure, classes, and tokens. A new
self-contained sub-page (e.g. /hvala/) inlines its own CSS + logos like the rest of the deploy dir.
- Images: new asset into
websites/<dir>/assets/…, point the existing<img>/background
at it, keep dimensions sane (resize big files with sharp).
- Keep changes scoped to what was asked. Don't reflow unrelated layout or "improve" other copy.
--dry-run: make the edit locally, show the diff, and STOP before deploying.
Step 4 — Publish (the path depends on WHERE you're running)
Deploy needs the Cloudflare token AND push creds, which by design never live in a sandbox. Check echo $agency-os_SANDBOX (or the env):
4a — Sandboxed colleague session (agency-os_SANDBOX=1) → ENQUEUE (never a proposal)
You can edit websites/ files but cannot deploy/push. Hand the edit to the privileged runner by enqueuing it (writes to the shared queue /var/tmp/landing-deploy-queue, which the sandbox guard allows — not a protected path, no push):
node shared/landing/scripts/landing_edit_enqueue.mjs \
--slug <slug> --project <cf_project> --site-dir websites/<dir> \
--ghl-webhook "<ghl_webhook_url or omit>" \
--variant "<path e.g. /v1>" [--mid-test] \
--summary "<one line of what changed>" \
--file websites/<dir>/v1/index.html [--file <every file you edited/created>]
Pass EVERY file you changed or created (root index.html mirror, the self-contained deploy file, any new sub-page, any new image). The runner (landing_deploy_runner.mjs, cron as faris) applies the exact contents to the canonical websites repo, deploys, commits + pushes, and logs to audit_log — typically live within ~1 min. Tell the user it's queued and will be live shortly (no approval step). Result lands in /var/tmp/landing-deploy-queue/processed/<id>.result.json (or failed/ with the error).
4b — Faris / any non-sandboxed session → DEPLOY DIRECTLY
# ALWAYS re-pass the GHL webhook if the client has one (footgun below).
node shared/landing/scripts/deploy_client.mjs \
--site websites/<dir> --project <cf_project> --client-slug <slug> \
--ghl-webhook "<ghl_webhook_url from resolver>"
The deploy script also auto-commits + pushes the websites/ umbrella repo (websites_backup.mjs), so deploy = publish + source sync in one step — the atomicity that stops "live is ahead of committed source" drift. Then log the edit:
node -e "fetch(process.env.LANDING_SUPABASE_URL.replace(/\/$/,'')+'/rest/v1/audit_log',{method:'POST',headers:{apikey:process.env.<id>,Authorization:'Bearer '+process.env.<id>,'Content-Type':'application/json'},body:JSON.stringify({action_type:'content-edit',payload:{client:'<slug>',variant:'<path>',by:'<who>',summary:'<one line>'}})}).then(r=>console.log(r.status))"
(The runner does this audit-log write itself for the enqueued path.)
Step 5 — Report
ONE line. What changed + the live URL (custom_domain if set, else live_url + route). If the page was mid-test, you may add at most ONE short clause noting it. That's it.
Do NOT: print the queue path, list the files, explain the enqueue/runner mechanism, say "will be live in ~1 min", restate the A/B warning at length, or add a closing paragraph. The user wants "done — <url>", not a status report. No Slack post — this is the colleague's own session.
Good: Eyebrow updated on ACME Agency control (v1) → live at ACME your-domain.example/v1 (logged; page is mid-test). Bad: a multi-line breakdown of what/where/why + queue paths + "bit će live za ~1 min".
Tracking is managed, not hand-edited — do not fight it (GTM and the conversion event)
/landing-edit ships the WHOLE file, so anything you paste into the HTML by hand gets clobbered the next time anyone edits that file from a clone that lacks it. This bit us twice: the GTM container (2026-06-25) and then the lead_submit_success conversion event (2026-07-01 — a teammate's "convert form to 4-step multistep" rebuild wiped a teammate's event push; the page then fired the conversion ZERO times, and a multistep form double-fires it). So both are now deploy-managed — registered per client in shared/landing/tracking.json and re-stamped into every HTML file on every deploy by deploy_client.mjs (via shared/landing/gtm_inject.mjs):
- GTM container — set it in the admin console (per-client "GTM" field → stored in
clients.gtm_id in Supabase), so colleagues can add/change it self-serve without touching the sandbox-protected tracking.json. deploy_client.mjs reads Supabase first, tracking.json ("<slug>": { "gtm_id": "GTM-XXXX" }) is the fallback. Applies on the lander's next deploy (any /landing-edit re-stamps it). GTM is the container — add GA4/Meta/other tags inside GTM's own UI, no code. (Edge-injection so a console change is instant with no deploy is the follow-up.)
- Conversion event — defaults to
lead_submit_successwhenever agtm_idis set; the stamp
is a form-agnostic bridge that fires the event ONCE on a successful submit-lead POST (kills the multistep double-fire; never fires on a 404 pACME Agencyw). Rename it with "lead_event": "<name>", or opt out with "lead_event": false. Opt out (false) when the page fires the conversion ITSELF — e.g. a form that redirects to a /hvala thank-you page and pushes the event on that page's load (redirect-safe). There the generic POST-fire bridge would double-count, so set false; the deploy then STRIPS any previously-stamped bridge, leaving the page's own single fire. (ACME Agency is opted out for exactly this reason.)
- Never hand-paste a GTM snippet or a
dataLayer.push({event:...})into the form JS — it won't
survive. Add the client to tracking.json instead. An edit that drops either is harmless now — the next deploy re-asserts it, and the stamp lands in canonical source so git carries it forward.
- The durable next phase is edge-injection (served by the Pages function from a Supabase/console
field, never in the page file). Until then, tracking.json is the source of truth.
Concurrent edits are rejected, never silently clobbered (optimistic lock)
Two people editing the same lander used to mean last deploy wins, silently — a teammate added tracking, a teammate's later whole-file deploy erased it (2026-07-01). The enqueuer now records the sha256 of the committed base each file was edited on top of; the runner compares it to the current canonical (post-reconcile = origin/main) before overwriting. If someone else deployed that file since you started, the runner rejects the whole request atomically — nothing is overwritten — and Slacks a warning naming you and the file. If you get a rejection: git pull inside websites/ (or re-run landing_resolve.mjs), reapply your change on the fresh version, and re-enqueue. This does NOT stop two people editing different landers (or different files) in parallel — only the actual collision.
The footgun — re-pass the GHL webhook every redeploy
deploy_client.mjs only sets GHL env vars when --ghl-webhook (or --ghl-key-env) is passed, and Cloudflare's project PATCH replaces the production env_var map. So a redeploy that omits the webhook silently drops lead forwarding to GHL — leads still land in Supabase but stop reaching the client's CRM. Always pass the ghl_webhook_url the resolver returns. (API-key path instead — ghl_api_key_env in clients.json — pass --ghl-key-env GHL_<CLIENT>_API_KEY.)
When NOT to trigger (hand off — still NOT a proposal)
- Build a brand-new page / new client / new A/B variant from scratch →
/landing. - Full redesign or re-brand (new color system, font stack, visual language) →
/landing. - SiteGround-only sites (no
cf_projectfrom the resolver;provider: sitegroundand no
Paradox Landing System registration) → /website deploy.
Guardrails
- Existing lander, any change → here. New page/variant/redesign →
/landing. Never a proposal. - Resolve before editing. Never guess the dir, project, or which file is the control.
- Re-pass the GHL webhook on every deploy (footgun above).
- A/B safety: warn before editing a page that's mid-test; log every edit to
audit_log. - Stay within brand: use the tokens already in the stylesheet; new palette/font =
/landing. - HR/BS copy: diacritics intact, no em-dashes, no machine-translation feel.
- No approval gate by design — that's the point. The audit log + git history are the trail.