Lovable deploy
Skill mkreindel/seo-toolkit-template/.claude/skills/lovable-deploy
Claude Code workspace template for running consistent SEO across a portfolio of managed websites.
npx -y skills add mkreindel/seo-toolkit-template --skill lovable-deployAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Drive a Lovable round-trip via Chrome DevTools MCP. Reads a prepared prompt from the site's _drafts/ folder, navigates the user's local Chrome to the right Lovable project, pastes the prompt atomically (DataTransfer-based, bypassing Cmd+V UTF-8 mangling), waits for Lovable to finish building, captures the diff, runs heuristic verification, and either queues for human approval (default mode) or clicks Publish → Update directly (--auto-approve, subject to eligibility rules). After publish, runs curl-based verification on the live URL. Interactive-only — requires a local Claude session with Chrome MCP connected. Use when the user types `/lovable-deploy` or asks to ship a queued Lovable deploy.
SKILL.md
23.7 KB, as published. Nobody here has run it
/lovable-deploy — Lovable round-trip driver
Usage
/lovable-deploy {site-slug} {prompt-source} [--auto-approve]
| Argument | Required | Description |
|---|---|---|
{site-slug} | yes | Site folder name under sites/ (e.g., site-a, site-b). Must have Lovable project ID field in site-info.md. |
{prompt-source} | yes | Either (a) a slug resolving to sites/{site}/_drafts/{slug}/lovable-prompt.md, or (b) an absolute path to any prompt file. |
--auto-approve | no | Opt-in. When set, skill clicks Publish → Update after build succeeds. Subject to eligibility rules — see "Auto-approve eligibility" below. |
Examples:
/lovable-deploy site-a 2026-05-22-blog-quick-wins-q3
/lovable-deploy site-b services-tapas-priced-menus --auto-approve
/lovable-deploy site-a /absolute/path/to/prompt.md
Why this skill exists
The weekly drafter cron writes Lovable-ready prompts to _drafts/ but cannot drive the user's local Chrome to publish them (remote sessions don't have access to local MCP servers). Manually pasting each draft into Lovable, waiting for the build, reviewing the diff, and clicking Publish takes 15-20 minutes of attention. This skill collapses that to 2-4 minutes of Claude-driven work that the user can supervise while doing other things.
When Anthropic ships cross-session MCP support, this skill becomes fully autonomous (cron fires it directly). For now, the cron queues _inbox/lovable-deploy-pending-*.md items; the user runs the skill from their local session to ship them.
Preconditions (verify before any action)
Run these checks before touching the browser or filesystem. If any fail, abort with a clear error message.
-
Local Claude session. This skill requires Chrome DevTools MCP. Confirm by calling
mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_pages. If the tool errors or returns no pages, you are likely in a remote session — abort with "lovable-deploy requires a local Claude session with Chrome MCP connected." -
Site folder exists.
sites/{site-slug}/must exist on disk. If not, abort with "Site {site-slug} not found in sites/." -
Lovable project ID present. Parse
sites/{site-slug}/site-info.md, find the line matching**Lovable project ID:** \([a-f0-9-]+)``. If absent or empty, abort with "Site {site-slug} has no Lovable project ID set in site-info.md. Add it before running /lovable-deploy." -
Prompt file resolves. If
{prompt-source}is an absolute path, check it exists. Otherwise resolve tosites/{site-slug}/_drafts/{prompt-source}/lovable-prompt.mdand check that exists. If neither path resolves, abort with "Prompt file not found at {expected path}." -
Prompt size. Read the prompt file and check character count. If > 49,500 chars, abort with "Prompt exceeds Lovable's 49,950 char cap (with safety margin). Split into multiple prompts."
-
3-strike check. If
_audit-log/runs.jsonlhas 3 consecutiveredentries forlovable-deploy.{site-slug}in the most recent runs, abort with "Site {site} has 3 consecutive failed lovable-deploy runs. Investigate before proceeding. See _audit-log/failures.json." -
Chrome signed into Lovable. Take a snapshot of
https://lovable.dev/— if the snapshot shows a login form (text containing "Sign in" or "Log in" with email/password inputs), abort with "Chrome is not signed into Lovable. Sign in manually, then re-run /lovable-deploy."
Step 1 — Resolve Lovable project URL
- From site-info.md, extract the Lovable project ID (already loaded during precondition 3).
- Construct:
https://lovable.dev/projects/{lovable-project-id} - This URL is the navigation target for Step 2.
Step 2 — Navigate Chrome to the Lovable project
- Call
mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_pagesto see open tabs. - If any open page's URL contains the Lovable project UUID, select it via
select_pagewithbringToFront: true. - Otherwise, call
navigate_pagewithtype: "url",url: <constructed URL>. Wait for navigation to complete (default timeout sufficient). - After navigation, call
list_pagesagain to confirm the URL contains the expected project UUID. If not, abort with "Failed to navigate to Lovable project {url}."
Step 3 — Paste prompt atomically into Lovable's chat input
The atomic-paste pattern bypasses two known Lovable quirks: (a) Cmd+V mangles UTF-8 em-dashes ([[lovable-paste-encoding-workaround]]); (b) multi-line input typed key-by-key fragments into the Queue via Enter ([[lovable-send-atomic]]).
- Take a snapshot via
take_snapshot. Save the result for selector matching. - Find the chat input element in the snapshot. Look for the textbox role at the bottom of the page, typically with a
nameorplaceholdercontaining one of:"Send a message","Message","Ask","Chat","Type". If multiple textboxes exist, pick the one visually at the bottom of the conversation panel (last in document order is a reasonable heuristic). - Record the chat input's
uidfrom the snapshot. - If no chat input found, escalate: take a screenshot via
take_screenshot, write it + the snapshot tosites/{site}/_inbox/lovable-deploy-failed-{slug}-{date}.mdwith explanation "could not locate Lovable chat input via accessibility tree", and abort. - Read the prompt file contents.
- Use
evaluate_scriptto dispatch a paste event on the chat input element. The args array passes the input'suidand the prompt text:
async function paste(element) {
const text = arguments[1];
// Method 1: DataTransfer paste event (works for most textarea/contenteditable)
const dt = new DataTransfer();
dt.setData('text/plain', text);
const pasteEvent = new ClipboardEvent('paste', {
clipboardData: dt,
bubbles: true,
cancelable: true,
});
element.focus();
const accepted = element.dispatchEvent(pasteEvent);
// Method 2: fallback for inputs that didn't intercept paste
if (accepted) {
if (element.value !== undefined && element.value.length === 0) {
element.value = text;
element.dispatchEvent(new Event('input', { bubbles: true }));
} else if (element.isContentEditable && element.textContent.length === 0) {
document.execCommand('insertText', false, text);
}
}
return {
pasted_length: text.length,
element_value_length: element.value !== undefined ? element.value.length : element.textContent.length,
focused: document.activeElement === element,
};
}
- Verify paste succeeded: take another snapshot, find the chat input, confirm it now contains text starting with the first ~80 chars of the prompt. If verification fails, retry once. If still failing after retry, escalate (Step 4 below) — DO NOT send.
Step 4 — Send the prompt
- The chat input has focus (from Step 3).
- Use
press_keywith the chat input'suidandkey: "Enter". - Wait 3 seconds.
- Take a snapshot. Find the most recent user-role message in the conversation log. Confirm its content matches the first ~80 chars of the prompt. If the prompt isn't in the conversation log, the send didn't work — escalate.
Step 5 — Wait for Lovable to finish building
Lovable's build cycle ranges from 30 seconds to 15+ minutes depending on prompt size and complexity. Detect completion via these signals (in order of preference):
-
Chat input re-enabled. While Lovable is processing, the chat input is typically disabled (
aria-disabled="true"ordisabledattribute). When done, it re-enables. Usewait_forwith a 20-minute timeout, watching for the chat input's accessibility-tree disabled-state to flip back to enabled. -
Assistant message appears. After Lovable's reply lands, the conversation log gains a new message with
role=articleorrole=groupcontaining assistant-attributable content (often a code block, diff, or "✅" sentinel).
Add a 5-second settle window after detecting either signal to let any streaming finish.
If the 20-minute timeout fires:
- Take a snapshot + screenshot for diagnosis
- Write
sites/{site}/_inbox/lovable-deploy-timeout-{slug}-{date}.mdwith the snapshot + screenshot - Append a
redentry to_audit-log/runs.jsonl - Abort. DO NOT click Publish.
Step 6 — Capture Lovable's reply (the diff)
- Take a snapshot.
- Find the latest assistant message in the conversation log (typically the last message bubble after your user-role message).
- Use
evaluate_scriptwith the message'suidto extract the full text content:
function getMessageText(messageEl) {
return messageEl.innerText || messageEl.textContent || '';
}
- Save the captured text in a working variable. Do NOT persist to disk yet — Step 8's approval gate decides whether to keep it.
Step 7 — Heuristic verification of the reply
Lovable's replies have a recognizable shape when builds succeed. Scan the captured text for these signals.
Success signals (need at least one):
- The literal string
✅ - The string
successfully(case-insensitive) - The string
complete(case-insensitive) - A
dist/listing (Lovable typically shows the build output) - The string
awaitingorready for approval(Lovable's standard closing) - The string
buildfollowed bygreenorpassedorsuccess
Failure signals (any one means STOP):
- The literal string
❌ - The string
errorprominently (count: more than 2 occurrences indicates real errors, not just mentions) - Stack traces (regex:
at .+\.(ts|tsx|js|jsx):\d+) - The string
failed to buildorbuild failed - The string
could notorunable todescribing the requested change
Decision matrix:
| Success signal? | Failure signal? | Action |
|---|---|---|
| yes | no | proceed to Step 8 |
| yes | yes | escalate — ambiguous reply, do NOT auto-publish |
| no | yes | escalate — clear failure |
| no | no | escalate — atypical reply, manual review needed |
When escalating, write sites/{site}/_inbox/lovable-deploy-failed-{slug}-{date}.md with the full captured reply + a header describing which signals were found.
Step 8 — Approval gate
Mode A: Default (no --auto-approve flag)
- Construct paths:
slug = derive from prompt-source filename, sanitize (lowercase, hyphens only)today = YYYY-MM-DD in user's timezoneinbox_path = sites/{site}/_inbox/lovable-deploy-pending-{slug}-{today}.md
- Write
inbox_pathwith this structure:
# Lovable deploy pending — {site} — {slug}
**Date:** {YYYY-MM-DD HH:MM TZ}
**Prompt source:** {resolved-path-to-prompt}
**Lovable project:** {lovable-project-url}
**Build verification:** {success signals matched, e.g., "✅ + dist/ listing + awaiting"}
**Status:** Awaiting human review.
## Lovable's reply (full diff + build output)
{captured reply text, verbatim, fenced as a markdown code block}
## To ship this deploy
**Option 1 — Autonomous (after review):**
/lovable-deploy {site} {slug} --auto-approve
This re-runs the skill in auto-approve mode. Eligibility is re-checked.
**Option 2 — Manual:**
1. Open the Lovable project: {lovable-project-url}
2. Click `Publish → Update` in the UI
3. After deploy confirms, optionally run post-deploy verification manually
## To reject this deploy
Delete this inbox file AND the corresponding `_drafts/{slug}/` folder.
- Append a
pendingentry to_audit-log/runs.jsonlvia the audit-log helper:
import { recordSuccess } from '../scripts/lib/audit-log.mjs';
recordSuccess('lovable-deploy', { site, slug, status: 'pending', inbox_path });
- Report the inbox path to the user:
✅ Lovable deploy queued for human review:
sites/{site}/_inbox/lovable-deploy-pending-{slug}-{date}.md
To ship: re-run with --auto-approve OR click Publish in Lovable manually.
- Skill ends. The Chrome tab remains open for the user's convenience.
Mode B: --auto-approve mode
Pre-check eligibility BEFORE clicking anything. If any rule fails, fall back to Mode A with a note explaining which rule failed.
Eligibility rules — all must pass:
- Prompt size: prompt length < 30,000 chars (more aggressive cap than the absolute 49,500 limit — auto-approve only for small changes).
- Site green-run floor: site has 4+ consecutive
greenentries in_audit-log/runs.jsonlforlovable-deploy.{site}, counting back from the most recent. Check via the audit-log helper. - Content-type safety:
- Prompt MUST NOT contain literal references to:
middleware.ts,vercel.json(config changes are dangerous to auto-approve) - Prompt MUST NOT contain
schemaorJSON-LDmodification instructions
- Prompt MUST NOT contain literal references to:
- Site Bsf-specific: if
site == "site-b", the prompt MUST NOT modify any user-visible text (per [[site-b-founder-voice-constraint]] memory). Heuristic check: scan prompt for words likereplace,change,rewrite,updatepaired with<h1>,<p>,text,copy,headline. If any match, fail eligibility.
If eligibility passes, execute the publish flow:
-
Take a snapshot of the current Lovable page.
-
Find the
Publishbutton. Search the snapshot forrole=buttonwhosename(case-insensitive) starts with "Publish" or equals "Publish". Record itsuid.- If not found: escalate. Lovable's UI may have shifted; do NOT guess at other buttons.
-
Click via
mcp__plugin_chrome-devtools-mcp_chrome-devtools__clickwith the button'suid. -
Wait 2 seconds for the publish panel to render. Take a snapshot.
-
Find the
Updatebutton within the publish panel. Search forrole=buttonwhosenamestarts with "Update" (handles "Update", "Update production", "Update Production", etc.).- If not found: escalate.
-
Click it.
-
Poll for deploy confirmation. Every 10 seconds, take a snapshot and check for:
- Text containing "Published" or "Live" or "Deployed"
- A timestamp adjacent to a published/deployed status
- The publish button reverting from "Publishing..." or "Updating..." to "Publish"
Timeout: 3 minutes. If timeout, escalate — Vercel deploy may still be in progress, but we won't auto-verify.
-
If success detected, proceed to Step 9 (post-deploy verification).
-
After Step 9 succeeds, write
sites/{site}/_inbox/lovable-deploy-shipped-{slug}-{date}.md(see template in Step 9).
If eligibility FAILS, write to inbox with explanation:
Same as Mode A's inbox_path, but with an additional section:
## Auto-approve declined
`--auto-approve` was requested but failed eligibility:
- {failed rule 1, e.g., "Site site-a has 2 prior green runs, needs 4 for auto-approve"}
- {failed rule 2, if applicable}
Default-safe mode kicked in. Review the diff above; re-run with `--auto-approve` once eligibility is met, OR proceed manually.
Step 9 — Post-deploy verification (curl-based + interactivity click-test)
After deploy confirmation in Lovable's UI (Mode B), or as an optional check in Mode A (only if the user already clicked Publish manually):
9a — Content verification (curl-based)
- Determine the live URL pattern from the prompt. Most prompts include the target URL explicitly. If not, derive from the prompt slug + the site's
Live URL pattern for blog postsorLive URL pattern for service pages(from site-info.md). - Wait 60 seconds for Vercel's edge propagation.
curl -sL <live_url>— capture response status + body.- Verify:
- HTTP 200 (or HTTP 308 followed to 200 — curl
-Lhandles redirects) - Body contains the expected
<h1>text (extract from prompt; if absent, skip this check with a warning) - Body contains the expected
<title>text (extract from prompt; if absent, skip)
- HTTP 200 (or HTTP 308 followed to 200 — curl
- For bilingual sites (site-a, site-b): also curl the sister-language URL and confirm 200 + correct language-specific content. The sister URL is derived from the prompt's hreflang declaration or the site's URL pattern.
9b — Interactivity click-test (CRITICAL — added 2026-05-17 after P0.1.1 postmortem)
Curl-based content checks are NOT sufficient on SSG sites. A site can have correct SSG-rendered HTML (passes all curl checks) but completely broken hydration (no event handlers attached, all interactive elements inert). This happened on example.com 2026-05-17: the P0.1 SSG migration introduced a circular dependency between vendor and ui chunks, throwing React.forwardRef undefined on hydration. The page rendered, PSI scored 91-95, all content checks passed — but every button, link, and form was dead. Bug shipped at 4pm and wasn't discovered until 8pm when a human tried to click something.
This Step 9b is the missing safety net. Run it on EVERY post-deploy verification for SSG sites (site-a, site-b, and any future SSG site):
- Navigate to the deployed URL via Chrome MCP
navigate_page. - Wait 4-5 seconds (let hydration complete fully — measured against a hydration-completion signal like the chat widget loading, or simply a fixed
wait_forwith a benign visible text). - Take an accessibility snapshot via
take_snapshot. - Use
evaluate_scriptto check for React fiber attachment:
() => {
const button = document.querySelector('button');
if (!button) return { status: 'no-buttons-on-page', reactFiber: null };
const fiberKeys = Object.keys(button).filter(k => k.startsWith('__react'));
return {
status: 'ok',
reactFiberAttached: fiberKeys.length > 0,
fiberKeys,
hasClickHandler: typeof button.onclick === 'function',
consoleErrorCount: 0 // populated by list_console_messages separately
};
}
- Run
list_console_messageswithtypes: ["error"]— assert zero errors. - If hydration looks healthy (fiberKeys non-empty, no console errors), do an actual interaction click as final proof:
- Find a low-risk reversible interactive element via snapshot. Good candidates: theme toggle (dark/light mode), language toggle (EN/ES), accordion expand, cookie consent dismiss.
- Click it via
mcp__plugin_chrome-devtools-mcp_chrome-devtools__click. - Wait 1 second, then
evaluate_scriptto verify a state change happened:- For theme toggle: check
document.documentElement.classNameflipped between empty and "dark" - For language toggle: check
<html lang>attribute changed - For accordion: check that the previously-collapsed section is now visible
- For theme toggle: check
- If state change confirmed → hydration confirmed working.
If 9b fails (no fiber attached, console errors, OR click produces no state change):
- Write
sites/{site}/_inbox/lovable-deploy-postcheck-hydration-failed-{slug}-{date}.mdwith the snapshot, console errors, and the specific assertion that failed. - Mark this as a P0 site-level outage. The deploy DID happen but the site is broken for real users.
- Append a
redentry to the audit log. - Recommend the user revert the deploy in Lovable OR draft a hotfix prompt addressing the hydration issue.
Why this matters more than 9a: content rendering is a SOLVED problem in SSG — Vite + vite-react-ssg + middleware injection produce correct HTML reliably. The hydration step is where novel bugs (chunk-splitting, lazy-load races, hydration mismatches, etc.) hide. 9a catches "did the build produce the right HTML?" 9b catches "is the site actually usable?" Both are required.
6. If verification passes, write sites/{site}/_inbox/lovable-deploy-shipped-{slug}-{date}.md:
# Lovable deploy shipped — {site} — {slug}
**Date:** {YYYY-MM-DD HH:MM TZ}
**Live URLs:**
- {primary URL}
- {sister URL if bilingual}
**Deploy duration (from /lovable-deploy start to verification):** {elapsed minutes}
**Verification:**
- HTTP {status} ✅
- H1 match ✅
- Title match ✅
- {sister URL HTTP status} ✅ (if bilingual)
## Lovable's diff (captured at submit)
{full captured reply, fenced}
- Append
greenentry to_audit-log/runs.jsonl. - Report success to the user with the live URL(s).
If verification FAILS:
- Write
sites/{site}/_inbox/lovable-deploy-postcheck-failed-{slug}-{date}.mdwith details - Append
redentry to_audit-log/runs.jsonl - Report failure with diagnostic info — the deploy DID happen, but the verification didn't pass. User needs to investigate.
Selector strategy: accessibility-tree, not CSS
This skill uses accessibility-tree snapshots (via take_snapshot) to find UI elements via ARIA roles + names. It does NOT use CSS selectors via evaluate_script's document.querySelector.
Why: Lovable's CSS classes (Tailwind utility classes, generated UUIDs) change with every Lovable redesign. Their ARIA roles + accessible names are intentionally exposed for assistive tech and rarely change. Accessibility-tree selectors survive UI redesigns; CSS selectors don't.
One exception: the atomic-paste step (Step 3) uses evaluate_script to dispatch a paste event on the chat input. This is a single DOM-direct operation, scoped to the element already located via the accessibility tree. The uid from the snapshot resolves to the actual DOM element inside evaluate_script's args — so we're still semantic-first, not CSS-first.
Fallback heuristics (in priority order, only if accessibility tree doesn't find a target):
- Snapshot text-content scanning (look for the element by visible label, not CSS class)
- Take a screenshot + take a full snapshot, persist both to
_inbox/lovable-deploy-failed-*.md, escalate to human
DO NOT write CSS selectors targeting Lovable internals (.message-text, #chat-input, etc.). They will break.
Audit log integration
Every /lovable-deploy invocation writes a row to _audit-log/runs.jsonl via scripts/lib/audit-log.mjs:
| Status | When written |
|---|---|
pending | Default mode: after the diff is captured and inbox file is written. |
green | After successful auto-publish + verification. |
red | Any escalation: precondition fail, paste fail, send fail, build timeout, heuristic verification fail, click fail, post-deploy verification fail. |
Fields recorded: timestamp, skill: "lovable-deploy", site, slug, status, elapsed_ms, inbox_path (if applicable), live_url (if shipped), error (if red).
The 3-strike contract (precondition 6) reads from this log.
Related memory entries
- [[lovable-paste-encoding-workaround]] — why DataTransfer paste, not Cmd+V
- [[lovable-send-atomic]] — why one-shot paste, never multi-line type
- [[lovable-prompt-size-limit]] — the 49,950 char cap and how to trim
- [[chrome-browser-pick]] — use the site-a Chrome browser, not Personal Chrome
- [[lovable-capa0-prompt-pattern]] — invisible-metadata patches that may auto-approve safely
- [[site-b-founder-voice-constraint]] — why site-b visible-text changes never auto-approve
- [[html-comment-tag-footgun]] — preflight check for migration-class patches (verify no literal tag names in HTML comments)
Out of scope for v1
- Driving non-Lovable platforms (Webflow, Squarespace, Shopify) — different code paths per platform; future siblings like
/webflow-deploywould follow the same contract. - Pre-build TypeScript / linter checks — Lovable does these internally and reports in chat; this skill just reads the report.
- Verifying that Lovable's diff actually matches the prompt intent (semantic diff) — out of scope; default mode's human review catches this.
- Multi-prompt patches that span > 49,500 chars total — user must split manually.