webcake-landing-mcp 1.0.75 → 1.0.76
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.76",
|
|
4
|
+
"d": "15/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "validate_page now warns when specials.custom_css sets layout or structural CSS properties (position, top, left, right, bottom, inset, width, height,…",
|
|
7
|
+
"vi": "validate_page nay cảnh báo khi specials.custom_css đặt các thuộc tính CSS layout hoặc cấu trúc (position, top, left, right, bottom, inset, width,…"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.0.75",
|
|
4
11
|
"d": "15/06/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Added",
|
|
34
41
|
"en": "ingest_html and ingest_url now automatically convert absolute-canvas builder exports (LadiPage-family / Webcake-published HTML) into a ready-to-save…",
|
|
35
42
|
"vi": "ingest_html và ingest_url nay tự động chuyển đổi các bản export từ builder absolute-canvas (LadiPage-family / Webcake-published HTML) thành source…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.70",
|
|
39
|
-
"d": "13/06/2026",
|
|
40
|
-
"type": "Changed",
|
|
41
|
-
"en": "upload_images default changed from dry_run:true to dry_run:false — the tool now uploads images and returns the hosted URL map on every call without…",
|
|
42
|
-
"vi": "Mặc định của upload_images được đổi từ dry_run:true thành dry_run:false — tool này nay upload ảnh và trả về bản đồ URL trong mọi lần gọi mà không…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -120,7 +120,7 @@ RULES
|
|
|
120
120
|
- Every form input MUST have a unique specials.field_name.
|
|
121
121
|
- events item: { "id", "type", "action", "target", ...action-specific extra fields }. TRIGGER (type): click & hover on any element; success & error on a FORM (success = after a successful submit, error = on validation failure); delay on any element (when it scrolls into view); unset on init. Action vocab per trigger: click→CLICK_ACTIONS, hover→HOVER_ACTIONS, success→SUCCESS_ACTIONS, error→ERROR_ACTIONS, delay→DELAY_ACTIONS (all returned by get_generation_guide). For element-targeting actions (open_popup, close_popup, scroll_to, show_section, hide_section, show_hide_element, change_tab, collapse) target = the target element's id; open_link/download_file target = URL; open_sms/send_email/phone_call target = phone/email; copy target = text (or element id when copyType='elementValue'); set_field_value target = field_name; target may be null (e.g. animation_hover). Each action also reads extra fields (e.g. open_link→targetURL/delayTime, scroll_to→scrollMore, change_tab→moveTo/tabIndex, lightbox→typeLightbox/alt, show_hide_element→onlyMode, open_app→appTarget+provider fields, set_field_value→set_value) — see the action maps for the full list.
|
|
122
122
|
- ANIMATION: each breakpoint's config has config.animation = { "name":"none", "delay":0, "duration":3, "repeat":null }. Animations only run on these 9 element types: group, image-block, text-block, rectangle, button, countdown, line, list-paragraph, notify (renderer contract: landing_page_build/render/build/animate.js). Any other type with a non-"none" name renders stuck/dim in its pre-animation state — keep "none" on all other types. The name must be from the editor's animate.css set; common entrance families: fadeIn* (fadeInUp, fadeInDown, fadeInLeft, fadeInRight…), slideIn* (slideInUp, slideInDown, slideInLeft, slideInRight), zoomIn* (zoomIn, zoomInUp, zoomInDown…), bounceIn* (bounceIn, bounceInUp…), backIn* (backInDown, backInLeft…), flipIn* (flipInX, flipInY), lightSpeedIn* (lightSpeedInLeft, lightSpeedInRight), rotateIn* (rotateIn, rotateInDownLeft…), rollIn, jackInTheBox; attention seekers: bounce, pulse, tada, headShake, wobble, jello, heartBeat, rubberBand, shakeX, shakeY. The full set is enforced by validate_page — use an invalid name and the element renders stuck. NEVER set styles.opacity < 1 for a "subtle" or "muted" look — opacity is permanent and renders the element and all its content faded forever; use rgba() alpha on the color or background property instead.
|
|
123
|
-
- BEYOND ELEMENT CAPABILITY (custom CSS / class / JS — use these so the page is as COMPLETE as possible instead of dropping an effect the built-in specials can't express): the renderer supports real escape hatches — REACH FOR THEM when a reference (esp. a Google Stitch / Tailwind page) has hover/transition effects, gradients, glassmorphism, custom shadows, gradient/clipped text, sticky tricks, or keyframe animations outside the 9-type entrance set. Tools, in order of preference: (1) element specials.custom_css — extra CSS DECLARATIONS injected INTO that element's own rule "#w-<id>{ … }" (declarations only — NO selector, NO :hover, NO @keyframes). Requires specials.customAdvance:true (without it the renderer drops it). Use for: "background:linear-gradient(...)" (gradient button/hero — also reproduces the AST's gradients), "box-shadow:0 20px 40px rgba(0,0,0,.08)", "backdrop-filter:blur(20px)" (glass header — pair with an rgba background), "transition:transform .3s ease", "-webkit-background-clip:text;-webkit-text-fill-color:transparent" (gradient text). (2) element specials.custom_class (comma-separated, also needs customAdvance:true) — adds class names to "#w-<id>" so settings.extra_css can target a group of elements. (3) page settings.extra_css — a FULL stylesheet injected raw into <head>: this is where SELECTORS live — :hover, :focus, @keyframes, media queries. The DOM id of any element is "#w-<its id>". THIS is how you implement the AST's per-section hover_effects: scale → "#w-<id>{transition:transform .3s} #w-<id>:hover{transform:scale(1.05)}"; card lift → "#w-<id>:hover{transform:translateY(-8px)}"; image-zoom in a card → "#w-<cardId>{overflow:hidden} #w-<imgId>{transition:transform .5s} #w-<cardId>:hover #w-<imgId>{transform:scale(1.1)}"; color/underline on hover are also fine as a hover EVENT (change_color / change_underline) — pick whichever, but DON'T silently drop the effect. (4) page settings.extra_script — raw JS injected before </body> for behavior the event vocab can't express. (5) page settings.bhet ('before </head>') and settings.bbet ('before </body>') — raw HTML BLOCKS (not just CSS/JS): bhet for <link> webfonts/stylesheets, <style>, <meta>, verification tags, analytics/pixels (GTM, FB); bbet for chat widgets, deferred <script>, GTM <noscript>, third-party embeds. Use these when you need a real font file, a tag/pixel, or an external widget — extra_css/extra_script are for raw CSS/JS only, bhet/bbet take arbitrary HTML (valid markup only).
|
|
123
|
+
- BEYOND ELEMENT CAPABILITY (custom CSS / class / JS — use these so the page is as COMPLETE as possible instead of dropping an effect the built-in specials can't express): the renderer supports real escape hatches — REACH FOR THEM when a reference (esp. a Google Stitch / Tailwind page) has hover/transition effects, gradients, glassmorphism, custom shadows, gradient/clipped text, sticky tricks, or keyframe animations outside the 9-type entrance set. Tools, in order of preference: (1) element specials.custom_css — extra CSS DECLARATIONS injected INTO that element's own rule "#w-<id>{ … }" (declarations only — NO selector, NO :hover, NO @keyframes). Requires specials.customAdvance:true (without it the renderer drops it). Use for: "background:linear-gradient(...)" (gradient button/hero — also reproduces the AST's gradients), "box-shadow:0 20px 40px rgba(0,0,0,.08)", "backdrop-filter:blur(20px)" (glass header — pair with an rgba background), "transition:transform .3s ease", "-webkit-background-clip:text;-webkit-text-fill-color:transparent" (gradient text). (2) element specials.custom_class (comma-separated, also needs customAdvance:true) — adds class names to "#w-<id>" so settings.extra_css can target a group of elements. (3) page settings.extra_css — a FULL stylesheet injected raw into <head>: this is where SELECTORS live — :hover, :focus, @keyframes, media queries. The DOM id of any element is "#w-<its id>". THIS is how you implement the AST's per-section hover_effects: scale → "#w-<id>{transition:transform .3s} #w-<id>:hover{transform:scale(1.05)}"; card lift → "#w-<id>:hover{transform:translateY(-8px)}"; image-zoom in a card → "#w-<cardId>{overflow:hidden} #w-<imgId>{transition:transform .5s} #w-<cardId>:hover #w-<imgId>{transform:scale(1.1)}"; color/underline on hover are also fine as a hover EVENT (change_color / change_underline) — pick whichever, but DON'T silently drop the effect. (4) page settings.extra_script — raw JS injected before </body> for behavior the event vocab can't express. (5) page settings.bhet ('before </head>') and settings.bbet ('before </body>') — raw HTML BLOCKS (not just CSS/JS): bhet for <link> webfonts/stylesheets, <style>, <meta>, verification tags, analytics/pixels (GTM, FB); bbet for chat widgets, deferred <script>, GTM <noscript>, third-party embeds. Use these when you need a real font file, a tag/pixel, or an external widget — extra_css/extra_script are for raw CSS/JS only, bhet/bbet take arbitrary HTML (valid markup only). SAFETY — custom is RAW and global, so sloppy custom is the #1 way to BREAK the UI; obey these or the page shatters (validate_page warns on each): (a) SCOPE every settings.extra_css rule to a specific element — "#w-<id>" or a specials.custom_class you added. NEVER write a bare-tag (body, div, section, p, img, a, button, ul, li, h1…), universal "*", or Webcake-internal-class (.section-container, .section-wrapper, .pageview, .rectangle-css, .text-block-css, .image-block-css, .button-css, .group-*, .gallery-*, .overlay, .full-width/.full-height…) selector — those restyle the WHOLE page and wreck the layout. (b) custom_css is VISUAL props ONLY (background, box-shadow, border, border-radius, filter, backdrop-filter, transition, transform, -webkit-background-clip) and declarations-only (no selector / :hover / @keyframes) and needs customAdvance:true — NEVER put position / top / left / right / bottom / inset / width / height / display / float / flex / grid in custom_css (they override the element box and break the absolute canvas; set geometry via responsive.<bp>.styles). (c) bhet/bbet must be VALID, fully-CLOSED HTML (an unclosed <script>/<style>/<div> swallows the rest of the page); put CSS in extra_css and JS in extra_script (bhet/bbet only for real HTML like <link>/<meta>/widgets), and any <style> inside them follows rule (a). (d) extra_script: wrap in try/catch, run on DOMContentLoaded, and NEVER remove/restructure "#w-…" elements (the renderer owns them) — only attach behavior. (e) balance your braces. (f) don't use custom to replace a capability a built-in special already has (entrance animation → config.animation; click/hover behavior → events; size/position → styles).
|
|
124
124
|
- ICONS (icon-font glyphs — don't drop them, and DON'T blind-svg-mask them): references (esp. Google Stitch) render icons with an icon-font CLASS, not images or inline SVG — Material Symbols (<span class="material-symbols-outlined">verified</span>) or Font Awesome (<i class="fa-solid fa-check">). ingest gives only the NAME as block.icon "ms:<name>" / "fa:<name>" (e.g. "ms:support_agent"). render it as Webcake's NATIVE icon element — a RECTANGLE with an svg mask: STEP 1 — call get_icon_svg with those refs to get each REAL <svg> (Material Symbols outlined / Font Awesome) via Iconify (NEVER invent an SVG from a name). STEP 2 — make a rectangle and put that svg in BOTH responsive.desktop.config.svgMask AND responsive.mobile.config.svgMask, set styles.background = the icon color, and use a SQUARE box (width === height). Why each rule: the svg is only a MASK (its own fill is IGNORED) — visible pixels come ENTIRELY from styles.background, so WITHOUT a solid background the icon is BLANK (that's the "svg in a rectangle doesn't show" bug); the renderer reads each breakpoint's config separately (no fallback), so the mask must be in BOTH; and it forces preserveAspectRatio='none', so a non-square box stretches the icon. validate_page warns if background / viewBox / both breakpoints are missing. If get_icon_svg can't resolve a ref, fall back to an emoji inline in a sentence or skip that slot — NEVER leave a feature card iconless.
|
|
125
125
|
- Real data the page DISPLAYS must come from the user — never invent it: phone/hotline/Zalo, price (+ original price), address, shop/brand name, links/URLs, email, opening hours, exact stats/social-proof numbers. If a value the page needs is missing, ASK for it (in intake, or pause before generating); use a clearly-labelled placeholder ONLY when the user explicitly declines, and tell them exactly what to fill. Write ALL page copy in the SAME language the user is chatting in (mirror it), with FULL, CORRECT diacritics/accents — for Vietnamese this means proper dấu (e.g. "Trân Trọng Kính Mời", "Ngày 15 Tháng 08 Năm 2025"), NEVER accent-stripped "không dấu" text. Do not romanize, transliterate, or drop accent marks from any language.
|
|
126
126
|
|
|
@@ -39,7 +39,7 @@ MODEL (essentials):
|
|
|
39
39
|
- 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).
|
|
40
40
|
- IMAGES: include them (hero/product, feature icons, about photo). The server AUTO-HOSTS external image URLs on every save: any real http(s) image URL you put in specials.src / a url(...) background / gallery item.link / video poster is downloaded and re-hosted to the Webcake CDN automatically by create_page/update_page/add_section/patch_page (the result's rehost field reports how many) — so you do NOT have to pre-call upload_images for reference or web images. SOURCE PRIORITY: (1) images the user supplied or that exist in the reference HTML/URL → put the REAL source URL straight into specials.src and let the save host it (use those exact images, never swap them for stock photos; the original URL must reach the save un-altered — NEVER replace a real source image URL with a placeholder). The ONE case you MUST still call upload_images yourself: LOCAL FILE PATHS from the user's computer (pass the path directly in upload_images urls — the save can't read local files; NEVER upload a user's local file to a third-party host like catbox or imgur first), then use the returned URL. (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); (3) if search_images returns ok:false / is unreachable / has no fitting photo → find a real image YOURSELF using whatever web search/fetch capability you have (brand site, product page, free-to-use source) and re-host it via upload_images; (4) a PLACEHOLDER sized to the box (https://placehold.co/<width>x<height>) is the LAST resort, only after (2) AND (3) both failed. (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.
|
|
41
41
|
|
|
42
|
-
- BEYOND ELEMENT CAPABILITY: when a reference effect can't be expressed with an element's built-in specials (hover scale/lift/zoom & transitions, gradients, glassmorphism/backdrop-blur, custom shadows, gradient/clipped text, keyframe animations outside the 9 entrance types) DON'T drop it — use the escape hatches so the page is as complete as possible: element specials.custom_css (extra DECLARATIONS in #w-<id>{…}; declarations-only) + specials.custom_class (both need specials.customAdvance:true) for per-element styling, and page settings.extra_css (a full raw stylesheet in <head> — where :hover/@keyframes/media-queries live, target #w-<element id>) + settings.extra_script (raw JS) + settings.bhet/bbet (raw HTML blocks at end of <head>/<body> — for webfont <link>s, <meta>/verification, analytics/pixels, chat widgets, third-party embeds). get_generation_guide has the recipes; validate_page
|
|
42
|
+
- BEYOND ELEMENT CAPABILITY: when a reference effect can't be expressed with an element's built-in specials (hover scale/lift/zoom & transitions, gradients, glassmorphism/backdrop-blur, custom shadows, gradient/clipped text, keyframe animations outside the 9 entrance types) DON'T drop it — use the escape hatches so the page is as complete as possible: element specials.custom_css (extra DECLARATIONS in #w-<id>{…}; declarations-only) + specials.custom_class (both need specials.customAdvance:true) for per-element styling, and page settings.extra_css (a full raw stylesheet in <head> — where :hover/@keyframes/media-queries live, target #w-<element id>) + settings.extra_script (raw JS) + settings.bhet/bbet (raw HTML blocks at end of <head>/<body> — for webfont <link>s, <meta>/verification, analytics/pixels, chat widgets, third-party embeds). get_generation_guide has the recipes. SAFETY (custom is raw + global → sloppy custom BREAKS the whole UI): SCOPE every extra_css rule to "#w-<id>" or a specials.custom_class — NEVER a bare tag / "*" / a Webcake-internal class (.section-container/.rectangle-css/.text-block-css/.group-*…); keep custom_css to VISUAL props only (no position/top/left/width/height/display/float); close every bhet/bbet tag; don't restructure "#w-…" elements in extra_script. validate_page flags unscoped selectors, layout props in custom_css, unbalanced braces/tags, missing customAdvance, and CSS/JS in the wrong field — fix every such warning.
|
|
43
43
|
- PREVIEW vs PUBLISH: for review share the EDITOR url (the builder renders the raw source). The editor_url SIGNS THE BROWSER IN automatically (it routes through the builder's /transport with the account token), so it works even when the user isn't logged in — but for the same reason it must go to the PAGE OWNER ONLY, never into anything public. create_page AUTO-PUBLISHES on success (builds the rendered app + publish_html), so a fresh page's preview_url renders right away — but the preview host (preview.localhost:5800 local / staging.webcake.me staging / www.webcake.me prod) only serves it for ~10 MINUTES after the last publish, then shows "Preview page is expired". The EDIT routes (update_page/add_section/patch_page) store source only — after finishing a round of edits, run publish_page({ page_id, dry_run:false }) to rebuild the rendered app (else the preview shows the STALE pre-edit build). ONLY a custom_domain (publish_page({ page_id, custom_domain, dry_run:false })) gives a permanent public URL; without one the page has just the ephemeral preview link — say so and suggest attaching a domain the user already points at Webcake.
|
|
44
44
|
|
|
45
45
|
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, get_icon_svg, upload_images, ingest_html, ingest_url, list_organizations, create_page, list_pages, find_pages, get_page, update_page, add_section, patch_page, publish_page.`;
|
|
@@ -315,6 +315,17 @@ export function validatePage(input) {
|
|
|
315
315
|
if (typeof sp.custom_css === "string" && /[{}]|@keyframes|:hover|:focus|::/.test(sp.custom_css)) {
|
|
316
316
|
warnings.push(`${path} (${type}): specials.custom_css is injected as plain declarations inside #w-${node.id}{…} — a selector/:hover/@keyframes/media-query there breaks the rule. Keep declarations only here (e.g. "box-shadow:0 20px 40px rgba(0,0,0,.08);backdrop-filter:blur(20px);"); put hover/keyframes/media rules in settings.extra_css targeting #w-${node.id} (or a specials.custom_class).`);
|
|
317
317
|
}
|
|
318
|
+
// Layout/structural props in custom_css fight the absolute-canvas system
|
|
319
|
+
// (the renderer sets the element's box + display) → the element jumps or the
|
|
320
|
+
// page breaks. Geometry belongs in responsive.<bp>.styles, not custom_css.
|
|
321
|
+
if (typeof sp.custom_css === "string") {
|
|
322
|
+
const badProps = (sp.custom_css.match(/(?:^|[;{]\s*)(position|top|left|right|bottom|inset|width|height|display|float|flex|grid)\s*:/gi) ?? [])
|
|
323
|
+
.map((m) => m.replace(/[;{:\s]/g, ""))
|
|
324
|
+
.filter((p, i, a) => a.indexOf(p) === i);
|
|
325
|
+
if (badProps.length) {
|
|
326
|
+
warnings.push(`${path} (${type}): specials.custom_css sets layout prop(s) ${badProps.join(", ")} — these override the element's box/display and break the absolute-canvas layout. Set size/position via responsive.<bp>.styles (top/left/width/height per breakpoint); keep custom_css to VISUAL props only (background, box-shadow, border, filter, backdrop-filter, transition, transform).`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
318
329
|
}
|
|
319
330
|
}
|
|
320
331
|
// animation contract — checked per breakpoint
|
|
@@ -1075,6 +1086,70 @@ export function validatePage(input) {
|
|
|
1075
1086
|
const ms = p?.responsive?.mobile?.styles ?? {};
|
|
1076
1087
|
checkBounds(p, num(ds.width) ?? rootCanvasD, num(ds.height) ?? DEFAULT_SECTION_HEIGHT, num(ms.width) ?? rootCanvasM, num(ms.height) ?? DEFAULT_SECTION_HEIGHT, `popup[${i}]`);
|
|
1077
1088
|
});
|
|
1089
|
+
// ── custom-code safety: settings.extra_css / extra_script / bhet / bbet ──────
|
|
1090
|
+
// These inject RAW into the page (extra_css in <head>, extra_script before
|
|
1091
|
+
// </body>, bhet at end of <head>, bbet at end of <body>) — unscoped/broken
|
|
1092
|
+
// custom code is the #1 way "custom breaks the UI". Flag the dangerous shapes.
|
|
1093
|
+
{
|
|
1094
|
+
// Webcake's own runtime class names — restyling them globally breaks the layout.
|
|
1095
|
+
const INTERNAL_CLASS_RE = /\.(section-container|section-wrapper|pageview|rectangle-css|text-block-css|image-block-css|button-css|group-[\w-]*|gallery-[\w-]*|popup-[\w-]*|overlay|lazy|full-mask-size|mask-position|full-(?:width|height)|ladi-[\w-]*)\b/;
|
|
1096
|
+
// Bare element selectors that, unscoped, restyle the WHOLE page.
|
|
1097
|
+
const BARE_TAG_SEL_RE = /(?:^|[,{}>+~\s])(html|body|section|article|header|footer|nav|main|aside|div|span|p|a|button|ul|ol|li|img|table|tr|td|input|form|h[1-6])\s*(?:[,{>+~:.[]|$)/i;
|
|
1098
|
+
/** Selectors in a raw stylesheet not scoped to #w-… that would restyle the whole page (broad tags, *, Webcake internals). */
|
|
1099
|
+
const broadCssSelectors = (css) => {
|
|
1100
|
+
const out = new Set();
|
|
1101
|
+
const ruleRe = /([^{}]+)\{[^{}]*\}/g;
|
|
1102
|
+
let m;
|
|
1103
|
+
while ((m = ruleRe.exec(css)) !== null && out.size < 5) {
|
|
1104
|
+
for (const selRaw of m[1].split(",")) {
|
|
1105
|
+
const sel = selRaw.trim();
|
|
1106
|
+
if (!sel || sel.startsWith("@") || sel.includes("#w-"))
|
|
1107
|
+
continue; // scoped/at-rule → safe
|
|
1108
|
+
if (/\*/.test(sel) || BARE_TAG_SEL_RE.test(sel) || INTERNAL_CLASS_RE.test(sel))
|
|
1109
|
+
out.add(sel.slice(0, 60));
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return [...out];
|
|
1113
|
+
};
|
|
1114
|
+
const settings = page?.settings;
|
|
1115
|
+
if (settings && typeof settings === "object") {
|
|
1116
|
+
const extraCss = settings.extra_css;
|
|
1117
|
+
if (typeof extraCss === "string" && extraCss.trim()) {
|
|
1118
|
+
const opens = (extraCss.match(/{/g) ?? []).length;
|
|
1119
|
+
const closes = (extraCss.match(/}/g) ?? []).length;
|
|
1120
|
+
if (opens !== closes) {
|
|
1121
|
+
warnings.push(`settings.extra_css has unbalanced braces (${opens} '{' vs ${closes} '}') — a malformed rule leaks into the rest of the page CSS and breaks the layout. Fix the braces.`);
|
|
1122
|
+
}
|
|
1123
|
+
const broad = broadCssSelectors(extraCss);
|
|
1124
|
+
if (broad.length) {
|
|
1125
|
+
warnings.push(`settings.extra_css has UNSCOPED selector(s) that restyle the whole page and break the UI: ${broad.join(" | ")}. Scope EVERY rule to a specific element — #w-<element id> (or a specials.custom_class you added) — never bare tags, '*', or Webcake's own classes (.section-container / .rectangle-css / .text-block-css / .group-* / …).`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
for (const field of ["bhet", "bbet"]) {
|
|
1129
|
+
const v = settings[field];
|
|
1130
|
+
if (typeof v !== "string" || !v.trim())
|
|
1131
|
+
continue;
|
|
1132
|
+
if (!v.includes("<")) {
|
|
1133
|
+
warnings.push(`settings.${field} contains no HTML tags — it is injected as raw HTML (${field === "bhet" ? "end of <head>" : "end of <body>"}), not CSS/JS. Put CSS in settings.extra_css and JS in settings.extra_script, or wrap this in <style>…</style> / <script>…</script>.`);
|
|
1134
|
+
}
|
|
1135
|
+
for (const sb of v.match(/<style[^>]*>([\s\S]*?)<\/style>/gi) ?? []) {
|
|
1136
|
+
const broad = broadCssSelectors(sb.replace(/<\/?style[^>]*>/gi, ""));
|
|
1137
|
+
if (broad.length) {
|
|
1138
|
+
warnings.push(`settings.${field} has a <style> with UNSCOPED selector(s) that break the page UI: ${broad.join(" | ")}. Scope every rule to #w-<id> / a custom_class, or move page-wide CSS into settings.extra_css with scoped selectors.`);
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
for (const tag of ["script", "style", "div"]) {
|
|
1143
|
+
const o = (v.match(new RegExp(`<${tag}\\b`, "gi")) ?? []).length;
|
|
1144
|
+
const c = (v.match(new RegExp(`</${tag}>`, "gi")) ?? []).length;
|
|
1145
|
+
if (o !== c) {
|
|
1146
|
+
warnings.push(`settings.${field} has an unbalanced <${tag}> tag (${o} open vs ${c} close) — an unclosed tag swallows the rest of the page and breaks rendering. Close every tag.`);
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1078
1153
|
return {
|
|
1079
1154
|
valid: errors.length === 0,
|
|
1080
1155
|
errors,
|
package/dist/smoke.js
CHANGED
|
@@ -224,6 +224,42 @@ console.log("== validate: custom CSS/class/JS escape hatches (beyond-element cap
|
|
|
224
224
|
check("escape hatch: warns selector/:hover inside custom_css", selR.warnings.some((w) => /declarations inside/.test(w)), selR.warnings);
|
|
225
225
|
check("escape hatch: declarations-only misuse does NOT block (warning, not error)", selR.valid, selR.errors);
|
|
226
226
|
}
|
|
227
|
+
console.log("== validate: custom-code SAFETY (broad/broken custom breaks the UI) ==");
|
|
228
|
+
{
|
|
229
|
+
const cloneG = () => JSON.parse(JSON.stringify(good));
|
|
230
|
+
const W = (r, re) => r.warnings.filter((w) => re.test(w));
|
|
231
|
+
// settings.extra_css with bare-tag + Webcake-internal selectors → unscoped warning; scoped #w- rule does NOT trip it.
|
|
232
|
+
const broad = cloneG();
|
|
233
|
+
broad.settings.extra_css = "body{margin:0} .rectangle-css{opacity:.5} #w-btn1:hover{transform:scale(1.02)}";
|
|
234
|
+
const broadR = validatePage(broad);
|
|
235
|
+
check("custom-safety: unscoped extra_css selectors (body/.rectangle-css) flagged", W(broadR, /UNSCOPED selector/).length > 0, broadR.warnings);
|
|
236
|
+
check("custom-safety: a #w- scoped rule is NOT flagged", !/#w-btn1/.test(W(broadR, /UNSCOPED selector/)[0] ?? ""), W(broadR, /UNSCOPED/));
|
|
237
|
+
// unbalanced braces in extra_css.
|
|
238
|
+
const braces = cloneG();
|
|
239
|
+
braces.settings.extra_css = "#w-btn1{color:red";
|
|
240
|
+
check("custom-safety: unbalanced extra_css braces flagged", W(validatePage(braces), /unbalanced braces/).length > 0);
|
|
241
|
+
// bhet holding raw CSS (no tags) → wrong-field warning.
|
|
242
|
+
const bhetCss = cloneG();
|
|
243
|
+
bhetCss.settings.bhet = "body{margin:0}";
|
|
244
|
+
check("custom-safety: bhet with no HTML tags flagged (belongs in extra_css)", W(validatePage(bhetCss), /no HTML tags/).length > 0);
|
|
245
|
+
// bbet with an unclosed <script> → swallow warning.
|
|
246
|
+
const bbetBad = cloneG();
|
|
247
|
+
bbetBad.settings.bbet = "<script>init()";
|
|
248
|
+
check("custom-safety: bbet unclosed <script> flagged", W(validatePage(bbetBad), /unbalanced <script>/).length > 0);
|
|
249
|
+
// element custom_css with layout props → break-layout warning (visual props alone do NOT trip it).
|
|
250
|
+
const layoutCss = cloneG();
|
|
251
|
+
layoutCss.page[0].children[0].specials = { text: "X", customAdvance: true, custom_css: "width:100%;display:flex;box-shadow:0 2px 8px rgba(0,0,0,.1);" };
|
|
252
|
+
check("custom-safety: custom_css layout props (width/display) flagged", W(validatePage(layoutCss), /layout prop/).length > 0, validatePage(layoutCss).warnings);
|
|
253
|
+
const visualCss = cloneG();
|
|
254
|
+
visualCss.page[0].children[0].specials = { text: "X", customAdvance: true, custom_css: "box-shadow:0 2px 8px rgba(0,0,0,.1);backdrop-filter:blur(8px);" };
|
|
255
|
+
check("custom-safety: visual-only custom_css is NOT flagged", W(validatePage(visualCss), /layout prop/).length === 0, validatePage(visualCss).warnings);
|
|
256
|
+
// a correct, fully-scoped custom setup → none of these warnings.
|
|
257
|
+
const clean = cloneG();
|
|
258
|
+
clean.settings.extra_css = "#w-btn1{transition:transform .3s}#w-btn1:hover{transform:translateY(-2px)}";
|
|
259
|
+
clean.settings.bhet = "<link href='https://fonts.googleapis.com/css2?family=Inter' rel='stylesheet'>";
|
|
260
|
+
clean.page[0].children[0].specials = { text: "X", customAdvance: true, custom_css: "box-shadow:0 8px 24px rgba(0,0,0,.08);" };
|
|
261
|
+
check("custom-safety: correctly-scoped custom triggers no safety warning", W(validatePage(clean), /UNSCOPED|unbalanced|no HTML tags|layout prop/).length === 0, validatePage(clean).warnings);
|
|
262
|
+
}
|
|
227
263
|
console.log("== validate: icon rendering (svg-mask needs background; font-class route is clean) ==");
|
|
228
264
|
{
|
|
229
265
|
const cloneG = () => JSON.parse(JSON.stringify(good));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.76",
|
|
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",
|