webcake-storefront-mcp 1.31.4 → 1.31.6

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 CHANGED
@@ -1,4 +1,8 @@
1
- # 🏪 WebCake Storefront MCP
1
+ <p align="center">
2
+ <img src="./assets/logo.svg" alt="WebCake Storefront MCP" width="96" height="96">
3
+ </p>
4
+
5
+ <h1 align="center">WebCake Storefront MCP</h1>
2
6
 
3
7
  **English** · [Tiếng Việt](./README.vi.md)
4
8
 
@@ -78,7 +82,7 @@ An MCP (Model Context Protocol) server that teaches AI agents the **WebCake/Stor
78
82
  page source; `build_page` creates the page and saves it, and `publish_site` makes the whole site live.
79
83
 
80
84
  Beyond page authoring, it exposes your real store: pages & custom code, products, orders, collections,
81
- blog articles, promotions, combos, themes, customers, and automation — **~101 tools** in total.
85
+ blog articles, promotions, combos, themes, customers, and automation — **~280 tools** in total.
82
86
 
83
87
  | Method | Best for | Auth |
84
88
  |--------|----------|------|
@@ -166,7 +170,7 @@ in chat via `update_auth` and `switch_site` — saved to a local config file at
166
170
 
167
171
  ## 🧰 The tools at a glance
168
172
 
169
- ~101 tools. The headline group **builds pages**; the rest read and edit your live store.
173
+ ~280 tools. The headline group **builds pages**; the rest read and edit your live store.
170
174
 
171
175
  | Group | Tools | Needs |
172
176
  |-------|-------|-------|
package/README.vi.md CHANGED
@@ -1,4 +1,8 @@
1
- # 🏪 WebCake Storefront MCP
1
+ <p align="center">
2
+ <img src="./assets/logo.svg" alt="WebCake Storefront MCP" width="96" height="96">
3
+ </p>
4
+
5
+ <h1 align="center">WebCake Storefront MCP</h1>
2
6
 
3
7
  [English](./README.md) · **Tiếng Việt**
4
8
 
@@ -78,7 +82,7 @@ WebCake/StoreCake (BuilderX)** và kết nối tới backend. AI tạo nguồn t
78
82
  `build_page` tạo + lưu trang, `publish_site` đưa cả site lên live.
79
83
 
80
84
  Ngoài dựng trang, nó mở ra cả store thật: trang & custom code, sản phẩm, đơn hàng, collection,
81
- bài blog, khuyến mãi, combo, theme, khách hàng, automation — **~101 tool** tổng cộng.
85
+ bài blog, khuyến mãi, combo, theme, khách hàng, automation — **~280 tool** tổng cộng.
82
86
 
83
87
  | Cách | Hợp cho | Auth |
84
88
  |--------|----------|------|
@@ -165,7 +169,7 @@ trong chat bằng `update_auth` và `switch_site` — lưu vào file cấu hình
165
169
 
166
170
  ## 🧰 Bộ công cụ tổng quan
167
171
 
168
- ~101 tool. Nhóm chủ lực **dựng trang**; phần còn lại đọc và sửa store thật của bạn.
172
+ ~280 tool. Nhóm chủ lực **dựng trang**; phần còn lại đọc và sửa store thật của bạn.
169
173
 
170
174
  | Nhóm | Tool | Cần |
171
175
  |-------|-------|-------|
