webcake-landing-mcp 1.0.56 → 1.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/changelog.json +14 -14
- package/dist/domains/landing/elements/content.js +14 -2
- package/dist/domains/landing/guide.js +9 -2
- package/dist/domains/landing/index.js +40 -2
- package/dist/domains/landing/instructions.js +2 -2
- package/dist/persistence/config.js +12 -3
- package/dist/persistence/html-ingest.js +460 -62
- package/dist/persistence/webcake-client.js +154 -10
- package/dist/smoke.js +257 -0
- package/dist/tools/ingest.js +30 -9
- package/dist/tools/media.js +154 -0
- package/dist/tools/persistence.js +34 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -152,6 +152,11 @@ npx -y webcake-landing-mcp login # opens the browser once, saves the token to
|
|
|
152
152
|
|
|
153
153
|
…or set `WEBCAKE_ENV` (`local` | `staging` | `prod` — fills in all base URLs) + `WEBCAKE_JWT`.
|
|
154
154
|
|
|
155
|
+
For `publish_page` to produce a **rendered** (non-blank) page, a build host is needed:
|
|
156
|
+
- `prod` preset auto-configures `https://build.webcake.io` — no extra setup.
|
|
157
|
+
- For staging/local, set `WEBCAKE_BUILD_BASE=<url>` or send the `x-webcake-build-base` header per request.
|
|
158
|
+
- Without it, `publish_page` falls back to source-only with `rendered:false` + a warning.
|
|
159
|
+
|
|
155
160
|
Everything else — the full env-var table, environment presets, per-request headers for the hosted
|
|
156
161
|
server, the `login` browser flow (+ backend contract), and how to grab a JWT by hand — lives in
|
|
157
162
|
**[docs/configuration.md](docs/configuration.md)**.
|
|
@@ -164,7 +169,7 @@ server, the `login` browser flow (+ backend contract), and how to grab a JWT by
|
|
|
164
169
|
|-------|---------------|
|
|
165
170
|
| **[Connect your IDE / claude.ai](docs/connect-mcp.md)** | Step-by-step connection for every client (npx & hosted URL), troubleshooting table. |
|
|
166
171
|
| **[Configuration](docs/configuration.md)** | Env vars, `--env` presets, browser `login`, per-request headers, getting a JWT. |
|
|
167
|
-
| **[Tools reference](docs/tools.md)** | All
|
|
172
|
+
| **[Tools reference](docs/tools.md)** | All 20 tools in detail + the step-by-step workflow + model notes. |
|
|
168
173
|
| **[Usage examples](docs/usage-examples.md)** | Three end-to-end walkthroughs: build from a brief, surgical edit, inspect a type. |
|
|
169
174
|
| **[Manual / advanced install](docs/manual-install.md)** | Shell installers, cloned builds, hand-written per-IDE config. |
|
|
170
175
|
| **[Page-element schema](docs/page-element-schema.md)** | The full element-model reference (+ [every special/event](docs/element-specials-reference.md)). |
|
|
@@ -173,13 +178,13 @@ server, the `login` browser flow (+ backend contract), and how to grab a JWT by
|
|
|
173
178
|
|
|
174
179
|
## 🧰 The tools at a glance
|
|
175
180
|
|
|
176
|
-
|
|
181
|
+
20 tools in five groups — full descriptions in **[docs/tools.md](docs/tools.md)**:
|
|
177
182
|
|
|
178
183
|
| Group | Tools | Needs |
|
|
179
184
|
|-------|-------|-------|
|
|
180
185
|
| **Reference** | `get_generation_guide` · `list_elements` · `get_element` · `get_page_schema` | nothing |
|
|
181
186
|
| **Generation** | `new_element` · `new_page_skeleton` · `validate_page` | nothing |
|
|
182
|
-
| **Media** | `search_images` (real Pexels stock photos)
|
|
187
|
+
| **Media** | `search_images` (real Pexels stock photos) · `upload_images` (re-host external images) | nothing |
|
|
183
188
|
| **Ingest** | `ingest_html` · `ingest_url` (recreate an existing page) | nothing |
|
|
184
189
|
| **Persistence** | `list_organizations` · `create_page` · `list_pages` · `find_pages` · `get_page` · `update_page` · `add_section` · `patch_page` · `publish_page` | `WEBCAKE_API_BASE` + `WEBCAKE_JWT` |
|
|
185
190
|
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.58",
|
|
4
|
+
"d": "11/06/2026",
|
|
5
|
+
"type": "Changed",
|
|
6
|
+
"en": "html-box descriptor (get_element) has been rewritten to document COMPOSITE VISUALS as the primary use case: intricate non-interactive mockups such…",
|
|
7
|
+
"vi": "Descriptor html-box (get_element) được viết lại để ghi lại COMPOSITE VISUALS là trường hợp sử dụng chính: các mockup phi tương tác phức tạp như…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.0.57",
|
|
11
|
+
"d": "11/06/2026",
|
|
12
|
+
"type": "Added",
|
|
13
|
+
"en": "New upload_images tool re-hosts up to 20 external image URLs or data: URIs as Webcake-hosted URLs (statics.pancake.vn) by downloading and uploading…",
|
|
14
|
+
"vi": "Công cụ upload_images mới tải lại tối đa 20 URL ảnh ngoài hoặc data: URI thành URL do Webcake lưu trữ (statics.pancake.vn) bằng cách tải về và…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.0.56",
|
|
4
18
|
"d": "11/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Fixed",
|
|
27
41
|
"en": "The login command on Windows now opens the connect URL correctly: cmd /c start previously split the URL at the first & character (treating it as a…",
|
|
28
42
|
"vi": "Lệnh login trên Windows nay mở URL kết nối chính xác: trước đây cmd /c start tách URL tại ký tự & đầu tiên (do hiểu & là dấu phân cách lệnh), khiến…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.0.52",
|
|
32
|
-
"d": "10/06/2026",
|
|
33
|
-
"type": "Added",
|
|
34
|
-
"en": "validate_page now errors when an element type that the renderer cannot animate (any type other than group, image-block, text-block, rectangle,…",
|
|
35
|
-
"vi": "validate_page nay báo lỗi khi một element có loại không được renderer hỗ trợ animation (chỉ group, image-block, text-block, rectangle, button,…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.51",
|
|
39
|
-
"d": "10/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "create_page, update_page, and add_section dry-run responses now include a draft_id, and all three tools now accept draft_id as an input parameter:…",
|
|
42
|
-
"vi": "Các response dry-run của create_page, update_page và add_section nay đều trả về draft_id, đồng thời cả ba công cụ đều nhận draft_id làm tham số đầu…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -3,7 +3,7 @@ export const CONTENT = [
|
|
|
3
3
|
{
|
|
4
4
|
type: "text-block", category: "content", container: false, defaultName: "Text",
|
|
5
5
|
summary: "Text. specials.text holds the content (may contain inline HTML); specials.tag sets the semantic tag. Supports template variables ({{key}}), formula mode, URL-param injection, and date formatting.",
|
|
6
|
-
useWhen: "Any headline, paragraph, label. Use tag h1/h2 for headings, p for body. Style via responsive.styles (fontSize, color, fontWeight, textAlign). ALWAYS set color to CONTRAST the band it sits on: near-black (e.g. rgba(26,32,44,1)) on light bands, near-white ONLY on a dark/image band — white text on a light band renders invisible. styles.background on a text-block = a GRADIENT TEXT FILL (emits -webkit-text-fill-color:transparent); you must also set styles['-webkitBackgroundClip']:'text' or the glyphs go invisible. The box background key is styles.backgroundTxt — use that for a colored box behind the text. NEVER set styles.background expecting a box fill.",
|
|
6
|
+
useWhen: "Any headline, paragraph, label. Use tag h1/h2 for headings, p for body. Style via responsive.styles (fontSize, color, fontWeight, textAlign). ALWAYS set color to CONTRAST the band it sits on: near-black (e.g. rgba(26,32,44,1)) on light bands, near-white ONLY on a dark/image band — white text on a light band renders invisible. styles.background on a text-block = a GRADIENT TEXT FILL (emits -webkit-text-fill-color:transparent); you must also set styles['-webkitBackgroundClip']:'text' or the glyphs go invisible. The box background key is styles.backgroundTxt — use that for a colored box behind the text. NEVER set styles.background expecting a box fill. text-block does NOT emit border-radius — for a rounded pill/badge, put a rectangle (borderRadius '13px', pill bg color) BEHIND the text-block (zIndex 2 on the text-block); the rounded shape comes from the rectangle, never from the text-block itself.",
|
|
7
7
|
keySpecials: {
|
|
8
8
|
text: "string — the visible text; may include inline HTML (<b>, <br>, <span style>…). Also supports template variables: {{today}}, {{yesterday}}, {{tomorrow}} (formatted dates), {{coupon_text}}, {{coupon_code}}, {{coupon_codes}}, {{spin_turn_left}}, {{cart_total_price}}, {{cart_subtotal}}, {{cart_shipping_fee}}, {{cart_discount_code}}, {{voucher_price_cart}}, {{cart_item}}, {{cart_bonus_item}}, {{form_error_log}}, {{total_cart}}. Dynamic form field binding: {{formId__fieldName}} substitutes a field value from a sibling form.",
|
|
9
9
|
tag: "p | h1 | h2 | h3 | h4 | h5 | h6 | span | div.",
|
|
@@ -237,13 +237,25 @@ export const CONTENT = [
|
|
|
237
237
|
{
|
|
238
238
|
type: "html-box", category: "content", container: false, defaultName: "HTML Box",
|
|
239
239
|
summary: "Raw HTML embed. specials.html holds the markup stored HTML-escaped (unescaped at render via v-html). Contrast with editor-blog which stores html RAW. Embedded <iframe> is auto-stretched to 100%×100% of the box. Wrapper height is FIXED to styles.height — content taller than the box overflows. in the stored value becomes a space at render. Use unescape-safe HTML (e.g. '<' → the literal character '<' after unescape).",
|
|
240
|
-
useWhen: "
|
|
240
|
+
useWhen: "COMPOSITE VISUALS (primary use): intricate, non-interactive mockups with 10+ nested parts — phone/chat mockup (message thread with bubbles/avatars/tags), mini dashboard, browser-window frame, inbox/notification list, ticket-style card. Use ONE html-box per mockup instead of 30-40 absolute-positioned elements. Recipe: (1) inline styles ONLY — no class/id (no stylesheet inside the box); (2) root div: style='width:100%;height:100%;box-sizing:border-box;overflow:hidden;…' — fills the element box; flex/grid are fine INSIDE the box (absolute-canvas rule applies only to Webcake elements); (3) design inner content to fit styles.height — content taller overflows silently; (4) specials.html must be HTML-ESCAPED ('<div…'); (5) set BOTH breakpoints with real width/height; (6) set font-family inline on the root if the mockup needs a specific font. WHEN NOT to use: real headings, CTAs, forms, prices, any content the user/editor must edit per-piece, or any element that events must target — html-box is an opaque blob in the editor and invisible to form/event wiring; never put the page's primary copy in one. ALSO: embedding third-party widgets or custom markup the standard elements can't express.",
|
|
241
241
|
keySpecials: { html: "string — raw HTML content stored HTML-escaped (e.g. '<p>Hello</p>'); the renderer unescapes it before injecting via v-html. The wrapper height is FIXED to styles.height — content that is taller overflows the box. An embedded <iframe> is auto-stretched to 100%×100% of the box." },
|
|
242
242
|
seed: (el) => {
|
|
243
243
|
seedPosition(el);
|
|
244
244
|
setBox(el, 280, 310);
|
|
245
245
|
el.specials.html = "";
|
|
246
246
|
},
|
|
247
|
+
example: {
|
|
248
|
+
id: "phone_chat1", type: "html-box",
|
|
249
|
+
responsive: {
|
|
250
|
+
desktop: { styles: { top: 60, left: 330, width: 300, height: 360, position: "absolute" } },
|
|
251
|
+
mobile: { styles: { top: 60, left: 60, width: 300, height: 360, position: "absolute" } },
|
|
252
|
+
},
|
|
253
|
+
specials: {
|
|
254
|
+
// Phone-chat mockup: rounded shell + header bar + 2 chat bubbles.
|
|
255
|
+
// Stored HTML-ESCAPED — the renderer unescapes before v-html injection.
|
|
256
|
+
html: "<div style="width:100%;height:100%;box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;border-radius:24px;background:#fff;box-shadow:0 8px 32px rgba(0,0,0,0.18);font-family:system-ui,sans-serif;"><div style="background:#4f46e5;padding:14px 16px;display:flex;align-items:center;gap:10px;"><div style="width:36px;height:36px;border-radius:50%;background:#a5b4fc;display:flex;align-items:center;justify-content:center;font-size:16px;color:#fff;font-weight:bold;">W</div><div><div style="color:#fff;font-weight:600;font-size:14px;">Webcake Support</div><div style="color:#c7d2fe;font-size:11px;">Online</div></div></div><div style="flex:1;padding:16px;display:flex;flex-direction:column;gap:12px;background:#f5f5f5;"><div style="display:flex;gap:8px;align-items:flex-end;"><div style="width:28px;height:28px;border-radius:50%;background:#4f46e5;flex-shrink:0;"></div><div style="background:#fff;border-radius:16px 16px 16px 4px;padding:10px 14px;font-size:13px;color:#1a1a2e;max-width:200px;box-shadow:0 1px 4px rgba(0,0,0,0.08);">Chào bạn! Mình có thể giúp gì cho bạn hôm nay? 😊</div></div><div style="display:flex;gap:8px;align-items:flex-end;flex-direction:row-reverse;"><div style="background:#4f46e5;border-radius:16px 16px 4px 16px;padding:10px 14px;font-size:13px;color:#fff;max-width:200px;">Mình muốn tạo landing page cho shop! 🚀</div></div></div></div>",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
247
259
|
},
|
|
248
260
|
{
|
|
249
261
|
type: "editor-blog", category: "content", container: false, defaultName: "Editor blog",
|
|
@@ -101,10 +101,12 @@ SECTION BUILD HINTS (apply to whichever sections the chosen archetype uses)
|
|
|
101
101
|
- SOCIAL PROOF — testimonial cards, a logo strip, or a row of stat counters (auto-number + label). Center the row.
|
|
102
102
|
- FORM / CTA — center the form box; stack inputs vertically with comfortable spacing; each input needs a unique specials.field_name (canonical: full_name, phone_number, email, address, quantity); prominent submit button.
|
|
103
103
|
- FOOTER — name + real contact lines, usually centered on a dark band. Only real data the user provided.
|
|
104
|
+
- TAG/BADGE "PILL" recipe — a rounded rectangle (borderRadius "13px", the pill bg color) BEHIND a text-block (zIndex 2, styles.backgroundTxt for the box fill if needed, textAlign center). NEVER put the pill color in styles.background on the text-block — that key activates gradient-text-fill mode (the renderer emits -webkit-text-fill-color:transparent and the glyphs go invisible). text-block does NOT emit border-radius at all — the rounded shape must come from a rectangle behind the text.
|
|
105
|
+
- COMPLEX COMPOSITE VISUAL (phone/chat mockup, mini dashboard, browser-window frame, inbox/notification list, ticket-style card) → ONE html-box with inline-styled HTML (root div: width:100%;height:100%;box-sizing:border-box;overflow:hidden; flex/grid allowed inside; content must fit styles.height or it overflows silently) instead of dozens of tiny elements. Never use html-box for primary copy, CTAs, form fields, or any element that events must target — it is an opaque blob in the editor.
|
|
104
106
|
|
|
105
107
|
RULES
|
|
106
108
|
- Visible content goes in "specials" (text-block.specials.text, image-block.specials.src…), NEVER in "styles".
|
|
107
|
-
- Colors as rgba(r,g,b,a). fontSize/borderWidth/top/left/width/height are NUMBERS (px).
|
|
109
|
+
- Colors as rgba(r,g,b,a). fontSize/borderWidth/top/left/width/height are NUMBERS (px). borderRadius is a STRING with CSS units ("8px", "50%", "16px 16px 0 0") — a bare number or unit-less string is auto-coerced to px by the server, but write the unit explicitly to avoid surprises.
|
|
108
110
|
- IMAGES: a real landing page has images (hero/product shot, feature icons, about photo). PREFER REAL PHOTOS: call search_images with a short English subject (e.g. 'fresh coffee cup', 'modern office team') and put a returned URL into image-block specials.src — use src.large for a hero/banner, src.medium for a card/thumb (avg_color helps pick a matching section background). ONLY if search_images returns ok:false (or is unreachable) FALL BACK to a PLACEHOLDER sized to the box: "https://placehold.co/<width>x<height>". NEVER leave src empty — it renders blank on the live page. The server automatically derives styles.background from specials.src on every expand (create/update/validate) using the editor's exact format: 'center center/ cover no-repeat scroll content-box url(<src>) border-box' — you do NOT need to set styles.background manually; if you do hand-write it, it must contain url(...). gallery.media = array of OBJECTS {type:'image', link:'<real-or-placeholder-url>', linkVideo:'', typeVideo:'youtube', imageCompression:true} (NOT plain URL strings — the gallery reads item.link); video.specials.img = a poster image (real photo, else placeholder). Do NOT set a flat (no url()) styles.background on a video element — it suppresses the poster image.
|
|
109
111
|
- CONTRAST (check EVERY text element against the band it sits on, especially SATURATED / mid-tone bands like yellow, orange, teal, pink — there "light vs dark text" is not obvious, so decide by the band's luminance): light bands → near-black text (e.g. rgba(20,30,25,1)); dark bands → near-white text; a saturated/mid-tone band → whichever of near-black or near-white actually reads (for a bright yellow/amber band that means DARK text, not white/grey). NEVER use muted-grey, low-alpha (alpha < ~0.85), or near-white text on a colored band — that is exactly what makes labels look faded/sunken. Muted-grey is ONLY for secondary text on a white/very-light band. Icons and their captions follow the SAME rule as the text beside them.
|
|
110
112
|
- movable:false for section/slide/grid-item/popup; otherwise true. runtime is always {}.
|
|
@@ -148,4 +150,9 @@ EDITING an existing page
|
|
|
148
150
|
- SMALL edit → PREFER patch_page(page_id, patches): send ONLY the changed elements by id, not the whole source. Ops — {op:'update',id,specials?,styles?:{desktop?,mobile?},config?:{desktop?,mobile?},events?,properties?} (shallow-merge; op defaults to 'update'), {op:'replace',id,element}, {op:'remove',id}, {op:'add',parent_id,element}. The MCP fetches the live source, applies the ops, validates the whole tree, and saves. Reserve update_page(page_id, full source) for when you're rewriting most of the page.
|
|
149
151
|
- To add an element: give it a unique id + top/left/width/height, then patch_page({op:'add', parent_id:<section id>, element:<node>}).
|
|
150
152
|
- FIX-AFTER-ERROR (never rebuild the whole source): when create_page/update_page/add_section reports validation errors, fix ONLY the offending element ids with patch_page. If create_page failed, its error returns a draft_id (source cached) → patch_page({ draft_id, patches, dry_run:false }) fixes + creates the page (a wrong type → { op:'update', id, type:'<allowed type>' }). If update_page/add_section failed, the page has a page_id → patch_page({ page_id, patches }).
|
|
151
|
-
- patch_page/update_page default to dry_run=true (preview); pass dry_run:false to save
|
|
153
|
+
- patch_page/update_page default to dry_run=true (preview); pass dry_run:false to save.
|
|
154
|
+
|
|
155
|
+
REFERENCE INPUT (HTML page to clone or adapt)
|
|
156
|
+
- ingest_html(html, detail:'full') / ingest_url(url, detail:'full') returns a richer AST for clone-quality rebuilds: CSS custom-property palette (design tokens by name), background_images from stylesheets (hero/CTA bg images invisible to a plain img scan), per-section blocks (repeating card/tile/step structures with title/body/image/cta), li lists, gradients, and images as { src, alt } objects. Use detail:'compact' (default) for a quick layout-only reference.
|
|
157
|
+
- Map AST section roles to Webcake elements: hero → section (background image/overlay) + text-block H1 + text-block subheading + button; features → group per card (icon rectangle + text-block title + text-block body); stats bar → group with text-block per stat; pricing → group with text-block list + button; footer → section (dark bg) + text-block + links. When the ingested page contains a composite widget (phone/device mockup, chat thread, mini dashboard, browser frame) → rebuild it as ONE html-box (clone its HTML with all styles inlined), NOT as element soup.
|
|
158
|
+
- When intent='clone': re-host image URLs found in the AST (images, background_images, og_image) via upload_images instead of hotlinking. For intent='adapt' (default): use the AST for layout/hierarchy only and write fresh content for the user's brand.`;
|
|
@@ -62,14 +62,52 @@ function normalizeImageBlocks(node) {
|
|
|
62
62
|
normalizeImageBlocks(child);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// borderRadius normalization: the renderer emits border-radius RAW from the
|
|
67
|
+
// styles object (exportCss.js: `border-radius: ${style.borderRadius};`).
|
|
68
|
+
// A bare number (e.g. 16) or a unit-less string (e.g. "16") produces invalid
|
|
69
|
+
// CSS that browsers silently ignore — every corner renders square. Valid values
|
|
70
|
+
// are strings with CSS units: "16px", "50%", "16px 16px 0 0".
|
|
71
|
+
//
|
|
72
|
+
// Fix: after every expand pass, walk every node and, for each breakpoint whose
|
|
73
|
+
// styles.borderRadius is a number or a unit-less numeric string, coerce it to
|
|
74
|
+
// "<n>px". Already-valid strings (contain a letter or %) pass through untouched.
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/** True when s is a plain number-string with no CSS unit (e.g. "16", "0"). */
|
|
77
|
+
function isUnitless(s) {
|
|
78
|
+
return /^\s*-?\d+(\.\d+)?\s*$/.test(s);
|
|
79
|
+
}
|
|
80
|
+
/** Walk a tree node and coerce numeric/unit-less borderRadius to "<n>px" in-place (mutates). */
|
|
81
|
+
function normalizeBorderRadius(node) {
|
|
82
|
+
if (!node || typeof node !== "object")
|
|
83
|
+
return;
|
|
84
|
+
for (const bp of ["desktop", "mobile"]) {
|
|
85
|
+
const styles = node.responsive?.[bp]?.styles;
|
|
86
|
+
if (!styles || typeof styles !== "object")
|
|
87
|
+
continue;
|
|
88
|
+
const br = styles.borderRadius;
|
|
89
|
+
if (typeof br === "number" && Number.isFinite(br)) {
|
|
90
|
+
styles.borderRadius = `${br}px`;
|
|
91
|
+
}
|
|
92
|
+
else if (typeof br === "string" && isUnitless(br)) {
|
|
93
|
+
styles.borderRadius = `${parseFloat(br)}px`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (Array.isArray(node.children)) {
|
|
97
|
+
for (const child of node.children)
|
|
98
|
+
normalizeBorderRadius(child);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Apply all post-expand normalizations to every node in a page source. */
|
|
66
102
|
function normalizeSource(source) {
|
|
67
103
|
if (!source || typeof source !== "object")
|
|
68
104
|
return source;
|
|
69
105
|
for (const arr of ["page", "popup", "dynamic_pages"]) {
|
|
70
106
|
if (Array.isArray(source[arr])) {
|
|
71
|
-
for (const node of source[arr])
|
|
107
|
+
for (const node of source[arr]) {
|
|
72
108
|
normalizeImageBlocks(node);
|
|
109
|
+
normalizeBorderRadius(node);
|
|
110
|
+
}
|
|
73
111
|
}
|
|
74
112
|
}
|
|
75
113
|
return source;
|
|
@@ -24,7 +24,7 @@ RULES (follow for every request):
|
|
|
24
24
|
A wrong element type is the most common error → { op:'update', id:'<element id>', type:'<allowed type>' } (run list_elements if unsure). Drafts expire in ~30 min.
|
|
25
25
|
- DRY-RUN CACHE: create_page, update_page, and add_section dry_run=true all cache the validated payload and return a draft_id. Re-run the same tool with { draft_id, dry_run:false } — no need to re-send the source.
|
|
26
26
|
- Organizations: call list_organizations and ask which to use; default to the is_default org. Endpoints are owner-scoped (only the account's own pages).
|
|
27
|
-
- REFERENCE INPUT — if the user provides a layout reference, USE it as the layout anchor (don't ignore it, don't re-invent from scratch). Three input modes: (1) IMAGE/screenshot attached in chat → analyze it natively (no tool call): identify section flow (hero/features/form/cta/footer), heading hierarchy, dominant colors, font feel, then map sections to Webcake elements. (2) HTML string → call ingest_html(html) to get
|
|
27
|
+
- REFERENCE INPUT — if the user provides a layout reference, USE it as the layout anchor (don't ignore it, don't re-invent from scratch). Three input modes: (1) IMAGE/screenshot attached in chat → analyze it natively (no tool call): identify section flow (hero/features/form/cta/footer), heading hierarchy, dominant colors, font feel, then map sections to Webcake elements using role→element hints (hero → section with background image/overlay + heading + paragraph + button; features → group blocks with icon/title/text; stats bar → group with heading+text per stat; pricing → group with text list + button; footer → section with text + links). When the reference contains a composite widget (phone/device mockup, chat thread, mini dashboard, browser frame) → rebuild it as ONE html-box (clone its HTML with all styles inlined), not as element soup. (2) HTML string → call ingest_html(html, detail:'full') to get the richer AST when cloning; use detail:'compact' (default) for a layout-only reference. (3) URL → call ingest_url(url, detail:'full') for the same richer AST. The AST classifies sections by role and lists headings/subheadings/ctas/images/form_fields plus brand hints (colors/fonts), CSS custom-property palette, background_images from stylesheets, and in full mode: per-section blocks (cards/tiles/steps with title/body/image/cta), li lists, and gradients — use it for LAYOUT + HIERARCHY, then generate FRESH content tailored to the user's brand (don't 1:1 copy text). When cloning (intent='clone'), re-host image URLs found in the AST (images, background_images, og_image) via upload_images instead of hotlinking. intent='clone' only when the user explicitly asks to mirror the original; default intent='adapt'. The reference workflow PRESERVES craft rules above (centering, page margin, premium spacing, real images) — apply them on top of the reference layout, don't bypass them.
|
|
28
28
|
|
|
29
29
|
MODEL (essentials):
|
|
30
30
|
- Top-level: { page:[sections], popup:[popups], dynamic_pages:[], settings:{}, options:{mobileOnly,versionID}, cartConfigs:{isActive:false}, svariations:[] }. Popups are a SEPARATE top-level array, NOT inside page; currency lives in settings.currency (not options). Leave dynamic_pages/svariations as [] for a static page, but keep them on edit round-trips.
|
|
@@ -39,4 +39,4 @@ MODEL (essentials):
|
|
|
39
39
|
|
|
40
40
|
- PREVIEW vs PUBLISH: the preview_url returned by create_page/update_page/add_section lives on the PREVIEW host (preview.localhost:5800 local / staging.webcake.me staging / www.webcake.me prod — NOT the builder subdomain) and renders the stored source immediately — share it as-is for review. When the user wants the page LIVE (public/published, optionally on their custom domain), call publish_page({ page_id, custom_domain?, custom_path?, dry_run:false }).
|
|
41
41
|
|
|
42
|
-
Start by calling get_generation_guide. Tools: get_generation_guide, list_elements, get_element, new_element, new_page_skeleton, get_page_schema, validate_page, search_images, ingest_html, ingest_url, list_organizations, create_page, list_pages, find_pages, get_page, update_page, add_section, patch_page, publish_page.`;
|
|
42
|
+
Start by calling get_generation_guide. Tools: get_generation_guide, list_elements, get_element, new_element, new_page_skeleton, get_page_schema, validate_page, search_images, upload_images, ingest_html, ingest_url, list_organizations, create_page, list_pages, find_pages, get_page, update_page, add_section, patch_page, publish_page.`;
|
|
@@ -39,9 +39,9 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
39
39
|
* after create/update (a distinct host — NOT the API and NOT the SPA).
|
|
40
40
|
*/
|
|
41
41
|
export const ENVIRONMENTS = {
|
|
42
|
-
local: { apiBase: "http://localhost:5800", appBase: "http://localhost:5173", builderBase: "http://builder.localhost:5800", previewBase: "http://preview.localhost:5800" },
|
|
43
|
-
staging: { apiBase: "https://api.staging.webcake.io", appBase: "https://staging.webcake.io", builderBase: "https://builder.staging.webcake.io", previewBase: "https://staging.webcake.me" },
|
|
44
|
-
prod: { apiBase: "https://api.webcake.io", appBase: "https://webcake.io", builderBase: "https://builder.webcake.io", previewBase: "https://www.webcake.me" },
|
|
42
|
+
local: { apiBase: "http://localhost:5800", appBase: "http://localhost:5173", builderBase: "http://builder.localhost:5800", previewBase: "http://preview.localhost:5800", buildBase: undefined },
|
|
43
|
+
staging: { apiBase: "https://api.staging.webcake.io", appBase: "https://staging.webcake.io", builderBase: "https://builder.staging.webcake.io", previewBase: "https://staging.webcake.me", buildBase: undefined },
|
|
44
|
+
prod: { apiBase: "https://api.webcake.io", appBase: "https://webcake.io", builderBase: "https://builder.webcake.io", previewBase: "https://www.webcake.me", buildBase: "https://build.webcake.io" },
|
|
45
45
|
};
|
|
46
46
|
export function stripTrailingSlash(s) {
|
|
47
47
|
return s?.replace(/\/+$/, "");
|
|
@@ -104,6 +104,13 @@ export function readConfig(overrides = {}) {
|
|
|
104
104
|
preset?.previewBase ??
|
|
105
105
|
saved.previewBase ??
|
|
106
106
|
"https://www.webcake.me");
|
|
107
|
+
// The build host is used by publish_page to build app/app_css before publishing.
|
|
108
|
+
// Prod preset has https://build.webcake.io; staging/local have no reliable public
|
|
109
|
+
// host so they leave it undefined — callers must set WEBCAKE_BUILD_BASE explicitly.
|
|
110
|
+
const buildBase = stripTrailingSlash(overrides.buildBase ??
|
|
111
|
+
process.env.WEBCAKE_BUILD_BASE ??
|
|
112
|
+
preset?.buildBase ??
|
|
113
|
+
saved.buildBase);
|
|
107
114
|
return {
|
|
108
115
|
config: {
|
|
109
116
|
base: cleanBase,
|
|
@@ -112,6 +119,7 @@ export function readConfig(overrides = {}) {
|
|
|
112
119
|
appBase: stripTrailingSlash(overrides.appBase ?? process.env.WEBCAKE_APP_BASE ?? preset?.appBase ?? saved.appBase),
|
|
113
120
|
builderBase,
|
|
114
121
|
previewBase,
|
|
122
|
+
buildBase,
|
|
115
123
|
},
|
|
116
124
|
missing: [],
|
|
117
125
|
};
|
|
@@ -142,6 +150,7 @@ export function configFromHeaders(headers) {
|
|
|
142
150
|
appBase: header(headers, "x-webcake-app-base"),
|
|
143
151
|
builderBase: header(headers, "x-webcake-builder-base"),
|
|
144
152
|
previewBase: header(headers, "x-webcake-preview-base"),
|
|
153
|
+
buildBase: header(headers, "x-webcake-build-base"),
|
|
145
154
|
env: header(headers, "x-webcake-env"),
|
|
146
155
|
};
|
|
147
156
|
}
|