# /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 


# /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 `/vN` page 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

```bash
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 holds `v1/`/`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 the `control`, and **`mid_test`**.

Stop and tell the user if:
- No client matches, or the match is ambiguous (re-run with the exact slug).
- `site_dir` is NOT FOUND → the `websites/` 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`](../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):

```bash
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

```bash
# 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:
```bash
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`](../../../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_success` whenever a `gtm_id` is 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_project` from the resolver; `provider: siteground` and 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.
