webcake-landing-mcp 1.0.30 → 1.0.32
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 +14 -14
- package/dist/domains/landing/elements/content.js +11 -3
- package/dist/domains/landing/elements/form.js +25 -4
- package/dist/domains/landing/elements/marketing.js +12 -2
- package/dist/domains/landing/guide.js +1 -1
- package/dist/domains/landing/instructions.js +1 -1
- package/dist/domains/landing/validate.js +32 -1
- package/package.json +1 -1
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.32",
|
|
4
|
+
"d": "08/06/2026",
|
|
5
|
+
"type": "Fixed",
|
|
6
|
+
"en": "get_element for select now marks specials.field_placeholder as required (the published renderer crashes without it), the element seed now emits a…",
|
|
7
|
+
"vi": "get_element cho select nay đánh dấu specials.field_placeholder là bắt buộc (renderer đã xuất bản sẽ crash nếu thiếu trường này), seed phần tử nay…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.0.31",
|
|
11
|
+
"d": "08/06/2026",
|
|
12
|
+
"type": "Fixed",
|
|
13
|
+
"en": "get_element for spin_wheel now correctly documents specials.code as a newline-delimited string (one line per segment in couponCode|Prize…",
|
|
14
|
+
"vi": "get_element cho spin_wheel nay ghi lại đúng specials.code là chuỗi phân cách bằng dấu xuống dòng (mỗi dòng một segment theo định dạng couponCode|Tên…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.0.30",
|
|
4
18
|
"d": "08/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Fixed",
|
|
27
41
|
"en": "The HTTP server's GET / route now serves the full HTML guide page to social and search crawlers (Facebook, Zalo, Twitter/X, LinkedIn, Slack,…",
|
|
28
42
|
"vi": "Route GET / của HTTP server nay phục vụ trang hướng dẫn HTML đầy đủ cho các crawler mạng xã hội và công cụ tìm kiếm (Facebook, Zalo, Twitter/X,…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.0.26",
|
|
32
|
-
"d": "07/06/2026",
|
|
33
|
-
"type": "Added",
|
|
34
|
-
"en": "The HTTP server now serves a pre-rendered 1200×630 PNG social card at GET /og.png; the guide page's og:image and twitter:image meta tags now point…",
|
|
35
|
-
"vi": "HTTP server nay phục vụ ảnh social card PNG được render sẵn 1200×630 tại GET /og.png; các meta tag og:image và twitter:image của trang hướng dẫn nay…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.25",
|
|
39
|
-
"d": "07/06/2026",
|
|
40
|
-
"type": "Changed",
|
|
41
|
-
"en": "currency has moved from options.currency to settings.currency in the page source model; new_page_skeleton now emits it in the correct location,…",
|
|
42
|
-
"vi": "currency đã được chuyển từ options.currency sang settings.currency trong mô hình nguồn trang; new_page_skeleton giờ xuất đúng vị trí,…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -152,9 +152,9 @@ export const CONTENT = [
|
|
|
152
152
|
{
|
|
153
153
|
type: "gallery", category: "content", container: false, defaultName: "Gallery",
|
|
154
154
|
summary: "Multi-image/video gallery with thumbnail strip. Content comes entirely from specials.media — this is NOT a container and has no children.",
|
|
155
|
-
useWhen: "Photo grids/galleries with several images or videos. No image API — fill specials.media with placeholder
|
|
155
|
+
useWhen: "Photo grids/galleries with several images or videos. No image API — fill specials.media with placeholder image objects (see media). NEVER use plain URL strings — the gallery reads item.link and renders blank for a string.",
|
|
156
156
|
keySpecials: {
|
|
157
|
-
media: "array of
|
|
157
|
+
media: "array of media OBJECTS (NOT plain URLs — the gallery reads item.link). Image item: {type:'image', link:'<url>', linkVideo:'', typeVideo:'youtube', imageCompression:true} — use a https://placehold.co/WxH URL for link if no real image. Video item: {type:'image', link:'<poster-url>', linkVideo:'<video-url>', typeVideo:'youtube'|'upload', imageCompression:true}.",
|
|
158
158
|
allowZoom: "'off' | 'carousel' | 'lightbox' — (config) zoom/lightbox mode when clicking an image.",
|
|
159
159
|
showNavigation: "boolean — (config) show prev/next navigation arrows.",
|
|
160
160
|
thumbnailAutoplay: "number (ms) | 'off' — (config) auto-advance thumbnails every N ms, or 'off' to disable.",
|
|
@@ -167,7 +167,15 @@ export const CONTENT = [
|
|
|
167
167
|
seed: (el) => {
|
|
168
168
|
seedPosition(el);
|
|
169
169
|
setBox(el, 350, 400);
|
|
170
|
-
|
|
170
|
+
// gallery media items are OBJECTS, not strings — the renderer reads item.link
|
|
171
|
+
// (a plain URL string renders blank). Shape mirrors the editor's seed.
|
|
172
|
+
el.specials.media = [1, 2, 3].map((n) => ({
|
|
173
|
+
type: "image",
|
|
174
|
+
link: imgPlaceholder(600, 400, String(n)),
|
|
175
|
+
linkVideo: "",
|
|
176
|
+
typeVideo: "youtube",
|
|
177
|
+
imageCompression: true,
|
|
178
|
+
}));
|
|
171
179
|
// gallery has NO children — content comes entirely from specials.media (gallery.js never reads vm.children)
|
|
172
180
|
},
|
|
173
181
|
},
|
|
@@ -109,19 +109,40 @@ export const FORM = [
|
|
|
109
109
|
useWhen: "Pick one from a list.",
|
|
110
110
|
keySpecials: {
|
|
111
111
|
field_name: "REQUIRED unique data key.",
|
|
112
|
-
field_placeholder: "placeholder/
|
|
112
|
+
field_placeholder: "REQUIRED — the disabled first-option label (the placeholder/prompt). The key is `field_placeholder`, NOT `placeholder`. The select renderer crashes if this is missing.",
|
|
113
113
|
default_value: "string (option id) to pre-select; 'default-none' = no pre-selection.",
|
|
114
114
|
defaultVariationId: "string — default product variation id registered on the parent form regardless of selection.",
|
|
115
115
|
defaultVariationQuantity: "number — quantity registered with defaultVariationId.",
|
|
116
116
|
ignoreOnHidden: "boolean — when CSS-hidden, remove this element's variations from the form total.",
|
|
117
117
|
isConnectSurvey: "boolean — link to a survey for conditional show/hide.",
|
|
118
118
|
connectedSurvey: "string — id of the connected survey.",
|
|
119
|
-
options: "array of
|
|
119
|
+
options: "array of option objects. The renderer builds each <option> from `id` and `name` ONLY — MINIMUM shape is {id, name} where name is BOTH the visible text and the submitted value. Do NOT use the HTML-style {label, value} — those keys are ignored and the option renders blank. Rich commerce options may also carry value, variations:[{id,quantity,price}], attrOnly/prodId/attrName/attrVal/attrs, quantityOnly/quantityProd/quantityValue, tags:[], toggleEvent, events_option:[] (show/hide, collapse, price/discount/shipping). See docs/element-specials-reference.md for the full option schema.",
|
|
120
120
|
},
|
|
121
121
|
seed: (el) => {
|
|
122
122
|
seedPosition(el);
|
|
123
123
|
setBox(el, 150, 36);
|
|
124
124
|
el.specials.field_name = `select_${el.id}`;
|
|
125
|
+
el.specials.field_placeholder = "Chọn...";
|
|
126
|
+
el.specials.default_value = "default-none";
|
|
127
|
+
},
|
|
128
|
+
example: {
|
|
129
|
+
id: "sel_attend", type: "select",
|
|
130
|
+
properties: { name: "Select", movable: true, sync: true },
|
|
131
|
+
responsive: {
|
|
132
|
+
desktop: { config: {}, styles: { top: 0, left: 0, width: 300, height: 44 } },
|
|
133
|
+
mobile: { config: {}, styles: { top: 0, left: 0, width: 280, height: 44 } },
|
|
134
|
+
},
|
|
135
|
+
// options use {id, name} — NOT {label, value}. field_placeholder is required.
|
|
136
|
+
specials: {
|
|
137
|
+
field_name: "attendance",
|
|
138
|
+
field_placeholder: "Bạn có tham dự không?",
|
|
139
|
+
default_value: "default-none",
|
|
140
|
+
options: [
|
|
141
|
+
{ id: "opt_yes", name: "Tôi sẽ tham dự" },
|
|
142
|
+
{ id: "opt_no", name: "Rất tiếc, tôi không thể đến" },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
runtime: {}, events: [],
|
|
125
146
|
},
|
|
126
147
|
},
|
|
127
148
|
{
|
|
@@ -147,7 +168,7 @@ export const FORM = [
|
|
|
147
168
|
ignoreOnHidden: "boolean — when CSS-hidden, exclude this element's variations from the form total.",
|
|
148
169
|
isConnectSurvey: "boolean — link to a survey for conditional show/hide.",
|
|
149
170
|
connectedSurvey: "string — id of the connected survey.",
|
|
150
|
-
options: "array of
|
|
171
|
+
options: "array of option objects, MINIMUM shape {id, name} (name = visible text; the renderer crashes on options that lack a string `name`). Do NOT use {label, value}. Same rich shape as select — additionally supports the tcb_auto_banking event type in events_option (sets the storecake_tcb payment gateway). See docs/element-specials-reference.md for the full option schema.",
|
|
151
172
|
},
|
|
152
173
|
seed: (el) => {
|
|
153
174
|
seedPosition(el);
|
|
@@ -169,7 +190,7 @@ export const FORM = [
|
|
|
169
190
|
ignoreOnHidden: "boolean — when hidden, exclude this element's variations from the form total.",
|
|
170
191
|
isConnectSurvey: "boolean — link to a survey for conditional show/hide.",
|
|
171
192
|
connectedSurvey: "string — id of the connected survey.",
|
|
172
|
-
options: "array of
|
|
193
|
+
options: "array of option objects, MINIMUM shape {id, name} (name = visible text; the renderer crashes on options that lack a string `name`). Do NOT use {label, value}. Same rich shape as select — events_option additionally supports 9 payment-gateway event types (tcb_auto_banking, xendit_banking, onepay_banking, mercadopago_banking, vnpay_banking, paymongo_banking, stripe_banking, paypal_banking, momopay_banking) that set the form's payment provider. See docs/element-specials-reference.md for the full option schema.",
|
|
173
194
|
},
|
|
174
195
|
seed: (el) => {
|
|
175
196
|
seedPosition(el);
|
|
@@ -105,9 +105,9 @@ export const MARKETING = [
|
|
|
105
105
|
summary: "Lucky-spin wheel with configurable prize segments and coupon codes. Can open a result popup after spinning and supports dataset-driven coupon lists.",
|
|
106
106
|
useWhen: "Gamified lead capture / promos. Users spin to win a coupon or prize.",
|
|
107
107
|
keySpecials: {
|
|
108
|
-
message: "
|
|
108
|
+
message: "string — result-popup message template shown after a spin (NOT segment labels). Supports placeholders {{coupon_text}}, {{coupon_code}}, {{spin_turn_left}}, {{coupon_codes}}.",
|
|
109
109
|
spin: "object — spin configuration (segment colors, angles, etc.).",
|
|
110
|
-
code: "
|
|
110
|
+
code: "string (NOT an array) — the segments. ONE LINE PER SEGMENT, each line `couponCode|Prize Name|percent`, lines joined by \\n. The visible label on each wheel slice is the middle field (Prize Name); percent is the win weight. e.g. 'SALE10|Giảm 10%|40\\nSALE50|Giảm 50%|10\\nMISS|Chúc may mắn|50'.",
|
|
111
111
|
dataType: "0 | 1 — 0=static codes, 1=dataset-driven codes.",
|
|
112
112
|
datasetId: "string — webcake dataset ID for coupon codes.",
|
|
113
113
|
codeDataset: "string — dataset column key for the coupon code.",
|
|
@@ -123,6 +123,16 @@ export const MARKETING = [
|
|
|
123
123
|
seedPosition(el);
|
|
124
124
|
setBox(el, 400, 400);
|
|
125
125
|
setStyle(el, "color", "rgba(255, 255, 255, 1)");
|
|
126
|
+
// `code` is a newline-delimited string, ONE segment per line `couponCode|Prize Name|percent`
|
|
127
|
+
// (the editor preview does code.split("\n") and crashes/renders blank if it is missing).
|
|
128
|
+
el.specials.code = [
|
|
129
|
+
"PRIZE1|Giải 1|20",
|
|
130
|
+
"PRIZE2|Giải 2|20",
|
|
131
|
+
"PRIZE3|Giải 3|20",
|
|
132
|
+
"MISS|Chúc may mắn|40",
|
|
133
|
+
].join("\n");
|
|
134
|
+
// `message` is the result-popup template (a string), NOT segment labels.
|
|
135
|
+
el.specials.message = "Chúc mừng! Bạn nhận được {{coupon_text}} (mã: {{coupon_code}}).";
|
|
126
136
|
},
|
|
127
137
|
},
|
|
128
138
|
{
|
|
@@ -78,7 +78,7 @@ SECTION BUILD HINTS (apply to whichever sections the chosen archetype uses)
|
|
|
78
78
|
RULES
|
|
79
79
|
- Visible content goes in "specials" (text-block.specials.text, image-block.specials.src…), NEVER in "styles".
|
|
80
80
|
- Colors as rgba(r,g,b,a). fontSize/borderWidth/top/left/width/height are NUMBERS (px).
|
|
81
|
-
- IMAGES: a real landing page has images (hero/product shot, feature icons, about photo). There is NO image API yet, so set image-block specials.src to a PLACEHOLDER URL sized to the box: "https://placehold.co/<width>x<height>". NEVER leave src empty — it renders blank and the page looks broken. gallery.media = array of
|
|
81
|
+
- IMAGES: a real landing page has images (hero/product shot, feature icons, about photo). There is NO image API yet, so set image-block specials.src to a PLACEHOLDER URL sized to the box: "https://placehold.co/<width>x<height>". NEVER leave src empty — it renders blank and the page looks broken. gallery.media = array of OBJECTS {type:'image', link:'<placeholder-url>', linkVideo:'', typeVideo:'youtube', imageCompression:true} (NOT plain URL strings — the gallery reads item.link); video.specials.img = a poster placeholder. The user replaces these later.
|
|
82
82
|
- CONTRAST: text must contrast with the section background (dark text on light sections, light text on dark sections). Don't put light-gray text on white or faint text on a dark background.
|
|
83
83
|
- movable:false for section/slide/grid-item/popup; otherwise true. runtime is always {}.
|
|
84
84
|
- Every form input MUST have a unique specials.field_name.
|
|
@@ -20,6 +20,6 @@ MODEL (essentials):
|
|
|
20
20
|
- CENTERING (the #1 layout defect — do the math, don't eyeball): to center a box compute left = round((canvas - width)/2) — 960 desktop, 420 mobile. textAlign:center only centers text inside the box, not the box itself. For a row of N items, center the whole row block (startLeft = round((canvas - (N*item + (N-1)*gap))/2)). Keep 0 ≤ left and left+width ≤ canvas on each breakpoint.
|
|
21
21
|
- 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.
|
|
22
22
|
- 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).
|
|
23
|
-
- IMAGES: include them (hero/product, feature icons, about photo). No image API yet → set image-block specials.src to a PLACEHOLDER sized to the box: https://placehold.co/<width>x<height> (gallery.media = array of
|
|
23
|
+
- IMAGES: include them (hero/product, feature icons, about photo). No image API yet → set image-block specials.src 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.
|
|
24
24
|
|
|
25
25
|
Start by calling get_generation_guide. Tools: get_generation_guide, list_elements, get_element, new_element, new_page_skeleton, get_page_schema, validate_page, list_organizations, create_page, list_pages, get_page, update_page.`;
|
|
@@ -26,6 +26,11 @@ const ELEMENT_TARGET_ACTIONS = new Set([
|
|
|
26
26
|
"show_hide_element", "change_tab", "collapse",
|
|
27
27
|
]);
|
|
28
28
|
const TOP_LEVEL_TYPES = new Set(["section", "dynamic_page", "popup"]);
|
|
29
|
+
// Fields whose renderer builds each <option> from `option.name`. A missing name
|
|
30
|
+
// crashes the published renderer (radio/checkbox-group call .replace/.normalize on
|
|
31
|
+
// it; select shows a blank option). The correct option shape is {id, name} — NOT
|
|
32
|
+
// the HTML-style {label, value}.
|
|
33
|
+
const OPTION_NAME_FIELDS = new Set(["select", "radio", "checkbox-group"]);
|
|
29
34
|
// Fixed canvas reference (matches vocab CANVAS) used for the layout/bounds check.
|
|
30
35
|
const CANVAS_DESKTOP = 960;
|
|
31
36
|
const CANVAS_MOBILE = 420;
|
|
@@ -108,10 +113,36 @@ export function validatePage(input) {
|
|
|
108
113
|
}
|
|
109
114
|
// form fields need field_name
|
|
110
115
|
if (type && FIELD_TYPES.has(type)) {
|
|
111
|
-
const
|
|
116
|
+
const specials = node.specials;
|
|
117
|
+
const fn = specials?.field_name;
|
|
112
118
|
if (!fn || typeof fn !== "string" || fn.trim() === "") {
|
|
113
119
|
warnings.push(`${path} (${type}): form input should have a unique specials.field_name.`);
|
|
114
120
|
}
|
|
121
|
+
// `field_placeholder` is the ONLY placeholder key the renderer reads. A stray
|
|
122
|
+
// `placeholder` renders blank; a select with no field_placeholder crashes the
|
|
123
|
+
// published renderer (unescapeHTML(undefined)).
|
|
124
|
+
if (specials && typeof specials === "object") {
|
|
125
|
+
const hasFieldPlaceholder = typeof specials.field_placeholder === "string";
|
|
126
|
+
if (typeof specials.placeholder === "string" && !hasFieldPlaceholder) {
|
|
127
|
+
warnings.push(`${path} (${type}): uses specials.placeholder — the renderer reads specials.field_placeholder. Rename "placeholder" → "field_placeholder".`);
|
|
128
|
+
}
|
|
129
|
+
if (type === "select" && !hasFieldPlaceholder) {
|
|
130
|
+
errors.push(`${path} (select): needs a string specials.field_placeholder (the select renderer crashes without it).`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// select/radio/checkbox-group render each option from option.name; a missing
|
|
134
|
+
// name crashes the renderer (radio/checkbox-group) or renders blank (select).
|
|
135
|
+
if (OPTION_NAME_FIELDS.has(type) && Array.isArray(specials?.options)) {
|
|
136
|
+
specials.options.forEach((opt, oi) => {
|
|
137
|
+
if (typeof opt?.name !== "string" || opt.name.trim() === "") {
|
|
138
|
+
const keys = opt && typeof opt === "object" ? Object.keys(opt) : [];
|
|
139
|
+
const hint = keys.includes("label") || keys.includes("value")
|
|
140
|
+
? ` Use {id, name} — not {label, value} (found keys: ${keys.join(", ")}).`
|
|
141
|
+
: "";
|
|
142
|
+
errors.push(`${path} (${type}): specials.options[${oi}] needs a non-empty string "name" (the visible option text).${hint}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
115
146
|
}
|
|
116
147
|
// collect events
|
|
117
148
|
if (Array.isArray(node.events)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
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
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|