package/dist/api.js CHANGED
@@ -150,19 +150,19 @@ export class WebcakeCmsApi {
150
150
  /** Create a page. The backend creates the page AND its source in one call, so `source`
151
151
  * is REQUIRED and must be a JSON string (stringified here if an object is passed).
152
152
  * `slug`/`is_homepage` are NOT applied at create — set them afterwards via updatePage. */
153
- createPage(params) {
153
+ createPage(params, opts) {
154
154
  const body = { ...params };
155
155
  if (body.source != null && typeof body.source !== "string")
156
156
  body.source = JSON.stringify(body.source);
157
157
  if (body.source == null)
158
158
  body.source = JSON.stringify({ sections: [] });
159
- return this.request("POST", `/api/v1/site/${this.siteId}/page`, { body });
159
+ return this.request("POST", `/api/v1/site/${this.siteId}/page`, { body, timeout: opts?.timeout });
160
160
  }
161
161
  updatePage(pageId, params) {
162
162
  return this.request("POST", `/api/v1/site/${this.siteId}/${pageId}/update_page`, { body: params });
163
163
  }
164
- updatePageSource(pageId, params) {
165
- return this.request("POST", `/api/v1/site/${this.siteId}/${pageId}/update_page_source`, { body: params });
164
+ updatePageSource(pageId, params, opts) {
165
+ return this.request("POST", `/api/v1/site/${this.siteId}/${pageId}/update_page_source`, { body: params, timeout: opts?.timeout });
166
166
  }
167
167
  deletePage(params) {
168
168
  return this.request("POST", `/api/v1/site/${this.siteId}/delete_page`, { body: params });
@@ -246,11 +246,20 @@ export class WebcakeCmsApi {
246
246
  });
247
247
  return { rebuilt: pages.length };
248
248
  }
249
- /** Publish the site live. /publish runs the full "save" pipeline, which OVERWRITES
250
- * site.settings with the body's `settings` so we send the CURRENT settings (else
251
- * they'd be nulled, disabling use_store/use_blog/etc.). Other collections default to [].
252
- * We ALSO rebuild every page's CSS first (rebuildSiteCss) because /publish alone does
253
- * not regenerate the storefront's dynamic CSS without it the live site looks unstyled. */
249
+ /** Publish the site live. /publish snapshots the pages in the body into the LIVE version —
250
+ * exactly like the builder's "Xuất bản" button (PagePublish.vue): it POSTs every page as
251
+ * `{ source, id, type, slug, is_homepage, settings }` plus a `changes` map. Sending NO
252
+ * pages (the old behaviour) made the backend publish nothing, so the storefront stayed on
253
+ * "Trang chưa giao diện hoặc chưa xuất bản" and every page kept is_build=false.
254
+ * We also (a) keep the CURRENT site.settings so /publish doesn't null the theme + data-source
255
+ * flags (use_store/use_blog/…), and (b) rebuild each page's compiled CSS first (rebuildSiteCss),
256
+ * because /publish alone does not regenerate the storefront's dynamic CSS.
257
+ * Global header/footer are already embedded inside each page's source, so they publish with
258
+ * the page; the global_* arrays stay empty (empty = leave server-side globals untouched).
259
+ * Pass `domain` (the site's live URL, e.g. https://<slug>.webcake.me) — builderx_spa sends
260
+ * `domain: this.link` and without it the backend publishes to an EXPIRING preview. The
261
+ * publish_site tool fills it from resolvePreviewUrl. Final body mirrors PagePublish.vue:
262
+ * { pages, settings, changes, domain, global_sources:[], global_sections:[], page_contents:[] }. */
254
263
  async publishSite(params = {}) {
255
264
  let settings = params.settings;
256
265
  if (settings === undefined) {
@@ -259,11 +268,46 @@ export class WebcakeCmsApi {
259
268
  // The save pipeline stores site.settings as a JSON STRING — an object body is
260
269
  // rejected (422). Stringify unless the caller already passed a string.
261
270
  const settingsStr = typeof settings === "string" ? settings : JSON.stringify(settings || {});
271
+ // Collect every page WITH its saved source so the backend has something to publish.
272
+ let pages = params.pages;
273
+ let changes = params.changes;
274
+ if (!Array.isArray(pages)) {
275
+ const res = await this.listPages();
276
+ const list = (res && res.data) || res || [];
277
+ pages = (Array.isArray(list) ? list : [])
278
+ .map((p) => {
279
+ const src = p && p.source && p.source.source;
280
+ if (src == null || src === "")
281
+ return null;
282
+ return {
283
+ id: p.id,
284
+ source: typeof src === "string" ? src : JSON.stringify(src),
285
+ type: p.type,
286
+ slug: p.slug,
287
+ is_homepage: p.is_homepage === true,
288
+ settings: JSON.stringify(p.settings || {}),
289
+ };
290
+ })
291
+ .filter(Boolean);
292
+ }
293
+ if (changes === undefined) {
294
+ changes = {};
295
+ for (const p of pages)
296
+ changes[p.id] = 1;
297
+ }
262
298
  // Regenerate compiled CSS for every page before publishing (no-op-safe on failure).
263
299
  await this.rebuildSiteCss(settingsStr).catch(() => { });
264
300
  return this.request("POST", `/api/v1/site/${this.siteId}/publish`, {
265
- body: { global_sources: [], global_sections: [], page_contents: [], ...params, settings: settingsStr },
266
- timeout: 60000,
301
+ body: {
302
+ pages,
303
+ changes,
304
+ global_sources: [],
305
+ global_sections: [],
306
+ page_contents: [],
307
+ ...params,
308
+ settings: settingsStr,
309
+ },
310
+ timeout: 120000,
267
311
  });
268
312
  }
269
313
  uploadImageBase64({ base64, content_type } = {}) {
@@ -163,6 +163,13 @@ export function buildElement(type, opts = {}) {
163
163
  node.bindings = opts.bindings;
164
164
  if (opts.events && !node.events)
165
165
  node.events = opts.events;
166
+ // Responsive CASCADE overrides: sparse per-breakpoint diffs ({ bp2|bp3|bp4: { style?, config? } }).
167
+ // Stashed on runtime.responsive; finalizeForRender cascades them bp1→bp4 (each smaller
168
+ // breakpoint inherits the resolved larger one, then applies its own diff).
169
+ if (opts.responsive && typeof opts.responsive === "object") {
170
+ node.runtime = node.runtime || {};
171
+ node.runtime.responsive = opts.responsive;
172
+ }
166
173
  if (Array.isArray(node.bindings) && node.bindings.length)
167
174
  node.bindings = normalizeBindings(node.bindings);
168
175
  if (Array.isArray(node.events) && node.events.length)
@@ -2,6 +2,62 @@
2
2
  // page model well enough to author pages that actually render.
3
3
  export const BUILD_GUIDE = `# BuilderX page authoring guide
4
4
 
5
+ ## DESIGN SYSTEM — lock this FIRST, every build (this is what makes free composition look DESIGNED)
6
+ There are NO page templates here and NO seeded data — you COMPOSE every page from elements
7
+ based on the user's actual goal. To make that composition look intentional (not a random pile
8
+ of default elements), LOCK a small design system before you place anything, and reuse it on
9
+ EVERY section and EVERY page:
10
+ - PALETTE — from the site THEME matrix vars \`var(--color_RC)\` (see Colours below). Pick: page
11
+ background, body text, one accent (for CTAs/prices/highlights), one or two soft section tints.
12
+ - TYPE SCALE — h1 40–56px / fontWeight 700, h2 28–34px / 600, body 16–18px with lineHeight "1.6",
13
+ small/muted 13–14px. Use the SAME sizes everywhere; don't invent a new size per section.
14
+ - SPACING — everything on an 8px grid: 8 / 16 / 24 / 32 / 48 / 64. Section padding 64–96px,
15
+ rowGap 16–24 inside sections. Generous whitespace = premium; cramped = cheap.
16
+ - ONE BUTTON SPEC — decide it once and reuse: e.g. \`{ background:"var(--color_24)",
17
+ color:"var(--color_00)", borderRadius:"8px", fontWeight:"600", height:48 }\`. Same button on
18
+ every page.
19
+ - ONE CARD SPEC + ONE CONTENT WIDTH + ONE RADIUS — reuse the same card padding/radius/shadow and
20
+ the same centred content width across sections. Consistency = looks professionally designed.
21
+
22
+ ⚠️ CONTRAST is the #1 ugliness/bug — get it right:
23
+ - The BRAND row \`var(--color_2C)\` goes LIGHT → DARK: \`var(--color_20)\` (lightest tint) …
24
+ \`var(--color_24)\` (darkest brand).
25
+ - Button / CTA / price BACKGROUNDS must use \`var(--color_24)\` (dark brand) WITH a white label
26
+ \`var(--color_00)\`. This is readable on every theme.
27
+ - NEVER put a white label on \`var(--color_20)\` — on many themes that's a pale tint (e.g. #f2decc)
28
+ and white text becomes INVISIBLE. Use \`var(--color_20)\`/\`var(--color_21)\` ONLY as soft section
29
+ tints / backgrounds, never as a button background under white text.
30
+ - Body text = \`var(--color_04)\` (near-black). Page background = \`var(--color_00)\` (white).
31
+
32
+ IMAGES (or the page looks broken/empty):
33
+ - Every image needs a WebCake-CDN url (search_images → cdn_url, or upload_images), or it won't
34
+ render — the storefront whitelists image domains.
35
+ - HERO must be a REAL full-width \`image\` element (width:"100%"), NOT a CSS \`background:url(...)\`
36
+ shorthand — the renderer ignores the shorthand and you get a blank band.
37
+ - grid-product cards show the PRODUCT-LEVEL image, so create products WITH a product image or the
38
+ cards are blank.
39
+
40
+ COLOR & SPACING DISCIPLINE:
41
+ - ONE accent, used sparingly (CTAs, prices, a few highlights) — not on everything.
42
+ - Alternate plain (\`var(--color_00)\`) and softly-tinted (\`var(--color_01)\`/\`var(--color_20)\`)
43
+ section backgrounds to give the page rhythm.
44
+ - Reuse the SAME content width + radius + button across sections.
45
+
46
+ COMPOSE, DON'T TEMPLATE — there are no templates and no seeded data, so for each page:
47
+ 1. Pick a SECTION ARCHETYPE for the page type (propose it, then confirm with the user via
48
+ get_intake_guide before building):
49
+ - home: hero · category tiles · featured products (grid-product) · story/USP · social proof · CTA
50
+ - category (type store): banner + breadcrumb + grid-product (+ optional filter sidebar)
51
+ - product detail (type store): 2-col [gallery | info: name/price/qty/add-to-cart/buy-now/trust]
52
+ + description band + related grid-product
53
+ - cart (type store): 2-col [cart-items | order summary card]
54
+ - checkout (type store): 2-col [form{form_order} | order summary]
55
+ - thank-you (type store): centred confirmation + order-items + continue-shopping
56
+ 2. Build each section from elements (new_section / new_row / new_element) using the locked
57
+ design system above.
58
+ 3. CREATE real product data + product images (create_product_category / create_product, images
59
+ from search_images/upload_images) so the dataset bindings (grid-product, product detail) resolve.
60
+
5
61
  ## Page shape
6
62
  A page's content is a single JSON object: \`{ "sections": [ <section>, ... ] }\`.
7
63
  - Save it via build_page (new page) or update_page_source (existing page).
@@ -70,10 +126,14 @@ already in this shape — see the \`responsive\` field on get_page_element/updat
70
126
  fontSize ("16px"), fontWeight, textAlign, border*, boxShadow, etc.
71
127
  - \`runtime.config.heightUnit\`: "auto" lets content set height (default for text/image).
72
128
  - Colours: use the site THEME matrix vars \`var(--color_RC)\` (R=row 0-4, C=col 0-4). Row 0 is
73
- greyscale: \`var(--color_00)\`=WHITE … \`var(--color_04)\`=BLACK. Row 2 is the BRAND row:
74
- \`var(--color_20)\` is the brand primary. So TEXT = \`var(--color_04)\` (NOT color_00, that's white →
75
- invisible), ACCENT/buttons/prices = \`var(--color_20)\` (darker shade \`var(--color_24)\` if you need
76
- white-on-accent contrast), page background = \`var(--color_00)\`. Plain hex/rgba() also work.
129
+ greyscale: \`var(--color_00)\`=WHITE … \`var(--color_04)\`=BLACK. Row 2 is the BRAND row, getting
130
+ DARKER left→right: \`var(--color_20)\` (lightest brand tint) \`var(--color_24)\` (darkest brand).
131
+ So TEXT = \`var(--color_04)\` (NOT color_00 = white → invisible). PAGE BACKGROUND = \`var(--color_00)\`.
132
+ BUTTON / CTA BACKGROUND = \`var(--color_24)\` (the darkest brand shade) WITH a white label
133
+ (\`color:var(--color_00)\`) — this is readable on EVERY theme. ⚠️ Do NOT put a white label on
134
+ \`var(--color_20)\`: on many themes color_20 is a pale tint (e.g. #f2decc) and white text vanishes.
135
+ Use \`var(--color_20)\`/\`var(--color_21)\` only as a LIGHT section tint / soft background, never as a
136
+ button background under white text. PRICES / accents = \`var(--color_24)\` too. Plain hex/rgba() also work.
77
137
 
78
138
  ## Make it look DESIGNED (not plain) — do this, every page
79
139
  A bare stack of default elements looks unfinished. Apply real styling:
@@ -85,11 +145,14 @@ A bare stack of default elements looks unfinished. Apply real styling:
85
145
  - TYPOGRAPHY hierarchy: h1 40–56px / fontWeight 700, h2 28–34px / 600, body 16–18px with
86
146
  lineHeight "1.6", muted color for sub-text. Center hero text (textAlign:"center").
87
147
  - BUTTONS HAVE NO DEFAULT COLOUR — you MUST style them or they look like plain text:
88
- \`{ type:"button", opts:{ text:"Mua ngay", style:{ background:"var(--color_20)", color:"var(--color_00)",
89
- borderRadius:"8px", fontWeight:"600", height:48 } } }\` (brand background, white label).
90
- - HERO: prefer a section with a background image + an overlay heading/sub/button on top
91
- (set section_opts.style.background or a full-width image, then text centered), rather than a
92
- small image stacked above text.
148
+ \`{ type:"button", opts:{ text:"Mua ngay", style:{ background:"var(--color_24)", color:"var(--color_00)",
149
+ borderRadius:"8px", fontWeight:"600", height:48 } } }\` (DARK brand background, white label — always readable).
150
+ - HERO: build it as a section whose FIRST child is a full-width \`image\` element (the background photo,
151
+ width:"100%", height ~480), then overlay the heading/sub/button on top. ⚠️ Do NOT rely on a CSS
152
+ \`background:"linear-gradient(...), url(...)"\` SHORTHAND on the section — the storefront renderer
153
+ ignores the shorthand and the photo won't show (you get a blank/flat band). A real \`image\` element
154
+ (src = a WebCake-CDN url) always renders; for a text overlay put a semi-transparent \`rectangle\`
155
+ over it and the text above that.
93
156
  - PRODUCT GRID: grid-product SELF-RENDERS the image+name+price card. Its look is
94
157
  INDUSTRY-DEPENDENT (surveyed fashion/kids/cosmetics/food/electronics) — only \`bold price\`,
95
158
  \`responsive\` collapse and \`show_original_price/show_discount_on_price\` are near-universal;
@@ -99,19 +162,31 @@ A bare stack of default elements looks unfinished. Apply real styling:
99
162
  and \`opts.specials\`: { products_per_load:8-36, on_hover:"zoom"|"swap", show_rating, show_ribbon }.
100
163
  (There are NO cardBorderRadius/cardBoxShadow keys.) For variations use \`attr\` elements
101
164
  (attrName:"auto"); steppers are \`quantity-input\` (defaults spinner:"hide-spin").
102
- - BRAND COLOURS: text = var(--color_04) (black), accent = var(--color_20) (brand) for buttons,
103
- prices, highlights — consistent accent = looks intentional. (var(--color_00) is WHITE — only
104
- for backgrounds / labels on the accent, never for text on a white surface.)
165
+ - BRAND COLOURS: text = var(--color_04) (black); accent = var(--color_24) (DARK brand) for button
166
+ backgrounds, prices, highlights — consistent accent = looks intentional. (var(--color_00) is WHITE
167
+ — only for page backgrounds / labels ON the dark accent, never for text on a white surface; and a
168
+ white label on the pale var(--color_20) tint is the #1 invisible-text bug.)
105
169
  - IMAGES: must be WebCake-CDN urls (search_images cdn_url / upload_images), else they won't show.
170
+ A product card (grid-product) shows the PRODUCT-LEVEL thumbnail — if a product only has variation
171
+ images and no product image, its card is blank, so set product images when you create products.
106
172
 
107
173
  ## Responsive breakpoints
108
174
  The four breakpoints (largest → smallest), keyed bp1..bp4, are:
109
175
  - \`bp1\` ≥1320px (desktop, the base) · \`bp2\` 993–1319 (laptop) · \`bp3\` 641–992 (tablet) · \`bp4\` 320–640 (mobile).
110
- For NEW pages you author once in \`runtime\` (desktop) and build_page copies it to all four
111
- breakpoints automatically the page renders identically across devices. To make a node
112
- look DIFFERENT on a smaller screen, set that breakpoint's key explicitly, e.g.
113
- \`node.bp4 = { style: { fontSize: "20px" }, config: {...} }\`. (There is no \`tablet\`/\`laptop\`
114
- key only bp1..bp4.)
176
+ For NEW pages you author once in \`runtime\` (the bp1/desktop base); on save the build expands
177
+ it into bp1..bp4. By default all four are the same (renders identically across devices), PLUS
178
+ sections re-centre their grid per breakpoint and multi-column rows auto-collapse (4→2→1 cols).
179
+
180
+ RESPONSIVE CASCADE (reason about each breakpoint, don't hand-copy): to make a node look
181
+ different on smaller screens, pass \`opts.responsive\` = SPARSE per-breakpoint diffs and the
182
+ build cascades them bp1→bp4 (each smaller breakpoint inherits the resolved larger one, then
183
+ applies only the keys you give). You only write what CHANGES:
184
+ \`new_element("text", { text:"...", style:{ fontSize:"52px" },
185
+ responsive:{ bp3:{ style:{ fontSize:"38px" } }, bp4:{ style:{ fontSize:"28px", textAlign:"center" } } } })\`
186
+ → bp1/bp2 = 52px; bp3 = 38px; bp4 = 28px centred. Works on new_element/new_section/new_row and
187
+ build_page children (in each child's \`opts.responsive\`). Use it for hero headline sizes, padding,
188
+ columnGap, hiding/realigning per device. (There is no \`tablet\`/\`laptop\` key — only bp1..bp4.)
189
+ You can still hand-set a final \`node.bpN\` when editing an already-saved page.
115
190
 
116
191
  ## Content & data
117
192
  - Text: \`specials.text\` (HTML allowed), \`specials.tag\` ("h1".."p").
@@ -203,13 +278,15 @@ set \`type\` accordingly so the binding source is turned on. A binding target li
203
278
  ## Build the WHOLE storefront — every page to the SAME standard (NOT just the home page)
204
279
  A shop is multi-page. Build EACH page to a real e-commerce standard with the same palette,
205
280
  spacing and header/footer — never leave the home page rich and the rest as bare stubs.
206
- \`scaffold_store_pages\` now builds FULLY-DESIGNED, palette-aware store pages by DEFAULT
207
- (style:"rich") banner+breadcrumb+styled grid (Category), 2-col gallery|info with price/
208
- quantity/add-to-cart+trust badges+related (Product), 2-col cart|summary, 2-col checkout
209
- form|summary, centred thank-you and auto-wires navigation between them. Then add chrome with
210
- \`scaffold_global_sections({ brand, contact })\` for a designed Header+Footer in one call. Pass
211
- style:"minimal" only if you want bare stubs to hand-build. The per-page recipe each rich page
212
- follows (so you can match it when editing or building extra pages):
281
+ There are NO page templates and NO scaffold shortcuts: you COMPOSE each store page from elements
282
+ (new_section / new_row / new_element → build_page) using the locked design system, then add a
283
+ GLOBAL header/footer via create_global_section. Create the store pages whose slugs the storefront
284
+ expects so navigation resolves (clicking a product/category/cart lands on a real page, not a 404):
285
+ Category (slug "collections"), Product (slug "products"), Cart (slug "cart"), Checkout (slug
286
+ "checkout"), Thank-you (slug "complete") all build_page type:'store' (auto-enables use_store);
287
+ plus optional member (login/register/profile, type 'member') and blog (blog/post, type 'blog')
288
+ pages. Wire navigation between them yourself with open_page events (add-to-cart → cart, cart →
289
+ checkout, order success → thank-you, thank-you → home). The per-page archetype to compose:
213
290
  - Category (collections, type store): banner + heading + grid-product (+ optional intro/CTA).
214
291
  - Product detail (products, type store): 2-col [product-gallery | info: text-dataset
215
292
  product::product_name + product_price/original_price + short_description, quantity-input,
@@ -237,9 +314,8 @@ create the globals — they embed into each page's source. If you later overwrit
237
314
  later with update_global_section_element(s) and it updates on every page at once.
238
315
 
239
316
  ## Popups (newsletter / promo / age-gate)
240
- A popup is a GLOBAL SOURCE, not a page section. FAST PATH: \`scaffold_popup({ brand, headline, offer })\`
241
- builds + saves a designed newsletter popup (heading + text + email form + close button, centred)
242
- and returns its id. To build one by hand:
317
+ A popup is a GLOBAL SOURCE, not a page section. Compose it from elements (there is no scaffold
318
+ shortcut), then save it as a "popup" global source:
243
319
  1. Build the popup node: new_element("popup", { children:[…heading,text,form,close button…], style:{ width:480, background:"#fff", borderRadius:"12px" }, config:{ popupHorizontalPosition:"center", popupVerticalPosition:"center" }, specials:{ effect:"fade-in", timeAnim:0.5 } }).
244
320
  - SIZE + POSITION live in the popup's runtime style/config (width/height/background + popupHorizontalPosition/popupVerticalPosition), NOT in specials. createPopup seeds a centred-modal default.
245
321
  - TRIGGER (auto-open) lives in SPECIALS: \`openPopupAction:"openPopupWithTime"\` + \`timeOpenPopup:<seconds>\` for a delay; \`page_ids:[…]\` to limit which pages it shows on; \`effect\`/\`timeAnim\` for the animation. (There is no exit-intent/only-once flag in the node — those are app settings.)
@@ -252,9 +328,25 @@ List existing popups with list_global_sources({component:"popup"}); edit via upd
252
328
 
253
329
  ## Workflow (do this every time)
254
330
  1. Intake: confirm goal, brand, colours, sections wanted (ask 3-5 questions if unclear).
255
- 2. list_elements / get_element to pick the right component types.
331
+ 2. list_elements / get_element to pick the right component types; get_page_schema for the exact
332
+ node contract ({ sections:[ { id,type,specials,runtime:{style,config},children,events,bindings } ] }).
256
333
  3. Build sections with new_section (or new_element for one node), fill content.
257
334
  4. validate_page — fix every error and review warnings.
258
335
  5. build_page with dry_run:true first → review → dry_run:false to persist.
259
336
  6. For existing pages, prefer surgical edits (update_page_element) over full rewrites.
337
+
338
+ ## LARGE PAGES — avoid timeout (use the DRAFT flow)
339
+ A multi-section page sent in one build_page can be a huge request and hit the 15s timeout.
340
+ For anything beyond a small/simple page, build it incrementally with the durable DRAFT flow:
341
+ 1. start_page_draft({ name, slug, type?, is_homepage?, seo? }) → draft_id (LOCAL, no network).
342
+ 2. add_draft_section({ draft_id, section }) ONCE per section — each is cached locally (safe,
343
+ can't time out) and quick-validated. Build the section with new_section first.
344
+ 3. commit_page_draft({ draft_id, dry_run:true }) to validate the whole page, then dry_run:false
345
+ to persist. It creates the page then appends sections ONE AT A TIME (small requests), saving
346
+ progress after each — and RESUMES from where it stopped if a request is interrupted (just call
347
+ commit_page_draft again). clear_page_draft discards a draft.
348
+ The draft cache is DISPOSABLE — Redis on the remote server when REDIS_URL is set, in-memory
349
+ otherwise, with a sliding ~2h TTL. A lost draft (expiry / cache restart) just means re-sending
350
+ the sections via start_page_draft + add_draft_section (or build_page directly) — never a failure.
351
+ Keep build_page for small/simple pages.
260
352
  Always keep Vietnamese text with full diacritics; reply in the user's language.`;
@@ -0,0 +1,156 @@
1
+ // Authoritative JSON Schema (Draft 2020-12) for the BuilderX storefront page SOURCE — the
2
+ // `{ sections: [...] }` object that build_page / update_page_source persist. Mirrors what
3
+ // webcake-landing-mcp exposes via get_page_schema, but for THIS product's CSS-GRID model
4
+ // (not absolute top/left). It documents the AUTHORING shape an AI emits via new_section /
5
+ // new_element (nodes carry a staging `runtime:{style,config}`; build_page expands that into
6
+ // the per-breakpoint bp1..bp4 keys the storefront actually renders). Use it as the structural
7
+ // contract; validate_page enforces the semantic rules (unique ids, valid grids, form fields).
8
+ export const PAGE_SCHEMA = {
9
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10
+ $id: "https://webcake.io/schemas/storefront-page.json",
11
+ title: "BuilderX storefront page source",
12
+ description: "A page's content: a vertical stack of section nodes. Build nodes with new_section/new_element (the factory fills correct defaults) — never hand-write a node from scratch.",
13
+ type: "object",
14
+ required: ["sections"],
15
+ additionalProperties: true,
16
+ properties: {
17
+ sections: {
18
+ type: "array",
19
+ description: "Top-level bands, rendered top→bottom. Only `section` nodes are valid here.",
20
+ items: { $ref: "#/$defs/node" },
21
+ },
22
+ },
23
+ $defs: {
24
+ node: {
25
+ type: "object",
26
+ required: ["id", "type"],
27
+ additionalProperties: true,
28
+ properties: {
29
+ id: {
30
+ type: "string",
31
+ description: "Unique per page. Prefix = TYPE- (e.g. 'TEXT-ab12cd34', 'SECTION-...'). Minted by the factory; never reuse.",
32
+ },
33
+ type: {
34
+ type: "string",
35
+ description: "Element type from list_elements (e.g. section, container, text, image, button, grid-product, form, input, cart-items, menu).",
36
+ },
37
+ name: { type: "string", description: "Optional editor label." },
38
+ specials: {
39
+ type: "object",
40
+ description: "CONTENT + behaviour. Per-type keys (see get_element): text/tag (text), src (image, also in config), field_name (form input), type (form: form_order|form_login|…), products_per_load (grid-product), linkType/linkPage (menu-item nav), …",
41
+ additionalProperties: true,
42
+ },
43
+ runtime: {
44
+ type: "object",
45
+ description: "STAGING shape emitted by new_section/new_element. The storefront does NOT read `runtime` — build_page/add_section (the SPA's buildElementWithBreakpoint) expand it into bp1..bp4 {style,config} on save and DELETE runtime; runtime.specials, if present, is merged into the top-level specials. Author here; don't hand-write bp1..bp4 for new pages.",
46
+ additionalProperties: true,
47
+ properties: {
48
+ style: { $ref: "#/$defs/style" },
49
+ config: { $ref: "#/$defs/config" },
50
+ specials: { type: "object", description: "Optional per-breakpoint specials override; merged into top-level specials on expand.", additionalProperties: true },
51
+ responsive: { $ref: "#/$defs/responsive" },
52
+ },
53
+ },
54
+ children: {
55
+ type: "array",
56
+ description: "Child nodes — ONLY for container types (section, container, form, and repeaters like grid-product/cart-items whose children are the per-item template).",
57
+ items: { $ref: "#/$defs/node" },
58
+ },
59
+ events: { type: "array", items: { $ref: "#/$defs/event" } },
60
+ bindings: { type: "array", items: { $ref: "#/$defs/binding" } },
61
+ bp1: { $ref: "#/$defs/breakpoint" },
62
+ bp2: { $ref: "#/$defs/breakpoint" },
63
+ bp3: { $ref: "#/$defs/breakpoint" },
64
+ bp4: { $ref: "#/$defs/breakpoint" },
65
+ },
66
+ },
67
+ responsive: {
68
+ type: "object",
69
+ description: "CASCADE overrides — sparse per-breakpoint diffs you (the AI) supply to make the page responsive by reasoning, not a flat copy. bp1 is the base (runtime.style/config). Each smaller breakpoint INHERITS the resolved larger one, then applies only the keys here. Example: { bp4: { style: { fontSize: '28px', textAlign: 'center' } }, bp3: { style: { fontSize: '36px' } } } → bp1/bp2 keep the base h1; bp3 shrinks it; bp4 shrinks + centres. Pass via opts.responsive on new_element/new_section/new_row/build_page children.",
70
+ additionalProperties: false,
71
+ properties: {
72
+ bp2: { $ref: "#/$defs/bpOverride" },
73
+ bp3: { $ref: "#/$defs/bpOverride" },
74
+ bp4: { $ref: "#/$defs/bpOverride" },
75
+ },
76
+ },
77
+ bpOverride: {
78
+ type: "object",
79
+ description: "Only the style/config keys that CHANGE at this breakpoint (everything else cascades from the larger breakpoint).",
80
+ additionalProperties: false,
81
+ properties: {
82
+ style: { $ref: "#/$defs/style" },
83
+ config: { $ref: "#/$defs/config" },
84
+ },
85
+ },
86
+ breakpoint: {
87
+ type: "object",
88
+ description: "Per-breakpoint render data (bp1 ≥1320 desktop · bp2 993–1319 · bp3 641–992 tablet · bp4 320–640 mobile). Written by build_page; set explicitly only to override a smaller screen.",
89
+ additionalProperties: false,
90
+ properties: {
91
+ style: { $ref: "#/$defs/style" },
92
+ config: { $ref: "#/$defs/config" },
93
+ },
94
+ },
95
+ style: {
96
+ type: "object",
97
+ description: "CSS-ish props. Numbers (width/height) are px. Colours: theme vars var(--color_RC) or hex/rgba. borderRadius is a STRING with units ('8px'). Common: width,height,color,background,fontSize,fontWeight,textAlign,lineHeight,border,boxShadow,padding*,borderRadius,overflow,justifyContent.",
98
+ additionalProperties: true,
99
+ },
100
+ config: {
101
+ type: "object",
102
+ description: "LAYOUT (CSS grid placement) + a few render flags. Not absolute top/left.",
103
+ additionalProperties: true,
104
+ properties: {
105
+ grid: { type: "string", description: "Grid template id, e.g. '3xN' (section: margin·content·margin) or '1xN' (container)." },
106
+ columns: { type: "array", description: "Column unit objects, e.g. [{unit:'fr',value:1},{unit:'px',absValue:1300,value:1},{unit:'fr',value:1}].", items: { type: "object" } },
107
+ rows: { type: "array", description: "Row unit objects — one per child.", items: { type: "object" } },
108
+ rowGap: { type: "number", description: "Vertical gap (px) between stacked children." },
109
+ columnGap: { type: "number", description: "Horizontal gap (px) between columns." },
110
+ heightUnit: { type: "string", description: "'auto' lets content set height (the common value); a fixed px height is used otherwise." },
111
+ loaded: { type: "boolean", description: "Internal flag set on expand (bp config). You don't set it." },
112
+ columnStart: { type: "number" },
113
+ columnEnd: { type: "number" },
114
+ rowStart: { type: "number" },
115
+ rowEnd: { type: "number" },
116
+ constraintX: { type: "array", items: { type: "string", enum: ["left", "right", "centerLeft"] } },
117
+ constraintY: { type: "array", items: { type: "string", enum: ["top", "bottom", "centerTop"] } },
118
+ src: { type: "string", description: "Image source URL (image elements). MUST be a WebCake-CDN url or it won't render." },
119
+ },
120
+ },
121
+ event: {
122
+ type: "object",
123
+ description: "Interaction. Set `action` (+ its fields); the factory mints id and a sensible eventName. See list_events.",
124
+ required: ["action"],
125
+ additionalProperties: true,
126
+ properties: {
127
+ action: {
128
+ type: "string",
129
+ description: "open_page | open_link | open_category | scroll_to | toggle | open_popup | close_popup | add_to_cart | buy_now | apply_promotion | phone_call | open_email | scale | …",
130
+ },
131
+ eventName: { type: "string", description: "Trigger: click (default) | hover | success | submit | mouseenter | mouseleave." },
132
+ open_page_id: { type: "string" },
133
+ open_category_id: { type: "string" },
134
+ link_target: { type: "string" },
135
+ link_target_url: { type: "string" },
136
+ scroll_to_id: { type: "string" },
137
+ toggle_id: { type: "string" },
138
+ popup_id: { type: "string" },
139
+ popup_overlay: { type: "boolean" },
140
+ open_page: { type: "string", description: "Commerce shortcut, e.g. add_to_cart with open_page:'cart'." },
141
+ phone_call_number: { type: "string" },
142
+ open_email: { type: "string" },
143
+ },
144
+ },
145
+ binding: {
146
+ type: "object",
147
+ description: "Dataset binding for dataset elements / repeater children. Set `target`; the builder mints id. Target only resolves on a page whose `type` enables that dataset (store/member/blog). See list_bindings.",
148
+ required: ["target"],
149
+ additionalProperties: true,
150
+ properties: {
151
+ target: { type: "string", description: "'<dataset>::<field>', e.g. product::product_price, cart_item::cart_item_name, order_item::product_name." },
152
+ name: { type: "string", description: "Dataset name (product, cart_item, order_item, post, category, customer_address, …)." },
153
+ },
154
+ },
155
+ },
156
+ };
@@ -282,18 +282,33 @@ export function validatePage(source) {
282
282
  */
283
283
  function expandNodeToBreakpoints(node) {
284
284
  const rt = node && node.runtime;
285
- if (rt && (rt.style || rt.config)) {
286
- const baseStyle = rt.style || {};
285
+ if (rt && (rt.style || rt.config || rt.responsive)) {
287
286
  const baseConfig = { ...(rt.config || {}), loaded: true };
288
287
  const isSection = node.type === "section";
289
288
  const rowMeta = baseConfig.__row; // this node is a multi-column row container
290
289
  const cellMeta = baseConfig.__cell; // this node is a cell inside a row
290
+ // Per-breakpoint sparse overrides the AI supplied (opts.responsive → runtime.responsive):
291
+ // { bp2?:{style?,config?}, bp3?:{...}, bp4?:{...} }. bp1 is the base (runtime.style/config).
292
+ const overrides = rt.responsive && typeof rt.responsive === "object" ? rt.responsive : {};
293
+ // CASCADE (waterfall) bp1→bp4: each breakpoint starts from the RESOLVED larger one, then
294
+ // applies its own diff. So the AI only writes what CHANGES at each breakpoint and the rest
295
+ // inherits downward — proper AI-reasoned responsive instead of a flat copy.
296
+ let accStyle = rt.style || {};
297
+ let accConfig = baseConfig;
291
298
  for (const [bp, [minW]] of Object.entries(BREAKPOINTS)) {
292
- const style = clone(baseStyle);
293
- const config = clone(baseConfig);
299
+ const ov = overrides[bp];
300
+ if (ov && typeof ov === "object") {
301
+ if (ov.style)
302
+ accStyle = { ...accStyle, ...ov.style };
303
+ if (ov.config)
304
+ accConfig = { ...accConfig, ...ov.config };
305
+ }
306
+ const style = clone(accStyle);
307
+ const config = clone(accConfig);
294
308
  // Internal build-time markers never get persisted.
295
309
  delete config.__row;
296
310
  delete config.__cell;
311
+ delete config.responsive;
297
312
  if (isSection) {
298
313
  const g = genGridByBp(minW);
299
314
  const sectionRows = baseConfig.rows && baseConfig.rows.length ? clone(baseConfig.rows) : clone(g.rows);
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.31.6",
4
+ "d": "26/06/2026",
5
+ "type": "Removed",
6
+ "en": "scaffold_store_pages, scaffold_global_sections, and scaffold_popup tools are removed; pages are now composed free-form from elements using…",
7
+ "vi": "Các tool scaffold_store_pages, scaffold_global_sections và scaffold_popup đã bị xóa; các trang nay được tổ hợp tự do từ các phần tử bằng…"
8
+ },
9
+ {
10
+ "v": "1.31.5",
11
+ "d": "26/06/2026",
12
+ "type": "Added",
13
+ "en": "Column definitions accepted by create_collection and update_collection_columns now support five new optional fields: note (help text for the…",
14
+ "vi": "Định nghĩa cột được chấp nhận bởi create_collection và update_collection_columns nay hỗ trợ thêm năm trường tùy chọn mới: note (ghi chú/mô tả cho…"
15
+ },
2
16
  {
3
17
  "v": "1.31.4",
4
18
  "d": "26/06/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Fixed",
27
41
  "en": "The webcake-data SDK reference embedded in get_http_function and get_site_custom_code now documents the correct Mongoose-document API: filters are…",
28
42
  "vi": "Tài liệu tham chiếu SDK webcake-data được nhúng trong get_http_function và get_site_custom_code nay ghi lại đúng API kiểu Mongoose-document: bộ lọc…"
29
- },
30
- {
31
- "v": "1.31.0",
32
- "d": "26/06/2026",
33
- "type": "Added",
34
- "en": "New create_collection tool creates a custom data table by accepting a name and a schema array of field definitions (with types such as string,…",
35
- "vi": "Tool mới create_collection tạo bảng dữ liệu tùy chỉnh bằng cách nhận tham số name và mảng schema chứa định nghĩa các trường (với các kiểu dữ liệu…"
36
- },
37
- {
38
- "v": "1.30.0",
39
- "d": "26/06/2026",
40
- "type": "Added",
41
- "en": "New list_customers tool browses or searches the site's customer list; accepts page, limit, and term (name/phone/email keyword) and returns a compact…",
42
- "vi": "Tool mới list_customers duyệt hoặc tìm kiếm danh sách khách hàng của site; nhận các tham số page, limit và term (từ khóa tên/điện thoại/email) và…"
43
43
  }
44
44
  ]
package/dist/db.js CHANGED
@@ -58,3 +58,5 @@ export function setCachedUpload(siteId, source, cdnUrl) {
58
58
  imageCache[`${siteId}::${source}`] = cdnUrl;
59
59
  writeJson(IMAGE_CACHE_FILE, imageCache);
60
60
  }
61
+ // Page-draft cache moved to src/persistence/draft-cache.ts (Redis-or-memory,
62
+ // disposable + sliding TTL) — no longer a shared on-disk file.