webcake-landing-mcp 1.0.59 → 1.0.60
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/dist/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.60",
|
|
4
|
+
"d": "11/06/2026",
|
|
5
|
+
"type": "Fixed",
|
|
6
|
+
"en": "The expand pipeline (invoked by create_page, update_page, add_section, validate_page, and patch_page) now auto-canonicalizes every url() layer in…",
|
|
7
|
+
"vi": "Pipeline expand (được gọi bởi create_page, update_page, add_section, validate_page, và patch_page) nay tự chuẩn hóa mọi layer url() trong…"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.0.59",
|
|
4
11
|
"d": "11/06/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Fixed",
|
|
34
41
|
"en": "The install command now correctly locates claude_desktop_config.json on Windows when Claude Desktop was installed from the Microsoft Store: the…",
|
|
35
42
|
"vi": "Lệnh install nay xác định đúng đường dẫn claude_desktop_config.json trên Windows khi Claude Desktop được cài từ Microsoft Store: bản Store bị…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.54",
|
|
39
|
-
"d": "10/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "The installer (install command) now supports five additional IDE/agent targets: Antigravity, Gemini CLI, Cline, Kiro, and OpenCode; pass --ide…",
|
|
42
|
-
"vi": "Trình cài đặt (lệnh install) nay hỗ trợ thêm năm IDE/agent: Antigravity, Gemini CLI, Cline, Kiro và OpenCode; truyền --ide antigravity, --ide…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -107,7 +107,7 @@ SECTION BUILD HINTS (apply to whichever sections the chosen archetype uses)
|
|
|
107
107
|
RULES
|
|
108
108
|
- Visible content goes in "specials" (text-block.specials.text, image-block.specials.src…), NEVER in "styles".
|
|
109
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.
|
|
110
|
-
- IMAGES: a real landing page has images (hero/product shot, feature icons, about photo).
|
|
110
|
+
- IMAGES: a real landing page has images (hero/product shot, feature icons, about photo). SOURCE PRIORITY: (1) images the user supplied or that exist in the reference HTML/URL (ingest AST images/background_images/og_image) → re-host them via upload_images and use those EXACT images in their slots — never swap them for stock photos; (2) only for slots with NO source image → 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(...). A SECTION background may layer a gradient overlay over an image: 'linear-gradient(...), center center/ cover no-repeat scroll content-box url(<src>) border-box' — the server canonicalises any url() layer into that exact editor shorthand on expand (other url() formats survive the first save but get mangled to 'undefined/ undefined/ …' the moment the page is edited in the Webcake editor). 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.
|
|
111
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.
|
|
112
112
|
- movable:false for section/slide/grid-item/popup; otherwise true. runtime is always {}.
|
|
113
113
|
- Every form input MUST have a unique specials.field_name.
|
|
@@ -139,7 +139,7 @@ WORKFLOW (recommended)
|
|
|
139
139
|
1. Call get_generation_guide (this) once, then new_page_skeleton for the top-level shape.
|
|
140
140
|
2. For each element type you'll use, call get_element to learn its specials & see an example.
|
|
141
141
|
3. Optionally call new_element to get a correct skeleton, then fill specials + coordinates.
|
|
142
|
-
3b. For every image the page needs (hero, product, about, feature, gallery), call search_images and put a returned URL into specials.src / gallery item.link. Use placehold.co ONLY when search_images returns ok:false.
|
|
142
|
+
3b. For every image the page needs (hero, product, about, feature, gallery): if the slot has a source image (user-supplied or from the reference HTML/URL), upload_images it and use the returned Webcake URL; otherwise call search_images and put a returned URL into specials.src / gallery item.link. Use placehold.co ONLY when search_images returns ok:false.
|
|
143
143
|
4. Assemble { page, popup, settings, options, cartConfigs }.
|
|
144
144
|
5. Call validate_page and fix every error.
|
|
145
145
|
6. To save: call list_organizations. If the account has EXACTLY ONE organization, create_page will auto-select it — no need to ask. If there are MULTIPLE organizations, show them to the user and ask which to use (highlight is_default as the suggested default); pass the chosen organization_id to create_page. If the user explicitly wants to save without any organization, pass organization_id:"personal". Then create_page (dry_run first, then dry_run:false). Note: create_page itself enforces this — it refuses to guess between multiple orgs and returns the org list asking you to pick.
|
|
@@ -155,4 +155,4 @@ EDITING an existing page
|
|
|
155
155
|
REFERENCE INPUT (HTML page to clone or adapt)
|
|
156
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
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
|
-
-
|
|
158
|
+
- Reference images are the user's assets — for BOTH intents (adapt AND clone), re-host every real image URL found in the AST (images, background_images, og_image) via upload_images and reuse it in the matching slot; never hotlink and never replace it with a search_images stock photo. intent='adapt' rewrites TEXT for the user's brand, not the imagery; search_images only fills slots that have no source image.`;
|
|
@@ -98,6 +98,79 @@ function normalizeBorderRadius(node) {
|
|
|
98
98
|
normalizeBorderRadius(child);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// background normalization: the editor's background trait can only parse a
|
|
103
|
+
// url() layer written in its own canonical shorthand
|
|
104
|
+
// '<pos>/ <size> <repeat> <attachment> content-box url(<src>) border-box'
|
|
105
|
+
// (splitBackground in landing_page_backend/assets/editor/common.js). A url()
|
|
106
|
+
// layer in any other format — e.g. plain CSS copied from a reference page like
|
|
107
|
+
// 'url(x) center/cover no-repeat' — survives the first save, but the moment the
|
|
108
|
+
// page is touched in the editor the picker re-composes it from unparsed parts
|
|
109
|
+
// as 'undefined/ undefined/ … content-box url(x)' and SAVES that garbage, so
|
|
110
|
+
// the band renders blank. Gradient/color layers are unaffected.
|
|
111
|
+
//
|
|
112
|
+
// Fix: after every expand pass, split styles.background into top-level comma
|
|
113
|
+
// layers (paren-aware); keep gradient/color layers unless they carry a literal
|
|
114
|
+
// 'undefined' token (a previously mangled layer — drop it); rewrite every url()
|
|
115
|
+
// layer that is not already editor-canonical into the canonical shorthand,
|
|
116
|
+
// preserving the url. Deterministic + idempotent, so expand(compact(x)) ==
|
|
117
|
+
// expand(x) still holds.
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
/** Split a CSS background value into top-level comma-separated layers. */
|
|
120
|
+
function splitBackgroundLayers(bg) {
|
|
121
|
+
const layers = [];
|
|
122
|
+
let depth = 0;
|
|
123
|
+
let cur = "";
|
|
124
|
+
for (const ch of bg) {
|
|
125
|
+
if (ch === "(")
|
|
126
|
+
depth++;
|
|
127
|
+
else if (ch === ")")
|
|
128
|
+
depth--;
|
|
129
|
+
if (ch === "," && depth === 0) {
|
|
130
|
+
layers.push(cur);
|
|
131
|
+
cur = "";
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
cur += ch;
|
|
135
|
+
}
|
|
136
|
+
layers.push(cur);
|
|
137
|
+
return layers.map((l) => l.trim()).filter((l) => l !== "");
|
|
138
|
+
}
|
|
139
|
+
/** The editor-canonical url() layer shape its splitBackground() can re-parse. */
|
|
140
|
+
const CANONICAL_URL_LAYER = /^(left|center|right) (top|center|bottom)\/ (cover|contain|auto|[\d.]+(?:px|%)(?: [\d.]+(?:px|%))?) (no-repeat|repeat|repeat-x|repeat-y|space|round)(?: (scroll|fixed|local))? content-box url\(.+\)(?: border-box)?$/;
|
|
141
|
+
/** Normalise one styles.background value (returns the input when no url layer). */
|
|
142
|
+
function normalizeBackgroundValue(bg) {
|
|
143
|
+
if (typeof bg !== "string" || !bg.includes("url("))
|
|
144
|
+
return bg;
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const layer of splitBackgroundLayers(bg)) {
|
|
147
|
+
const url = layer.match(/url\((['"]?)(.*?)\1\)/);
|
|
148
|
+
if (url) {
|
|
149
|
+
out.push(CANONICAL_URL_LAYER.test(layer) ? layer : imgBackground(url[2]));
|
|
150
|
+
}
|
|
151
|
+
else if (!/\bundefined\b/.test(layer)) {
|
|
152
|
+
out.push(layer);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out.length ? out.join(", ") : bg;
|
|
156
|
+
}
|
|
157
|
+
/** Walk a tree node and canonicalise every url() background layer in-place (mutates). */
|
|
158
|
+
function normalizeBackgrounds(node) {
|
|
159
|
+
if (!node || typeof node !== "object")
|
|
160
|
+
return;
|
|
161
|
+
for (const bp of ["desktop", "mobile"]) {
|
|
162
|
+
const styles = node.responsive?.[bp]?.styles;
|
|
163
|
+
if (!styles || typeof styles !== "object")
|
|
164
|
+
continue;
|
|
165
|
+
const fixed = normalizeBackgroundValue(styles.background);
|
|
166
|
+
if (fixed !== styles.background)
|
|
167
|
+
styles.background = fixed;
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(node.children)) {
|
|
170
|
+
for (const child of node.children)
|
|
171
|
+
normalizeBackgrounds(child);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
101
174
|
/** Apply all post-expand normalizations to every node in a page source. */
|
|
102
175
|
function normalizeSource(source) {
|
|
103
176
|
if (!source || typeof source !== "object")
|
|
@@ -107,6 +180,7 @@ function normalizeSource(source) {
|
|
|
107
180
|
for (const node of source[arr]) {
|
|
108
181
|
normalizeImageBlocks(node);
|
|
109
182
|
normalizeBorderRadius(node);
|
|
183
|
+
normalizeBackgrounds(node);
|
|
110
184
|
}
|
|
111
185
|
}
|
|
112
186
|
}
|
|
@@ -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 before saving. If the account has exactly ONE org, create_page auto-selects it — no need to ask. If there are MULTIPLE orgs, show them and ask the user which to use (is_default is the suggested default); pass the chosen organization_id to create_page. Pass organization_id:"personal" only when the user explicitly wants no org. create_page enforces this itself (it refuses to guess between multiple orgs). 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 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).
|
|
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). IMAGES FROM THE REFERENCE ARE THE USER'S ASSETS — for BOTH intents (adapt AND clone), every real image URL found in the AST (images, background_images, og_image) MUST be re-hosted via upload_images and reused in the corresponding slot; do NOT replace them with search_images stock photos. Call search_images ONLY for slots that have no source image in the reference. intent='clone' only when the user explicitly asks to mirror the original; default intent='adapt' (adapt rewrites TEXT, not the imagery). 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.
|
|
@@ -35,7 +35,7 @@ MODEL (essentials):
|
|
|
35
35
|
- PREMIUM CRAFT (read "sang"): generous whitespace (don't cram; ~48–72px above each band's first element, ≥16–24px between elements); clear type scale (H1 40–56 / body 16–18, big jump); ONE accent used sparingly + neutrals; snap spacing to an 8px grid; reuse the same content width / margin / card+button radius across sections.
|
|
36
36
|
- STICKY HEADER: a sticky/fixed header (config.sticky) OVERLAYS the page — it does NOT push sections below it down. Offset the first section's top content DOWN by the header height (~60–72px) so nothing hides behind it, and do NOT duplicate the shop name in both the header and the top of the hero. A non-sticky header stacks normally and needs no offset.
|
|
37
37
|
- Visible content lives in specials (text, src, field_name…), never in styles. Colors as rgba(). Animation in config.animation={name,delay,duration,repeat}. Form inputs need a unique specials.field_name (use canonical keys: full_name, phone_number, email, address, quantity).
|
|
38
|
-
- IMAGES: include them (hero/product, feature icons, about photo).
|
|
38
|
+
- IMAGES: include them (hero/product, feature icons, about photo). SOURCE PRIORITY: (1) images the user provided or that exist in the reference HTML/URL → re-host via upload_images and use those exact images; (2) only for slots with NO source image → call search_images with a short English subject (e.g. 'fresh coffee cup') and put a returned URL (src.large for a hero/banner, src.medium for a card/thumb) into the image-block specials.src; it works out of the box (a shared proxy supplies images). Only if search_images returns ok:false, FALL BACK to a PLACEHOLDER sized to the box: https://placehold.co/<width>x<height>. (gallery.media = array of OBJECTS {type:'image',link:'<url>',linkVideo:'',typeVideo:'youtube',imageCompression:true} — NOT plain strings, the gallery reads item.link; video.specials.img = poster). NEVER leave src empty (renders blank). Ensure text contrasts with its section background.
|
|
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
|
|
package/dist/smoke.js
CHANGED
|
@@ -817,6 +817,44 @@ console.log("== borderRadius normalization: numeric/unitless coerced to px by ex
|
|
|
817
817
|
const reexpanded16 = landingDomain.expand(compacted16);
|
|
818
818
|
check("borderRadius round-trip: expand(compact(expand(br=16))) deep-equals expand(br=16)", deepEq(reexpanded16, expanded16));
|
|
819
819
|
}
|
|
820
|
+
console.log("== background normalization: url() layers canonicalised to the editor shorthand ==");
|
|
821
|
+
{
|
|
822
|
+
const CANON = "center center/ cover no-repeat scroll content-box url(https://x.test/a.jpg) border-box";
|
|
823
|
+
const mkBgPage = (bgValue) => ({
|
|
824
|
+
page: [
|
|
825
|
+
{
|
|
826
|
+
id: "bg_sec", type: "section",
|
|
827
|
+
responsive: {
|
|
828
|
+
desktop: { styles: { height: 400, background: bgValue } },
|
|
829
|
+
mobile: { styles: { height: 400, background: bgValue } },
|
|
830
|
+
},
|
|
831
|
+
children: [],
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
settings: { title: "t", description: "d", keywords: "k", lang: "vi" },
|
|
835
|
+
});
|
|
836
|
+
const bgOf = (src) => src.page[0].responsive.desktop.styles.background;
|
|
837
|
+
// 1) plain-CSS url layer (reference-page style) → canonical shorthand
|
|
838
|
+
const expPlain = landingDomain.expand(mkBgPage("url(https://x.test/a.jpg) center/cover no-repeat"));
|
|
839
|
+
check("background: plain 'url(x) center/cover no-repeat' → canonical", bgOf(expPlain) === CANON, bgOf(expPlain));
|
|
840
|
+
// 2) gradient overlay + non-canonical url layer → gradient kept, url layer canonicalised
|
|
841
|
+
const grad = "linear-gradient(160deg, rgba(13,45,58,0.88) 0%, rgba(10,124,110,0.75) 60%, rgba(13,45,58,0.9) 100%)";
|
|
842
|
+
const expGrad = landingDomain.expand(mkBgPage(`${grad}, url(https://x.test/a.jpg) center/cover`));
|
|
843
|
+
check("background: gradient + url layer → gradient kept + canonical url", bgOf(expGrad) === `${grad}, ${CANON}`, bgOf(expGrad));
|
|
844
|
+
// 3) editor-mangled 'undefined/ …' layer → repaired to canonical, gradient kept
|
|
845
|
+
const expBroken = landingDomain.expand(mkBgPage(`${grad}, undefined/ undefined/ undefined/ undefined/ content-box url(https://x.test/a.jpg)`));
|
|
846
|
+
check("background: mangled 'undefined/…' url layer → repaired", bgOf(expBroken) === `${grad}, ${CANON}`, bgOf(expBroken));
|
|
847
|
+
// 4) already-canonical layer left byte-identical (idempotent)
|
|
848
|
+
const expCanon = landingDomain.expand(mkBgPage(CANON));
|
|
849
|
+
check("background: canonical layer untouched", bgOf(expCanon) === CANON, bgOf(expCanon));
|
|
850
|
+
// 5) gradient-only / color-only backgrounds untouched
|
|
851
|
+
const expOnlyGrad = landingDomain.expand(mkBgPage(grad));
|
|
852
|
+
check("background: gradient-only untouched", bgOf(expOnlyGrad) === grad, bgOf(expOnlyGrad));
|
|
853
|
+
// 6) expand(compact(x)) invariant with a canonicalised background
|
|
854
|
+
const expanded = landingDomain.expand(mkBgPage(`${grad}, url(https://x.test/a.jpg) center/cover`));
|
|
855
|
+
const reexpanded = landingDomain.expand(landingDomain.compact(expanded));
|
|
856
|
+
check("background round-trip: expand(compact(expand(x))) deep-equals expand(x)", deepEq(reexpanded, expanded));
|
|
857
|
+
}
|
|
820
858
|
console.log("== text-block styles.background warning (gradient-text-fill mode) ==");
|
|
821
859
|
{
|
|
822
860
|
const mkTbBgPage = (bgValue, withClip) => ({
|
package/dist/tools/ingest.js
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* per-section blocks (cards/tiles/steps), lists, extended
|
|
11
11
|
* paragraphs, images as { src, alt } objects. Use for
|
|
12
12
|
* clone-faithful rebuilds. Image URLs found in the result
|
|
13
|
-
* (images, background_images, og_image)
|
|
14
|
-
* via the upload_images tool
|
|
15
|
-
*
|
|
13
|
+
* (images, background_images, og_image) are the user's
|
|
14
|
+
* assets: re-host them via the upload_images tool and reuse
|
|
15
|
+
* them for BOTH intents — adapt rewrites text, not imagery.
|
|
16
16
|
*
|
|
17
17
|
* Image references need no tool: when the user attaches a screenshot, Claude
|
|
18
18
|
* analyzes it natively (multimodal). The server's INSTRUCTIONS string already
|
|
@@ -24,22 +24,22 @@ import { parseHtml, fetchHtml } from "../persistence/html-ingest.js";
|
|
|
24
24
|
const detailParam = z
|
|
25
25
|
.enum(["compact", "full"])
|
|
26
26
|
.optional()
|
|
27
|
-
.describe("Level of detail in the returned AST. 'compact' (default) — backward-compatible ~2-5 KB shape with top colors/fonts from inline styles. 'full' — richer AST: CSS custom-property palette (design tokens by name), background_images from stylesheets, gradients, per-section blocks (repeating card/tile/step structures with title/body/image/cta), li lists, extended paragraphs, and images as { src, alt } objects. Use 'full' for clone-faithful rebuilds. Image URLs found in the result (images, background_images, og_image)
|
|
27
|
+
.describe("Level of detail in the returned AST. 'compact' (default) — backward-compatible ~2-5 KB shape with top colors/fonts from inline styles. 'full' — richer AST: CSS custom-property palette (design tokens by name), background_images from stylesheets, gradients, per-section blocks (repeating card/tile/step structures with title/body/image/cta), li lists, extended paragraphs, and images as { src, alt } objects. Use 'full' for clone-faithful rebuilds. Image URLs found in the result (images, background_images, og_image) are the user's assets: re-host them via upload_images and reuse them in the generated page for BOTH intents (never hotlink, never replace them with search_images stock photos).");
|
|
28
28
|
export function registerIngestTools(server) {
|
|
29
|
-
server.tool("ingest_html", "Parses an HTML string into a reference AST: title, description, og_image, language, and sections classified by role (header, hero, features, about, form, cta, gallery, testimonials, pricing, faq, footer, unknown) with headings, subheadings, paragraphs, images, ctas, links, form fields — plus top colors, fonts, CSS custom-property palette, and background_images pulled from both inline styles and <style> blocks. Returns ~2-5KB (compact) or up to ~25KB (full). Use detail:'full' for clone-faithful rebuilds — it adds per-section blocks (cards/tiles/steps), li lists, gradients, and images as { src, alt } objects. Image URLs in the result (images, background_images, og_image)
|
|
29
|
+
server.tool("ingest_html", "Parses an HTML string into a reference AST: title, description, og_image, language, and sections classified by role (header, hero, features, about, form, cta, gallery, testimonials, pricing, faq, footer, unknown) with headings, subheadings, paragraphs, images, ctas, links, form fields — plus top colors, fonts, CSS custom-property palette, and background_images pulled from both inline styles and <style> blocks. Returns ~2-5KB (compact) or up to ~25KB (full). Use detail:'full' for clone-faithful rebuilds — it adds per-section blocks (cards/tiles/steps), li lists, gradients, and images as { src, alt } objects. Image URLs in the result (images, background_images, og_image) are the user's assets — re-host them via upload_images and reuse them for BOTH intents; use search_images only for slots with no source image.", {
|
|
30
30
|
html: z.string().describe("Raw HTML of a page or a section."),
|
|
31
31
|
intent: z
|
|
32
32
|
.enum(["adapt", "clone"])
|
|
33
33
|
.optional()
|
|
34
|
-
.describe("How the caller intends to use the result. 'adapt' (default) — use as a layout reference and rewrite
|
|
34
|
+
.describe("How the caller intends to use the result. 'adapt' (default) — use as a layout reference and rewrite the TEXT for the user's brand (images from the reference are still re-hosted via upload_images and reused). 'clone' — keep text and images close to the original."),
|
|
35
35
|
detail: detailParam,
|
|
36
36
|
}, { title: "Ingest HTML Reference", readOnlyHint: true, openWorldHint: false }, async ({ html, intent, detail }) => text({ intent: intent ?? "adapt", ...parseHtml(html, detail ?? "compact") }));
|
|
37
|
-
server.tool("ingest_url", "Fetches a public webpage (GET, 10s timeout, 2MB cap) and parses it into the same reference AST as ingest_html. Returns a warning when the page appears client-rendered (empty <body>) so the caller can fall back to a screenshot — Claude can analyze a screenshot natively without this tool. Does not execute JavaScript; sites built with React/Vue/Next.js may return little content. Use detail:'full' for clone-faithful rebuilds — adds CSS palette, background_images, per-section blocks, lists, and images as { src, alt } objects. Image URLs in the result
|
|
37
|
+
server.tool("ingest_url", "Fetches a public webpage (GET, 10s timeout, 2MB cap) and parses it into the same reference AST as ingest_html. Returns a warning when the page appears client-rendered (empty <body>) so the caller can fall back to a screenshot — Claude can analyze a screenshot natively without this tool. Does not execute JavaScript; sites built with React/Vue/Next.js may return little content. Use detail:'full' for clone-faithful rebuilds — adds CSS palette, background_images, per-section blocks, lists, and images as { src, alt } objects. Image URLs in the result are the user's assets — re-host them via upload_images and reuse them for BOTH intents; use search_images only for slots with no source image.", {
|
|
38
38
|
url: z.string().describe("Public HTTP(S) URL of the page to fetch."),
|
|
39
39
|
intent: z
|
|
40
40
|
.enum(["adapt", "clone"])
|
|
41
41
|
.optional()
|
|
42
|
-
.describe("How the caller intends to use the result. 'adapt' (default) — use as a layout reference and rewrite
|
|
42
|
+
.describe("How the caller intends to use the result. 'adapt' (default) — use as a layout reference and rewrite the TEXT for the user's brand (images from the reference are still re-hosted via upload_images and reused). 'clone' — keep text and images close to the original."),
|
|
43
43
|
detail: detailParam,
|
|
44
44
|
}, { title: "Ingest URL Reference", readOnlyHint: true, openWorldHint: true }, async ({ url, intent, detail }) => {
|
|
45
45
|
const fetched = await fetchHtml(url);
|
package/dist/tools/media.js
CHANGED
|
@@ -69,7 +69,7 @@ function extFromUrl(url) {
|
|
|
69
69
|
}
|
|
70
70
|
export function registerMediaTools(server) {
|
|
71
71
|
// 13) Search images ---------------------------------------------------------
|
|
72
|
-
server.tool("search_images", "Searches Pexels stock photos (see https://www.pexels.com/api/) by short English subject queries. Returns hotlinkable URLs at several sizes (src.large for heroes/banners, src.medium for cards/thumbs), `avg_color` for matching section backgrounds, plus photographer name and attribution URL. BATCH MODE: pass `queries: [...]` to fetch multiple subjects in PARALLEL — e.g. ['fresh coffee cup','barista pouring','interior cafe'] for hero + about + gallery — returns { queries: { [q]: result } } so the caller picks one image per slot in a single round-trip; default `pick='best'` returns only the top photo per query (compact, drop-in for specials.src), `pick='all'` returns the full list. `query` (single) returns the full result like before. Works out of the box via a shared hosted proxy; set PEXELS_API_KEY env or x-pexels-key header to use your own quota.", {
|
|
72
|
+
server.tool("search_images", "Searches Pexels stock photos (see https://www.pexels.com/api/) by short English subject queries. Returns hotlinkable URLs at several sizes (src.large for heroes/banners, src.medium for cards/thumbs), `avg_color` for matching section backgrounds, plus photographer name and attribution URL. BATCH MODE: pass `queries: [...]` to fetch multiple subjects in PARALLEL — e.g. ['fresh coffee cup','barista pouring','interior cafe'] for hero + about + gallery — returns { queries: { [q]: result } } so the caller picks one image per slot in a single round-trip; default `pick='best'` returns only the top photo per query (compact, drop-in for specials.src), `pick='all'` returns the full list. `query` (single) returns the full result like before. Works out of the box via a shared hosted proxy; set PEXELS_API_KEY env or x-pexels-key header to use your own quota. ONLY for image slots with NO source image: when the user supplied images or the reference HTML/URL contains image URLs (ingest AST images/background_images/og_image), re-host THOSE via upload_images instead of searching stock photos.", {
|
|
73
73
|
query: z.string().optional().describe("Single subject query — backward-compat. Prefer `queries` when the page needs 2+ images."),
|
|
74
74
|
queries: z
|
|
75
75
|
.array(z.string())
|
|
@@ -130,7 +130,7 @@ export function registerMediaTools(server) {
|
|
|
130
130
|
return text({ queries: out });
|
|
131
131
|
});
|
|
132
132
|
// 14) Upload images to Webcake -----------------------------------------------
|
|
133
|
-
server.tool("upload_images", "Converts external image URLs (typically collected from ingest_html/ingest_url results) or data: URIs into Webcake-hosted URLs (statics.pancake.vn) by downloading each image and re-uploading it to the Webcake backend. Use this
|
|
133
|
+
server.tool("upload_images", "Converts external image URLs (typically collected from ingest_html/ingest_url results) or data: URIs into Webcake-hosted URLs (statics.pancake.vn) by downloading each image and re-uploading it to the Webcake backend. Use this whenever the page is built from a reference HTML/URL (BOTH intents — adapt AND clone) or the user supplies their own image URLs: reference images are the user's assets, so re-host and reuse them rather than replacing them with stock photos, and never hotlink third-party hosts that may block hotlinking or disappear. The returned URLs go directly into specials.src — same as search_images results. Processes up to 20 URLs per call in parallel, with an 8 MB per-image cap. No Webcake credentials required (the upload endpoint is public). DEFAULTS to dry_run=true (returns a preview of what would be uploaded, no network calls); set dry_run=false to actually upload. Use search_images instead when you need stock photos.", {
|
|
134
134
|
urls: z
|
|
135
135
|
.array(z.string())
|
|
136
136
|
.min(1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.60",
|
|
4
4
|
"description": "MCP server exposing Webcake landing-page element schemas + AI usage hints, and persisting LLM-generated page sources to a Webcake backend.",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/webcake-landing-mcp",
|
|
6
6
|
"type": "module",
|