webcake-storefront-mcp 1.18.0 → 1.19.0
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/builder/guide.js +47 -0
- package/dist/builder/templates.js +399 -0
- package/dist/changelog.json +14 -14
- package/dist/server.js +2 -0
- package/dist/tools/builder.js +76 -35
- package/dist/tools/context.js +2 -2
- package/dist/tools/global-section-write.js +91 -0
- package/dist/tools/products.js +6 -1
- package/package.json +1 -1
package/dist/builder/guide.js
CHANGED
|
@@ -158,6 +158,53 @@ Rule of thumb: if the page shows products, a cart, customer/order data, or blog
|
|
|
158
158
|
set \`type\` accordingly so the binding source is turned on. A binding target like
|
|
159
159
|
\`product::product_price\` REQUIRES its page to be the matching type.
|
|
160
160
|
|
|
161
|
+
## Build the WHOLE storefront — every page to the SAME standard (NOT just the home page)
|
|
162
|
+
A shop is multi-page. Build EACH page to a real e-commerce standard with the same palette,
|
|
163
|
+
spacing and header/footer — never leave the home page rich and the rest as bare stubs.
|
|
164
|
+
\`scaffold_store_pages\` now builds FULLY-DESIGNED, palette-aware store pages by DEFAULT
|
|
165
|
+
(style:"rich") — banner+breadcrumb+styled grid (Category), 2-col gallery|info with price/
|
|
166
|
+
quantity/add-to-cart+trust badges+related (Product), 2-col cart|summary, 2-col checkout
|
|
167
|
+
form|summary, centred thank-you — and auto-wires navigation between them. Then add chrome with
|
|
168
|
+
\`scaffold_global_sections({ brand, contact })\` for a designed Header+Footer in one call. Pass
|
|
169
|
+
style:"minimal" only if you want bare stubs to hand-build. The per-page recipe each rich page
|
|
170
|
+
follows (so you can match it when editing or building extra pages):
|
|
171
|
+
- Category (collections, type store): banner + heading + grid-product (+ optional intro/CTA).
|
|
172
|
+
- Product detail (products, type store): 2-col [product-gallery | info: text-dataset
|
|
173
|
+
product::product_name + product_price/original_price + short_description, quantity-input,
|
|
174
|
+
"Thêm vào giỏ" (add_to_cart) + "Mua ngay" (buy_now), trust badges] then a description/feature
|
|
175
|
+
band then a related grid-product ("Có thể bạn cũng thích").
|
|
176
|
+
- Cart (cart, type store): heading + 2-col [cart-items | order-summary card with a
|
|
177
|
+
"Tiến hành thanh toán" button -> { action:"open_page", open_page_id:<checkout> }] + continue link.
|
|
178
|
+
- Checkout (checkout, type store): heading + 2-col [form{type:form_order} with input/
|
|
179
|
+
phone-number/email/address + submit-button "Đặt hàng" | order summary (cart-items)].
|
|
180
|
+
- Thank-you (complete, type store): centred confirmation + order-items + continue-shopping.
|
|
181
|
+
- Optional: About / Contact (custom), Blog (type blog: post-list) + Post.
|
|
182
|
+
Reuse the SAME section helpers, palette and card styling as the home page so the whole site
|
|
183
|
+
feels like ONE design.
|
|
184
|
+
|
|
185
|
+
## Global Header & Footer — create them for EVERY site (don't inline per page)
|
|
186
|
+
A header (logo/nav/cart) and footer (links/contact/copyright) belong on EVERY page, so make them
|
|
187
|
+
GLOBAL, not copied into each page. Build a header section + a footer section (new_section, same as
|
|
188
|
+
any section), then:
|
|
189
|
+
create_global_section({ type:"header", name:"Header", section:<headerSection> }) // top of every page
|
|
190
|
+
create_global_section({ type:"footer", name:"Footer", section:<footerSection> }) // bottom of every page
|
|
191
|
+
(omit page_ids to apply to ALL pages). ORDER MATTERS: build/save the page CONTENT first, THEN
|
|
192
|
+
create the globals — they embed into each page's source. If you later overwrite a page's source
|
|
193
|
+
(update_page_source/build_page) you WIPE its embedded header/footer, so re-create the globals
|
|
194
|
+
(delete_global_section by the section NODE id, then create once) to re-embed cleanly. Edit a global
|
|
195
|
+
later with update_global_section_element(s) and it updates on every page at once.
|
|
196
|
+
|
|
197
|
+
## Popups (newsletter / promo / age-gate)
|
|
198
|
+
A popup is a GLOBAL SOURCE, not a page section. Build it, store it, then trigger it:
|
|
199
|
+
1. Build the popup body (new_section: heading + text + form/input + a close button), then wrap it:
|
|
200
|
+
new_element("popup", { children:[<that section>], specials:{ /* trigger + overlay */ } }).
|
|
201
|
+
Trigger/behaviour (auto-open after a delay, exit-intent, only-once, overlay) lives in specials.
|
|
202
|
+
2. Save it: create_global_source({ component:"popup", source:{ sections:[<popupNode>] } }) -> returns its id.
|
|
203
|
+
3. Open/close from any element via events: a button { action:"open_popup", popup_id:"<id>", popup_overlay:true };
|
|
204
|
+
a close button inside { action:"close_popup", popup_id:"<id>" }; a form can auto-close on its
|
|
205
|
+
success trigger ({ eventName:"success", action:"close_popup", popup_id:"<id>" }).
|
|
206
|
+
List existing popups with list_global_sources({component:"popup"}); edit via update_global_source(_element).
|
|
207
|
+
|
|
161
208
|
## Workflow (do this every time)
|
|
162
209
|
1. Intake: confirm goal, brand, colours, sections wanted (ask 3-5 questions if unclear).
|
|
163
210
|
2. list_elements / get_element to pick the right component types.
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// High-level, palette-aware PAGE & CHROME templates.
|
|
2
|
+
//
|
|
3
|
+
// The low-level builders (new_section/new_element/new_row -> buildSection/buildRow/
|
|
4
|
+
// buildElement) are flexible but leave the AI to hand-compose every node, so generated
|
|
5
|
+
// store pages end up bare. These templates encode the per-page recipes from the BUILD_GUIDE
|
|
6
|
+
// as ready-to-save `{ sections: [...] }` sources that already look like a real shop:
|
|
7
|
+
// styled headings, breadcrumbs, 2-column product detail, styled product-grid cards, trust
|
|
8
|
+
// badges, an order summary, and a designed header/footer.
|
|
9
|
+
//
|
|
10
|
+
// Everything is built through buildSection/buildRow so the output is the same runtime shape
|
|
11
|
+
// the builder emits; finalizeForRender() (called by the tools) expands it to bp1..bp4.
|
|
12
|
+
//
|
|
13
|
+
// Colours use the site theme CSS vars by default (var(--color_02) accent, var(--color_00)
|
|
14
|
+
// text) so a generated site matches whatever palette the theme already defines; callers can
|
|
15
|
+
// override any slot via a Palette object.
|
|
16
|
+
import { buildSection, walk } from "./page.js";
|
|
17
|
+
import { normalizeEvents } from "./events.js";
|
|
18
|
+
const DEFAULT_PALETTE = {
|
|
19
|
+
accent: "var(--color_02)",
|
|
20
|
+
onAccent: "#ffffff",
|
|
21
|
+
text: "var(--color_00)",
|
|
22
|
+
muted: "#6b7280",
|
|
23
|
+
surface: "#ffffff",
|
|
24
|
+
surfaceAlt: "#f7f5f2",
|
|
25
|
+
border: "#e8e3dc",
|
|
26
|
+
};
|
|
27
|
+
export function resolvePalette(p = {}) {
|
|
28
|
+
return { ...DEFAULT_PALETTE, ...Object.fromEntries(Object.entries(p).filter(([, v]) => v != null)) };
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// small spec helpers (return element specs for buildSection/buildRow children)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
const heading = (text, opts = {}) => ({
|
|
34
|
+
type: "text",
|
|
35
|
+
opts: {
|
|
36
|
+
text,
|
|
37
|
+
specials: { tag: opts.tag || "h2" },
|
|
38
|
+
style: {
|
|
39
|
+
fontSize: `${opts.size ?? 30}px`,
|
|
40
|
+
fontWeight: String(opts.weight ?? 700),
|
|
41
|
+
color: opts.color,
|
|
42
|
+
textAlign: opts.align,
|
|
43
|
+
lineHeight: "1.25",
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const paragraph = (text, p, opts = {}) => ({
|
|
48
|
+
type: "text",
|
|
49
|
+
opts: {
|
|
50
|
+
text,
|
|
51
|
+
style: {
|
|
52
|
+
fontSize: `${opts.size ?? 16}px`,
|
|
53
|
+
fontWeight: "400",
|
|
54
|
+
color: opts.color || p.muted,
|
|
55
|
+
textAlign: opts.align,
|
|
56
|
+
lineHeight: "1.6",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
/** Accent CTA button. `navTo` is a slug sentinel the scaffolder resolves to a real
|
|
61
|
+
* open_page event once all pages exist (see wireNavigation). The sentinel rides INSIDE an
|
|
62
|
+
* events entry because the button factory keeps opts.events but drops arbitrary specials. */
|
|
63
|
+
const cta = (text, p, opts = {}) => ({
|
|
64
|
+
type: "button",
|
|
65
|
+
opts: {
|
|
66
|
+
text,
|
|
67
|
+
...(opts.events || opts.navTo ? { events: [...(opts.events || []), ...(opts.navTo ? [{ action: "open_page", _navTo: opts.navTo }] : [])] } : {}),
|
|
68
|
+
style: {
|
|
69
|
+
background: p.accent,
|
|
70
|
+
color: p.onAccent,
|
|
71
|
+
borderRadius: "10px",
|
|
72
|
+
height: opts.height ?? 50,
|
|
73
|
+
fontWeight: "600",
|
|
74
|
+
fontSize: "16px",
|
|
75
|
+
paddingLeft: "28px",
|
|
76
|
+
paddingRight: "28px",
|
|
77
|
+
...(opts.full ? { width: "100%" } : {}),
|
|
78
|
+
textAlign: "center",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const outlineBtn = (text, p, opts = {}) => ({
|
|
83
|
+
type: "button",
|
|
84
|
+
opts: {
|
|
85
|
+
text,
|
|
86
|
+
...(opts.navTo ? { events: [{ action: "open_page", _navTo: opts.navTo }] } : {}),
|
|
87
|
+
style: {
|
|
88
|
+
background: "transparent",
|
|
89
|
+
color: p.accent,
|
|
90
|
+
border: `1px solid ${p.accent}`,
|
|
91
|
+
borderRadius: "10px",
|
|
92
|
+
height: opts.height ?? 48,
|
|
93
|
+
fontWeight: "600",
|
|
94
|
+
fontSize: "15px",
|
|
95
|
+
paddingLeft: "24px",
|
|
96
|
+
paddingRight: "24px",
|
|
97
|
+
textAlign: "center",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
/** A small "icon + label" trust badge (free shipping, authentic, support…). */
|
|
102
|
+
const trustBadge = (icon, label, p) => ({
|
|
103
|
+
type: "container",
|
|
104
|
+
layout: "row",
|
|
105
|
+
columnGap: 10,
|
|
106
|
+
colWidths: [{ unit: "px", absValue: 30 }, { unit: "fr", value: 1 }],
|
|
107
|
+
collapse: { bp4: 2 },
|
|
108
|
+
children: [
|
|
109
|
+
{ type: "text", opts: { text: icon, style: { fontSize: "22px", lineHeight: "1.2" } } },
|
|
110
|
+
{ type: "text", opts: { text: label, style: { fontSize: "13px", fontWeight: "600", color: p.text, lineHeight: "1.3" } } },
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
/** A styled product grid card config that looks like a real shop, not a bare list. */
|
|
114
|
+
const productGrid = (p, columns = 4) => ({
|
|
115
|
+
type: "grid-product",
|
|
116
|
+
opts: {
|
|
117
|
+
config: {
|
|
118
|
+
columns,
|
|
119
|
+
image_ratio: "1/1",
|
|
120
|
+
img_object_fit: "cover",
|
|
121
|
+
gap_column: 24,
|
|
122
|
+
gap_row: 32,
|
|
123
|
+
cardBorderRadius: 14,
|
|
124
|
+
cardBoxShadow: "0 6px 22px rgba(0,0,0,0.06)",
|
|
125
|
+
cardPadding: 12,
|
|
126
|
+
cardBackground: p.surface,
|
|
127
|
+
productNameColor: p.text,
|
|
128
|
+
productPriceColor: p.accent,
|
|
129
|
+
showAddToCart: true,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
/** Dataset text bound to a product field (name/price/description). */
|
|
134
|
+
const productField = (field, style) => ({
|
|
135
|
+
type: "text-dataset",
|
|
136
|
+
opts: {
|
|
137
|
+
bindings: [{ id: "BINDING" + Math.random().toString(36).slice(2, 8), name: "product", target: `product::${field}` }],
|
|
138
|
+
style,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const sectionStyle = (p, bg, padY = 64) => ({
|
|
142
|
+
style: { background: bg, paddingTop: padY, paddingBottom: padY, paddingLeft: 20, paddingRight: 20 },
|
|
143
|
+
});
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// STORE PAGE templates -> { sections: [...] }
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
export function categoryPageSource(p, opts = {}) {
|
|
148
|
+
const banner = buildSection([
|
|
149
|
+
{ type: "breadcrumb", opts: { style: { fontSize: "13px", color: p.muted } } },
|
|
150
|
+
heading(opts.title || "Tất cả sản phẩm", { tag: "h1", size: 40, color: p.text, align: "center" }),
|
|
151
|
+
paragraph(opts.subtitle || "Khám phá bộ sưu tập của chúng tôi", p, { align: "center", size: 17 }),
|
|
152
|
+
], { ...sectionStyle(p, p.surfaceAlt, 56), rowGap: 12 });
|
|
153
|
+
const grid = buildSection([productGrid(p, 4)], sectionStyle(p, p.surface, 56));
|
|
154
|
+
return { sections: [banner, grid] };
|
|
155
|
+
}
|
|
156
|
+
export function productPageSource(p) {
|
|
157
|
+
const breadcrumbRow = buildSection([{ type: "breadcrumb", opts: { style: { fontSize: "13px", color: p.muted } } }], { ...sectionStyle(p, p.surface, 24) });
|
|
158
|
+
const infoColumn = {
|
|
159
|
+
type: "container",
|
|
160
|
+
children: [
|
|
161
|
+
productField("product_name", { fontSize: "30px", fontWeight: "700", color: p.text, lineHeight: "1.25" }),
|
|
162
|
+
productField("product_price", { fontSize: "26px", fontWeight: "700", color: p.accent }),
|
|
163
|
+
productField("product_description", { fontSize: "15px", fontWeight: "400", color: p.muted, lineHeight: "1.6" }),
|
|
164
|
+
{ type: "quantity-input", opts: { style: { borderColor: p.border, borderRadius: "8px" } } },
|
|
165
|
+
cta("Thêm vào giỏ", p, { full: true, height: 52, events: [{ action: "add_to_cart", open_page: "cart" }] }),
|
|
166
|
+
{
|
|
167
|
+
type: "container",
|
|
168
|
+
layout: "row",
|
|
169
|
+
columnGap: 16,
|
|
170
|
+
collapse: { bp4: 1 },
|
|
171
|
+
children: [
|
|
172
|
+
trustBadge("🚚", "Miễn phí giao hàng", p),
|
|
173
|
+
trustBadge("✅", "Hàng chính hãng", p),
|
|
174
|
+
trustBadge("↩️", "Đổi trả 7 ngày", p),
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
const detail = buildSection([
|
|
180
|
+
{
|
|
181
|
+
type: "container",
|
|
182
|
+
layout: "row",
|
|
183
|
+
columnGap: 48,
|
|
184
|
+
colWidths: [{ unit: "fr", value: 1 }, { unit: "fr", value: 1 }],
|
|
185
|
+
collapse: { bp3: 1, bp4: 1 },
|
|
186
|
+
children: [{ type: "product-gallery", opts: { style: { borderRadius: "14px" } } }, infoColumn],
|
|
187
|
+
},
|
|
188
|
+
], sectionStyle(p, p.surface, 32));
|
|
189
|
+
const related = buildSection([heading("Sản phẩm liên quan", { tag: "h2", size: 26, color: p.text, align: "center" }), productGrid(p, 4)], { ...sectionStyle(p, p.surfaceAlt, 56), rowGap: 24 });
|
|
190
|
+
return { sections: [breadcrumbRow, detail, related] };
|
|
191
|
+
}
|
|
192
|
+
export function cartPageSource(p) {
|
|
193
|
+
const summary = {
|
|
194
|
+
type: "container",
|
|
195
|
+
opts: { style: { background: p.surfaceAlt, borderRadius: "14px", padding: "24px", border: `1px solid ${p.border}` } },
|
|
196
|
+
children: [
|
|
197
|
+
heading("Tóm tắt đơn hàng", { tag: "h3", size: 20, color: p.text }),
|
|
198
|
+
paragraph("Phí vận chuyển và mã giảm giá sẽ được tính ở bước thanh toán.", p, { size: 14 }),
|
|
199
|
+
cta("Tiến hành thanh toán", p, { full: true, height: 52, navTo: "checkout" }),
|
|
200
|
+
outlineBtn("Tiếp tục mua sắm", p, { navTo: "home" }),
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
const body = buildSection([
|
|
204
|
+
heading("Giỏ hàng của bạn", { tag: "h1", size: 34, color: p.text }),
|
|
205
|
+
{
|
|
206
|
+
type: "container",
|
|
207
|
+
layout: "row",
|
|
208
|
+
columnGap: 40,
|
|
209
|
+
colWidths: [{ unit: "fr", value: 2 }, { unit: "fr", value: 1 }],
|
|
210
|
+
collapse: { bp3: 1, bp4: 1 },
|
|
211
|
+
children: [{ type: "cart-items", opts: {} }, summary],
|
|
212
|
+
},
|
|
213
|
+
], { ...sectionStyle(p, p.surface, 48), rowGap: 24 });
|
|
214
|
+
return { sections: [body] };
|
|
215
|
+
}
|
|
216
|
+
export function checkoutPageSource(p) {
|
|
217
|
+
const form = {
|
|
218
|
+
type: "form",
|
|
219
|
+
opts: { specials: { type: "form_order" }, style: { background: p.surface } },
|
|
220
|
+
children: [
|
|
221
|
+
{ type: "input", opts: { specials: { field_name: "full_name", label: "Họ và tên", placeholder: "Nguyễn Văn A", required: true, show_label: true }, style: { borderColor: p.border, borderRadius: "8px" } } },
|
|
222
|
+
{ type: "phone-number", opts: { specials: { field_name: "phone_number", label: "Số điện thoại", required: true, show_label: true }, style: { borderColor: p.border, borderRadius: "8px" } } },
|
|
223
|
+
{ type: "address", opts: { specials: { field_name: "address", label: "Địa chỉ nhận hàng", show_label: true }, style: { borderColor: p.border, borderRadius: "8px" } } },
|
|
224
|
+
{ type: "text-area", opts: { specials: { field_name: "note", label: "Ghi chú", show_label: true }, style: { borderColor: p.border, borderRadius: "8px" } } },
|
|
225
|
+
{ type: "submit-button", opts: { text: "Đặt hàng", style: { background: p.accent, color: p.onAccent, borderRadius: "10px", height: 52, fontWeight: "600", width: "100%", textAlign: "center" } } },
|
|
226
|
+
],
|
|
227
|
+
};
|
|
228
|
+
const summary = {
|
|
229
|
+
type: "container",
|
|
230
|
+
opts: { style: { background: p.surfaceAlt, borderRadius: "14px", padding: "24px", border: `1px solid ${p.border}` } },
|
|
231
|
+
children: [heading("Đơn hàng của bạn", { tag: "h3", size: 20, color: p.text }), { type: "cart-items", opts: {} }],
|
|
232
|
+
};
|
|
233
|
+
const body = buildSection([
|
|
234
|
+
heading("Thanh toán", { tag: "h1", size: 34, color: p.text }),
|
|
235
|
+
{
|
|
236
|
+
type: "container",
|
|
237
|
+
layout: "row",
|
|
238
|
+
columnGap: 40,
|
|
239
|
+
colWidths: [{ unit: "fr", value: 3 }, { unit: "fr", value: 2 }],
|
|
240
|
+
collapse: { bp3: 1, bp4: 1 },
|
|
241
|
+
children: [form, summary],
|
|
242
|
+
},
|
|
243
|
+
], { ...sectionStyle(p, p.surface, 48), rowGap: 24 });
|
|
244
|
+
return { sections: [body] };
|
|
245
|
+
}
|
|
246
|
+
export function thankYouPageSource(p) {
|
|
247
|
+
const body = buildSection([
|
|
248
|
+
heading("🎉", { tag: "h2", size: 56, align: "center" }),
|
|
249
|
+
heading("Cảm ơn bạn đã đặt hàng!", { tag: "h1", size: 34, color: p.text, align: "center" }),
|
|
250
|
+
paragraph("Chúng tôi đã nhận được đơn hàng của bạn và sẽ liên hệ xác nhận trong thời gian sớm nhất.", p, { align: "center", size: 17 }),
|
|
251
|
+
{ type: "order-items", opts: {} },
|
|
252
|
+
cta("Tiếp tục mua sắm", p, { navTo: "home" }),
|
|
253
|
+
], { ...sectionStyle(p, p.surface, 64), rowGap: 18 });
|
|
254
|
+
return { sections: [body] };
|
|
255
|
+
}
|
|
256
|
+
export function headerSection(opts = {}) {
|
|
257
|
+
const p = resolvePalette(opts.palette);
|
|
258
|
+
const links = opts.links && opts.links.length ? opts.links : [
|
|
259
|
+
{ label: "Trang chủ", navTo: "home" },
|
|
260
|
+
{ label: "Sản phẩm", navTo: "collections" },
|
|
261
|
+
{ label: "Giỏ hàng", navTo: "cart" },
|
|
262
|
+
];
|
|
263
|
+
const logo = { type: "text", opts: { text: opts.brand || "Shop", specials: { tag: "h2" }, style: { fontSize: "24px", fontWeight: "800", color: p.text } } };
|
|
264
|
+
const nav = {
|
|
265
|
+
type: "container",
|
|
266
|
+
layout: "row",
|
|
267
|
+
columnGap: 28,
|
|
268
|
+
collapse: { bp4: 3 },
|
|
269
|
+
children: links.map((l) => ({
|
|
270
|
+
type: "text",
|
|
271
|
+
opts: {
|
|
272
|
+
text: l.label,
|
|
273
|
+
...(l.navTo || l.url ? { specials: { ...(l.navTo ? { _navTo: l.navTo } : {}), ...(l.url ? { _navUrl: l.url } : {}) } } : {}),
|
|
274
|
+
style: { fontSize: "15px", fontWeight: "600", color: p.text, cursor: "pointer" },
|
|
275
|
+
},
|
|
276
|
+
})),
|
|
277
|
+
};
|
|
278
|
+
const actions = {
|
|
279
|
+
type: "container",
|
|
280
|
+
layout: "row",
|
|
281
|
+
columnGap: 16,
|
|
282
|
+
colWidths: [{ unit: "px", absValue: 32 }, { unit: "max-c" }],
|
|
283
|
+
collapse: { bp4: 2 },
|
|
284
|
+
children: [
|
|
285
|
+
{ type: "cart-icon", opts: { config: { color: p.text }, style: { width: 26, height: 26 } } },
|
|
286
|
+
cta(opts.cta || "Đặt mua ngay", p, { navTo: "collections", height: 44 }),
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
return buildSection([
|
|
290
|
+
{
|
|
291
|
+
type: "container",
|
|
292
|
+
layout: "row",
|
|
293
|
+
columnGap: 24,
|
|
294
|
+
colWidths: [{ unit: "max-c" }, { unit: "fr", value: 1 }, { unit: "max-c" }],
|
|
295
|
+
collapse: { bp4: 1 },
|
|
296
|
+
children: [logo, nav, actions],
|
|
297
|
+
},
|
|
298
|
+
], { style: { background: p.surface, paddingTop: 16, paddingBottom: 16, paddingLeft: 20, paddingRight: 20, borderBottom: `1px solid ${p.border}` } });
|
|
299
|
+
}
|
|
300
|
+
export function footerSection(opts = {}) {
|
|
301
|
+
const p = resolvePalette(opts.palette);
|
|
302
|
+
const brandCol = {
|
|
303
|
+
type: "container",
|
|
304
|
+
children: [
|
|
305
|
+
{ type: "text", opts: { text: opts.brand || "Shop", specials: { tag: "h3" }, style: { fontSize: "20px", fontWeight: "800", color: "#fff" } } },
|
|
306
|
+
{ type: "text", opts: { text: opts.tagline || "Cảm ơn bạn đã ghé thăm cửa hàng.", style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "1.6" } } },
|
|
307
|
+
],
|
|
308
|
+
};
|
|
309
|
+
const linkCols = (opts.columns && opts.columns.length ? opts.columns : [
|
|
310
|
+
{ title: "Cửa hàng", links: ["Trang chủ", "Sản phẩm", "Giỏ hàng"] },
|
|
311
|
+
{ title: "Hỗ trợ", links: ["Chính sách đổi trả", "Giao hàng", "Liên hệ"] },
|
|
312
|
+
]).map((col) => ({
|
|
313
|
+
type: "container",
|
|
314
|
+
children: [
|
|
315
|
+
{ type: "text", opts: { text: col.title, style: { fontSize: "15px", fontWeight: "700", color: "#fff" } } },
|
|
316
|
+
...col.links.map((t) => ({ type: "text", opts: { text: t, style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "2" } } })),
|
|
317
|
+
],
|
|
318
|
+
}));
|
|
319
|
+
const c = opts.contact || {};
|
|
320
|
+
const contactCol = {
|
|
321
|
+
type: "container",
|
|
322
|
+
children: [
|
|
323
|
+
{ type: "text", opts: { text: "Liên hệ", style: { fontSize: "15px", fontWeight: "700", color: "#fff" } } },
|
|
324
|
+
...(c.phone ? [{ type: "text", opts: { text: `📞 ${c.phone}`, style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "2" } } }] : []),
|
|
325
|
+
...(c.email ? [{ type: "text", opts: { text: `✉️ ${c.email}`, style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "2" } } }] : []),
|
|
326
|
+
...(c.address ? [{ type: "text", opts: { text: `📍 ${c.address}`, style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "1.6" } } }] : []),
|
|
327
|
+
],
|
|
328
|
+
};
|
|
329
|
+
const topRow = {
|
|
330
|
+
type: "container",
|
|
331
|
+
layout: "row",
|
|
332
|
+
columnGap: 40,
|
|
333
|
+
colWidths: [{ unit: "fr", value: 2 }, { unit: "fr", value: 1 }, { unit: "fr", value: 1 }, { unit: "fr", value: 1 }],
|
|
334
|
+
collapse: { bp3: 2, bp4: 1 },
|
|
335
|
+
children: [brandCol, ...linkCols, contactCol],
|
|
336
|
+
};
|
|
337
|
+
const copyright = { type: "text", opts: { text: `© ${opts.brand || "Shop"}. All rights reserved.`, style: { fontSize: "13px", color: "rgba(255,255,255,0.5)", textAlign: "center" } } };
|
|
338
|
+
return buildSection([topRow, copyright], {
|
|
339
|
+
style: { background: "#1c1917", paddingTop: 56, paddingBottom: 32, paddingLeft: 20, paddingRight: 20 },
|
|
340
|
+
rowGap: 32,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
// registry the scaffolder iterates
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
/**
|
|
347
|
+
* Resolve the `_navTo` / `_navUrl` sentinels the templates leave on buttons/links into real
|
|
348
|
+
* open_page / open_link events, now that every page exists and we know its id. `slugToId`
|
|
349
|
+
* maps a page slug (e.g. "checkout", "cart", "collections") to its page id; the special
|
|
350
|
+
* slug "home" maps to the homepage id. Returns the number of nodes wired so the caller can
|
|
351
|
+
* skip re-saving an unchanged page. Run on the raw source BEFORE finalizeForRender.
|
|
352
|
+
*/
|
|
353
|
+
export function wireNavigation(source, slugToId) {
|
|
354
|
+
let wired = 0;
|
|
355
|
+
const resolve = (slug) => slugToId[slug] || slugToId[slug.replace(/^\//, "")];
|
|
356
|
+
walk(source, (node) => {
|
|
357
|
+
// (a) text links carry the sentinel in specials (the text factory keeps it).
|
|
358
|
+
const sp = node && node.specials;
|
|
359
|
+
if (sp && sp._navTo) {
|
|
360
|
+
const id = resolve(sp._navTo);
|
|
361
|
+
if (id) {
|
|
362
|
+
node.events = normalizeEvents([{ action: "open_page", open_page_id: id }], node.type);
|
|
363
|
+
wired++;
|
|
364
|
+
}
|
|
365
|
+
delete sp._navTo;
|
|
366
|
+
}
|
|
367
|
+
if (sp && sp._navUrl) {
|
|
368
|
+
node.events = normalizeEvents([{ action: "open_link", link_target: sp._navUrl, link_target_url: sp._navUrl }], node.type);
|
|
369
|
+
delete sp._navUrl;
|
|
370
|
+
wired++;
|
|
371
|
+
}
|
|
372
|
+
// (b) buttons carry the sentinel inside an events entry (the button factory keeps events
|
|
373
|
+
// but drops arbitrary specials). Resolve _navTo -> open_page_id in place; drop the
|
|
374
|
+
// event entirely if its target page doesn't exist so we never emit a dead open_page.
|
|
375
|
+
if (Array.isArray(node && node.events) && node.events.length) {
|
|
376
|
+
node.events = node.events
|
|
377
|
+
.map((e) => {
|
|
378
|
+
if (e && e._navTo) {
|
|
379
|
+
const id = resolve(e._navTo);
|
|
380
|
+
const { _navTo, ...rest } = e;
|
|
381
|
+
if (!id)
|
|
382
|
+
return null;
|
|
383
|
+
wired++;
|
|
384
|
+
return { ...rest, open_page_id: id };
|
|
385
|
+
}
|
|
386
|
+
return e;
|
|
387
|
+
})
|
|
388
|
+
.filter(Boolean);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
return wired;
|
|
392
|
+
}
|
|
393
|
+
export const STORE_PAGE_TEMPLATES = [
|
|
394
|
+
{ name: "Category Page", slug: "collections", build: categoryPageSource },
|
|
395
|
+
{ name: "Product Page", slug: "products", build: productPageSource },
|
|
396
|
+
{ name: "Cart Page", slug: "cart", build: cartPageSource },
|
|
397
|
+
{ name: "Checkout Page", slug: "checkout", build: checkoutPageSource },
|
|
398
|
+
{ name: "Thank You Page", slug: "complete", build: thankYouPageSource },
|
|
399
|
+
];
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.19.0",
|
|
4
|
+
"d": "25/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "New scaffold_global_sections tool generates a fully-designed global Header and Footer in one call — logo, nav links, cart icon, CTA button, and a…",
|
|
7
|
+
"vi": "Tool mới scaffold_global_sections tạo Header và Footer toàn cục đã thiết kế sẵn trong một lần gọi — logo, liên kết điều hướng, icon giỏ hàng, nút…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.18.1",
|
|
11
|
+
"d": "25/06/2026",
|
|
12
|
+
"type": "Changed",
|
|
13
|
+
"en": "get_build_guide now includes three new sections: \"Build the WHOLE storefront\" with per-page recipes for enriching category, product detail, cart,…",
|
|
14
|
+
"vi": "get_build_guide nay bổ sung ba mục mới: \"Build the WHOLE storefront\" — công thức từng trang để nâng cấp danh mục, trang chi tiết sản phẩm, giỏ hàng,…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.18.0",
|
|
4
18
|
"d": "25/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Added",
|
|
27
41
|
"en": "New uninstall_app, update_app, and update_app_review tools complete the app lifecycle: uninstall an installed app by subscription id, patch an app's…",
|
|
28
42
|
"vi": "Tool mới uninstall_app, update_app và update_app_review hoàn thiện vòng đời ứng dụng: gỡ cài đặt theo subscription id, cập nhật config/status của…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.15.1",
|
|
32
|
-
"d": "24/06/2026",
|
|
33
|
-
"type": "Fixed",
|
|
34
|
-
"en": "update_page_element, update_page_elements, update_global_source_element, and update_global_source_elements now normalize events and bindings arrays…",
|
|
35
|
-
"vi": "update_page_element, update_page_elements, update_global_source_element và update_global_source_elements nay chuẩn hóa mảng events và bindings khi…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.15.0",
|
|
39
|
-
"d": "24/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "New get_global_section tool returns a compact tree representation of a single global section (Header/Footer/block), listing each element's id, type,…",
|
|
42
|
-
"vi": "Tool mới get_global_section trả về cây phần tử dạng compact của một global section (Header/Footer/block), liệt kê id, type, text, class, binding và…"
|
|
43
43
|
}
|
|
44
44
|
]
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,8 @@ Tool discovery: the common tools (pages, builder, products, orders, blog, custom
|
|
|
8
8
|
|
|
9
9
|
You can also BUILD pages: use get_build_guide, list_elements, get_element to learn the BuilderX component model, new_section/new_element to compose, validate_page to check, then build_page (dry_run first) to create. Publishing is site-level via publish_site.
|
|
10
10
|
|
|
11
|
+
Build the WHOLE site, not just the home page: enrich EVERY page (category, product detail, cart, checkout, thank-you, …) to the same e-commerce standard with the same palette/header/footer — scaffold_store_pages makes MINIMAL starter pages, so design them properly afterwards. Make the Header and Footer GLOBAL (create_global_section type:"header"/"footer") so they're consistent on every page — don't inline them per page. For newsletter/promo offers, build a popup (a "popup" global_source) and open it with an open_popup event. See get_build_guide for the per-page recipes, the global-section order rules, and the popup workflow.
|
|
12
|
+
|
|
11
13
|
To make a generated site look real, also CREATE DATA so dataset bindings resolve: create_product_category + create_product (storefront), create_blog_category + create_article (blog). Get image URLs from search_images / upload_images first, then reference them. A good flow for a fresh site: create_site → create a few categories → create products in them → build_page (home + store/blog pages) → publish_site.
|
|
12
14
|
|
|
13
15
|
INTAKE FIRST — when the user asks to BUILD/CREATE a site, store, or page (a fresh build, not a small edit or a data question), do NOT jump straight to create_site/build_page on the same turn. Act as a professional shop-website designer doing client work: first ask ONE short batch of questions (call get_intake_guide for the exact list), restate the plan you'll build, get a "yes", THEN build. The shop owner is an ORDINARY person, not a designer — ask in plain words and visual outcomes ("tông nâu ấm, ảnh sản phẩm to"), never jargon. Ask the essentials (with sensible defaults so they answer fast): what they sell + a few products & prices, brand/shop name, primary color/style, which pages they need, real contact info (hotline/Zalo, address, email, hours) + the main call-to-action, and any promotion. NEVER invent or silently placeholder real data (shop name, products, prices, phone, address) — ask for it; placeholder only what the user explicitly skips, and tell them what to fill in. Skip intake only when the user already gave the brief, says "just do it / tự quyết", or it's a tiny edit.
|
package/dist/tools/builder.js
CHANGED
|
@@ -4,6 +4,7 @@ import { listElements, getElement, buildElement } from "../builder/catalog.js";
|
|
|
4
4
|
import { describeEventsCatalog } from "../builder/events.js";
|
|
5
5
|
import { describeBindingsCatalog } from "../builder/bindings.js";
|
|
6
6
|
import { buildSection, buildRow, newPageSkeleton, validatePage, finalizeForRender, reassignIds, } from "../builder/page.js";
|
|
7
|
+
import { STORE_PAGE_TEMPLATES, resolvePalette, wireNavigation } from "../builder/templates.js";
|
|
7
8
|
// Recursive spec for new_section / build_page children.
|
|
8
9
|
const elementSpec = z.object({
|
|
9
10
|
type: z.string().describe("Element type (see list_elements)"),
|
|
@@ -197,11 +198,20 @@ product/category/cart from any page lands on a real page instead of a 404.
|
|
|
197
198
|
Creates only the ones MISSING (matched by slug): Category (collections), Product (products),
|
|
198
199
|
Cart (cart), Checkout (checkout), Thank-you (complete) — all type 'store' (enables use_store),
|
|
199
200
|
plus optional member (login/register/profile) and blog (blog/post) pages.
|
|
200
|
-
Run this right after you create products/categories. dry_run=true (default) previews
|
|
201
|
+
Run this right after you create products/categories. dry_run=true (default) previews.
|
|
202
|
+
By default (style:"rich") each store page is a FULLY-DESIGNED, palette-aware page — banner +
|
|
203
|
+
breadcrumb + styled product-grid cards (Category), 2-column gallery|info with price/quantity/
|
|
204
|
+
add-to-cart + trust badges + related products (Product), 2-col cart|summary, 2-col checkout
|
|
205
|
+
form|summary, and a centred thank-you — and navigation between them (cart→checkout, add-to-cart
|
|
206
|
+
→cart, thank-you→home) is auto-wired. Colours follow the site theme vars unless you pass a palette.
|
|
207
|
+
Still add a GLOBAL header/footer with scaffold_global_sections (or create_global_section).
|
|
208
|
+
Pass style:"minimal" for the old bare stubs (heading + binding element only).`, {
|
|
209
|
+
style: z.enum(["rich", "minimal"]).default("rich").describe("'rich' = fully-designed, palette-aware store pages (default). 'minimal' = bare starter stubs you must enrich yourself."),
|
|
210
|
+
palette: z.record(z.any()).optional().describe("Optional colour overrides for rich pages: { accent, onAccent, text, muted, surface, surfaceAlt, border }. Defaults to the site theme CSS vars (var(--color_02) accent, var(--color_00) text)."),
|
|
201
211
|
include_member: z.boolean().default(false).describe("Also create login/register/profile (type member, use_member)"),
|
|
202
212
|
include_blog: z.boolean().default(false).describe("Also create blog list + post pages (type blog, use_blog)"),
|
|
203
213
|
dry_run: z.boolean().default(true).describe("Preview (true) or actually create the missing pages (false)"),
|
|
204
|
-
}, ({ include_member, include_blog, dry_run }) => handle(async () => {
|
|
214
|
+
}, ({ style, palette, include_member, include_blog, dry_run }) => handle(async () => {
|
|
205
215
|
const bind = (name, field) => ({
|
|
206
216
|
id: "BINDING" + Math.random().toString(36).slice(2, 8),
|
|
207
217
|
name,
|
|
@@ -209,38 +219,38 @@ Run this right after you create products/categories. dry_run=true (default) prev
|
|
|
209
219
|
});
|
|
210
220
|
const h1 = (text) => ({ type: "text", opts: { text, specials: { tag: "h1" }, style: { fontSize: "32px", fontWeight: "700" } } });
|
|
211
221
|
const accentBtn = (text, type = "button") => ({ type, opts: { text, style: { background: "var(--color_02)", color: "#fff", borderRadius: "8px", height: 48, fontWeight: "600" } } });
|
|
222
|
+
// Rich (default) store pages come from the designed, palette-aware templates;
|
|
223
|
+
// 'minimal' falls back to the original bare stubs.
|
|
224
|
+
const pal = resolvePalette(palette || {});
|
|
225
|
+
const minimalStore = {
|
|
226
|
+
collections: () => ({ sections: [buildSection([h1("Danh mục sản phẩm"), { type: "grid-product", opts: { config: { columns: 3, image_ratio: "1/1", gap_column: 24, gap_row: 32 } } }])] }),
|
|
227
|
+
products: () => ({ sections: [buildSection([
|
|
228
|
+
{ type: "product-gallery", opts: {} },
|
|
229
|
+
{ type: "text-dataset", opts: { bindings: [bind("product", "product_name")], style: { fontSize: "28px", fontWeight: "700" } } },
|
|
230
|
+
{ type: "text-dataset", opts: { bindings: [bind("product", "product_price")], style: { fontSize: "22px", fontWeight: "700", color: "var(--color_02)" } } },
|
|
231
|
+
{ type: "quantity-input", opts: {} },
|
|
232
|
+
accentBtn("Thêm vào giỏ"),
|
|
233
|
+
])] }),
|
|
234
|
+
cart: () => ({ sections: [buildSection([h1("Giỏ hàng"), { type: "cart-items", opts: {} }, accentBtn("Tiến hành thanh toán")])] }),
|
|
235
|
+
checkout: () => ({ sections: [buildSection([
|
|
236
|
+
h1("Thanh toán"),
|
|
237
|
+
{ type: "form", opts: { specials: { type: "form_order" } }, children: [
|
|
238
|
+
{ type: "input", opts: { specials: { field_name: "full_name", label: "Họ tên", placeholder: "Họ tên", required: true, show_label: true } } },
|
|
239
|
+
{ type: "phone-number", opts: { specials: { field_name: "phone_number", label: "Số điện thoại", required: true, show_label: true } } },
|
|
240
|
+
{ type: "address", opts: { specials: { field_name: "address", label: "Địa chỉ", show_label: true } } },
|
|
241
|
+
accentBtn("Đặt hàng", "submit-button"),
|
|
242
|
+
] },
|
|
243
|
+
])] }),
|
|
244
|
+
complete: () => ({ sections: [buildSection([h1("Cảm ơn bạn đã đặt hàng!"), { type: "order-items", opts: {} }])] }),
|
|
245
|
+
};
|
|
212
246
|
// Each spec: { name, slug, kind, build() -> { sections } }
|
|
213
|
-
const SPECS =
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{ type: "text-dataset", opts: { bindings: [bind("product", "product_name")], style: { fontSize: "28px", fontWeight: "700" } } },
|
|
221
|
-
{ type: "text-dataset", opts: { bindings: [bind("product", "product_price")], style: { fontSize: "22px", fontWeight: "700", color: "var(--color_02)" } } },
|
|
222
|
-
{ type: "quantity-input", opts: {} },
|
|
223
|
-
accentBtn("Thêm vào giỏ"),
|
|
224
|
-
]),
|
|
225
|
-
] }) },
|
|
226
|
-
{ name: "Cart Page", slug: "cart", kind: "store", flag: "use_store", build: () => ({ sections: [
|
|
227
|
-
buildSection([h1("Giỏ hàng"), { type: "cart-items", opts: {} }, accentBtn("Tiến hành thanh toán")]),
|
|
228
|
-
] }) },
|
|
229
|
-
{ name: "Checkout Page", slug: "checkout", kind: "store", flag: "use_store", build: () => ({ sections: [
|
|
230
|
-
buildSection([
|
|
231
|
-
h1("Thanh toán"),
|
|
232
|
-
{ type: "form", opts: { specials: { type: "form_order" } }, children: [
|
|
233
|
-
{ type: "input", opts: { specials: { field_name: "full_name", label: "Họ tên", placeholder: "Họ tên", required: true, show_label: true } } },
|
|
234
|
-
{ type: "phone-number", opts: { specials: { field_name: "phone_number", label: "Số điện thoại", required: true, show_label: true } } },
|
|
235
|
-
{ type: "address", opts: { specials: { field_name: "address", label: "Địa chỉ", show_label: true } } },
|
|
236
|
-
accentBtn("Đặt hàng", "submit-button"),
|
|
237
|
-
] },
|
|
238
|
-
]),
|
|
239
|
-
] }) },
|
|
240
|
-
{ name: "Thank You Page", slug: "complete", kind: "store", flag: "use_store", build: () => ({ sections: [
|
|
241
|
-
buildSection([h1("Cảm ơn bạn đã đặt hàng!"), { type: "order-items", opts: {} }]),
|
|
242
|
-
] }) },
|
|
243
|
-
];
|
|
247
|
+
const SPECS = STORE_PAGE_TEMPLATES.map((t) => ({
|
|
248
|
+
name: t.name,
|
|
249
|
+
slug: t.slug,
|
|
250
|
+
kind: "store",
|
|
251
|
+
flag: "use_store",
|
|
252
|
+
build: () => (style === "minimal" ? minimalStore[t.slug]() : t.build(pal)),
|
|
253
|
+
}));
|
|
244
254
|
if (include_member) {
|
|
245
255
|
SPECS.push({ name: "Login Page", slug: "login", kind: "member", flag: "use_member", build: () => ({ sections: [
|
|
246
256
|
buildSection([h1("Đăng nhập"), { type: "form", opts: { specials: { type: "form_login" } }, children: [
|
|
@@ -279,6 +289,18 @@ Run this right after you create products/categories. dry_run=true (default) prev
|
|
|
279
289
|
const created = [];
|
|
280
290
|
const errors = [];
|
|
281
291
|
const flagsEnabled = new Set();
|
|
292
|
+
// slug -> page id, seeded with the pages that already exist (so cross-page nav
|
|
293
|
+
// resolves even when only some store pages are newly created).
|
|
294
|
+
const slugToId = {};
|
|
295
|
+
for (const pg of Array.isArray(pages) ? pages : []) {
|
|
296
|
+
const sl = (pg.slug || "").replace(/^\//, "");
|
|
297
|
+
if (sl)
|
|
298
|
+
slugToId[sl] = pg.id;
|
|
299
|
+
if (pg.is_homepage)
|
|
300
|
+
slugToId["home"] = pg.id;
|
|
301
|
+
}
|
|
302
|
+
// Keep each newly-built source so we can wire navigation once every id is known.
|
|
303
|
+
const built = [];
|
|
282
304
|
for (const spec of missing) {
|
|
283
305
|
try {
|
|
284
306
|
if (!flagsEnabled.has(spec.flag)) {
|
|
@@ -294,21 +316,40 @@ Run this right after you create products/categories. dry_run=true (default) prev
|
|
|
294
316
|
finalizeForRender(source);
|
|
295
317
|
const res = await api.createPage({ name: spec.name, source, type: PAGE_TYPE_NUM[spec.kind] });
|
|
296
318
|
const pid = newPageId(res);
|
|
297
|
-
if (pid)
|
|
319
|
+
if (pid) {
|
|
298
320
|
await api.updatePage(pid, { slug: spec.slug }).catch(() => { });
|
|
321
|
+
slugToId[spec.slug] = pid;
|
|
322
|
+
built.push({ spec, pid, source });
|
|
323
|
+
}
|
|
299
324
|
created.push({ name: spec.name, slug: spec.slug, type: spec.kind, page_id: pid });
|
|
300
325
|
}
|
|
301
326
|
catch (e) {
|
|
302
327
|
errors.push({ slug: spec.slug, error: e?.message ?? String(e) });
|
|
303
328
|
}
|
|
304
329
|
}
|
|
330
|
+
// Second pass: resolve the _navTo sentinels the templates left on buttons/links into
|
|
331
|
+
// real open_page events (cart→checkout, thank-you→home, …), now that every page id is
|
|
332
|
+
// known. Only re-save the pages that actually changed.
|
|
333
|
+
let nav_wired = 0;
|
|
334
|
+
for (const { pid, source } of built) {
|
|
335
|
+
try {
|
|
336
|
+
const n = wireNavigation(source, slugToId);
|
|
337
|
+
if (n > 0) {
|
|
338
|
+
nav_wired += n;
|
|
339
|
+
await api.updatePageSource(pid, { source }).catch(() => { });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch { /* nav wiring is best-effort */ }
|
|
343
|
+
}
|
|
305
344
|
return {
|
|
306
345
|
success: true,
|
|
346
|
+
style,
|
|
307
347
|
created,
|
|
308
348
|
already_exist: skipped,
|
|
309
349
|
...(errors.length ? { errors } : {}),
|
|
310
350
|
data_sources_enabled: [...flagsEnabled],
|
|
311
|
-
|
|
351
|
+
nav_links_wired: nav_wired,
|
|
352
|
+
note: "Add a header/footer with scaffold_global_sections, then publish_site to take the new pages live.",
|
|
312
353
|
};
|
|
313
354
|
}));
|
|
314
355
|
}
|
package/dist/tools/context.js
CHANGED
|
@@ -34,8 +34,8 @@ export function registerContextTools(server, api, handle) {
|
|
|
34
34
|
"create_site (tên + slug) → tự chuyển sang site mới",
|
|
35
35
|
"create_product_category + create_product cho từng sản phẩm (ảnh từ search_images/upload_images trước)",
|
|
36
36
|
"build_page trang chủ (type:'main', is_homepage:true) với hero, lưới sản phẩm, câu chuyện, CTA, …",
|
|
37
|
-
"scaffold_store_pages
|
|
38
|
-
"
|
|
37
|
+
"scaffold_store_pages (style:'rich' mặc định) → trang danh mục/chi tiết/giỏ/thanh toán/cảm ơn ĐÃ thiết kế sẵn, tự nối điều hướng",
|
|
38
|
+
"scaffold_global_sections({ brand, contact }) → Header + Footer thiết kế sẵn, dùng chung mọi trang (tự bỏ qua slot đã có)",
|
|
39
39
|
"publish_site (cũng rebuild CSS storefront)",
|
|
40
40
|
],
|
|
41
41
|
notes: "Sau khi build xong, QA trên builder editor (app_base/editor/:site_id) hoặc storefront đã publish; publish_site sẽ rebuild CSS để hết tình trạng trang thiếu style.",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { validatePage, finalizeForRender } from "../builder/page.js";
|
|
3
|
+
import { headerSection, footerSection, wireNavigation } from "../builder/templates.js";
|
|
3
4
|
/**
|
|
4
5
|
* Write tools for global SECTIONS (Header / Footer / reusable blocks).
|
|
5
6
|
*
|
|
@@ -124,6 +125,96 @@ Two-step safety: dry_run=true (default) previews which pages change; dry_run=fal
|
|
|
124
125
|
raw: ok ? undefined : res,
|
|
125
126
|
};
|
|
126
127
|
}));
|
|
128
|
+
server.tool("scaffold_global_sections", `Generate a DESIGNED global Header and Footer in one call and embed them on every page —
|
|
129
|
+
the fast path to consistent site chrome. The header has logo + nav links + cart icon + CTA;
|
|
130
|
+
the footer has brand blurb + link columns + contact + copyright. Navigation is auto-wired to
|
|
131
|
+
real pages (Home/Products/Cart). Colours follow the site theme vars unless you pass a palette.
|
|
132
|
+
SKIPS a slot that already has a global section (won't create a second header/footer) unless
|
|
133
|
+
force=true. Two-step safety: dry_run=true (default) previews; dry_run=false performs the atomic save.`, {
|
|
134
|
+
brand: z.string().describe("Shop / brand name shown as the logo and in the footer."),
|
|
135
|
+
links: z.array(z.object({ label: z.string(), navTo: z.string().optional(), url: z.string().optional() })).optional().describe("Header nav links. navTo = a page slug ('home','collections','cart') auto-wired to open_page; url = external link. Defaults to Home/Products/Cart."),
|
|
136
|
+
contact: z.object({ phone: z.string().optional(), email: z.string().optional(), address: z.string().optional() }).optional().describe("Real contact info for the footer (don't invent — omit what you don't have)."),
|
|
137
|
+
cta: z.string().optional().describe("Header call-to-action button label (default 'Đặt mua ngay')."),
|
|
138
|
+
palette: z.record(z.any()).optional().describe("Optional colour overrides { accent, onAccent, text, muted, surface, surfaceAlt, border }."),
|
|
139
|
+
include: z.enum(["both", "header", "footer"]).default("both").describe("Which chrome to generate."),
|
|
140
|
+
force: z.boolean().default(false).describe("Create even if a header/footer global already exists (otherwise that slot is skipped)."),
|
|
141
|
+
dry_run: z.boolean().default(true).describe("Preview (true) or perform the atomic save (false)."),
|
|
142
|
+
}, ({ brand, links, contact, cta, palette, include, force, dry_run }) => handle(async () => {
|
|
143
|
+
// Which slots already have a global section? (dedupe by type unless force)
|
|
144
|
+
const existingRes = await api.listGlobalSections().catch(() => null);
|
|
145
|
+
const existing = (existingRes && (existingRes.data || existingRes.global_sections || existingRes)) || [];
|
|
146
|
+
const slotTaken = (slot) => Array.isArray(existing) && existing.some((g) => (g.slot || "").toLowerCase() === slot || TYPE_NUM[slot] === g.type);
|
|
147
|
+
const wantHeader = include !== "footer";
|
|
148
|
+
const wantFooter = include !== "header";
|
|
149
|
+
const skipped = [];
|
|
150
|
+
// Resolve slug -> page id for nav wiring + page embedding.
|
|
151
|
+
const allPages = await loadPages(api);
|
|
152
|
+
const slugToId = {};
|
|
153
|
+
const pagesRes = await api.listPages();
|
|
154
|
+
for (const pg of (pagesRes && pagesRes.data) || pagesRes || []) {
|
|
155
|
+
const sl = (pg.slug || "").replace(/^\//, "");
|
|
156
|
+
if (sl)
|
|
157
|
+
slugToId[sl] = pg.id;
|
|
158
|
+
if (pg.is_homepage)
|
|
159
|
+
slugToId["home"] = pg.id;
|
|
160
|
+
}
|
|
161
|
+
// Build the finalized, nav-wired global nodes for each requested slot.
|
|
162
|
+
const toCreate = [];
|
|
163
|
+
const buildNode = (type, raw) => {
|
|
164
|
+
raw.specials = { ...(raw.specials || {}), global: type };
|
|
165
|
+
const wrap = { sections: [raw] };
|
|
166
|
+
const validation = validatePage(wrap);
|
|
167
|
+
if (!validation.valid)
|
|
168
|
+
throw new Error(`${type} failed validation: ${JSON.stringify(validation.errors)}`);
|
|
169
|
+
finalizeForRender(wrap);
|
|
170
|
+
wireNavigation(wrap, slugToId); // resolve _navTo on header links/CTA
|
|
171
|
+
return wrap.sections[0];
|
|
172
|
+
};
|
|
173
|
+
if (wantHeader) {
|
|
174
|
+
if (!force && slotTaken("header"))
|
|
175
|
+
skipped.push("header");
|
|
176
|
+
else
|
|
177
|
+
toCreate.push({ type: "header", name: "Header", node: buildNode("header", headerSection({ brand, links: links, cta, palette })) });
|
|
178
|
+
}
|
|
179
|
+
if (wantFooter) {
|
|
180
|
+
if (!force && slotTaken("footer"))
|
|
181
|
+
skipped.push("footer");
|
|
182
|
+
else
|
|
183
|
+
toCreate.push({ type: "footer", name: "Footer", node: buildNode("footer", footerSection({ brand, contact, palette })) });
|
|
184
|
+
}
|
|
185
|
+
if (!toCreate.length) {
|
|
186
|
+
return { success: true, created: [], skipped, note: skipped.length ? "Those slots already have a global section (pass force=true to add anyway)." : "Nothing to create." };
|
|
187
|
+
}
|
|
188
|
+
if (dry_run) {
|
|
189
|
+
return {
|
|
190
|
+
dry_run: true,
|
|
191
|
+
will_create: toCreate.map((t) => ({ type: t.type, section_id: t.node.id })),
|
|
192
|
+
skipped,
|
|
193
|
+
embeds_into_pages: allPages.length,
|
|
194
|
+
hint: "Call again with dry_run=false to create + embed the header/footer, then publish_site.",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Inject every node into every page, then one atomic /save with all globals + pages.
|
|
198
|
+
for (const t of toCreate)
|
|
199
|
+
for (const p of allPages)
|
|
200
|
+
injectNode(p.source, t.node, t.type);
|
|
201
|
+
const changedPages = allPages.map((p) => ({ id: p.id, source: JSON.stringify(p.source) }));
|
|
202
|
+
const changes = allPages.reduce((o, p) => { o[p.id] = 1; return o; }, {});
|
|
203
|
+
const global_sections = toCreate.map((t) => ({
|
|
204
|
+
section_id: t.node.id, name: t.name, type: TYPE_NUM[t.type],
|
|
205
|
+
pages: allPages.map((p) => p.id), section: t.node, status: "new", contents: [],
|
|
206
|
+
}));
|
|
207
|
+
const res = await api.saveGlobalSections({ global_sections, pages: changedPages, changes });
|
|
208
|
+
const ok = !!(res && (res.success || res.data));
|
|
209
|
+
return {
|
|
210
|
+
success: ok,
|
|
211
|
+
created: toCreate.map((t) => ({ type: t.type, section_id: t.node.id })),
|
|
212
|
+
skipped,
|
|
213
|
+
embedded_pages: changedPages.length,
|
|
214
|
+
note: "Publish the site (publish_site) to take the header/footer live.",
|
|
215
|
+
raw: ok ? undefined : res,
|
|
216
|
+
};
|
|
217
|
+
}));
|
|
127
218
|
server.tool("delete_global_section", `Delete a global section (Header/Footer/block) and remove its node from every page source.
|
|
128
219
|
Two-step safety: dry_run=true (default) shows which pages would change; dry_run=false performs the atomic save.`, {
|
|
129
220
|
section_id: z.string().describe("The global section's section_id (the section node id) — from list_global_sections."),
|
package/dist/tools/products.js
CHANGED
|
@@ -5,7 +5,12 @@ export function registerProductTools(server, api, handle) {
|
|
|
5
5
|
limit: z.number().optional().describe("Items per page"),
|
|
6
6
|
term: z.string().optional().describe("Search by product name"),
|
|
7
7
|
}, ({ page, limit, term }) => handle(async () => {
|
|
8
|
-
|
|
8
|
+
// Build a clean query — the /products/all endpoint 400s on undefined/blank keys,
|
|
9
|
+
// so default page/limit and drop term unless it's a real search string.
|
|
10
|
+
const query = { page: page ?? 1, limit: limit ?? 50 };
|
|
11
|
+
if (term && term.trim())
|
|
12
|
+
query.term = term.trim();
|
|
13
|
+
const res = await api.listProducts(query);
|
|
9
14
|
const products = (res && res.data) || res || [];
|
|
10
15
|
if (!Array.isArray(products))
|
|
11
16
|
return res;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-storefront-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "MCP server for the WebCake/StoreCake storefront builder — page CRUD, page authoring, products, orders, and more",
|
|
5
5
|
"mcpName": "io.github.vuluu2k/webcake-storefront-mcp",
|
|
6
6
|
"license": "MIT",
|