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 +7 -3
- package/README.vi.md +7 -3
- package/dist/api.js +55 -11
- package/dist/builder/catalog.js +7 -0
- package/dist/builder/guide.js +120 -28
- package/dist/builder/page-schema.js +156 -0
- package/dist/builder/page.js +19 -4
- package/dist/changelog.json +14 -14
- package/dist/db.js +2 -0
- package/dist/persistence/draft-cache.js +198 -0
- package/dist/server.js +13 -2
- package/dist/tools/builder-extras.js +4 -1
- package/dist/tools/builder.js +3 -163
- package/dist/tools/collections.js +24 -3
- package/dist/tools/context.js +85 -11
- package/dist/tools/global-section-write.js +0 -95
- package/dist/tools/global-sources.js +0 -31
- package/dist/tools/page-draft.js +174 -0
- package/dist/tools/registry.js +2 -0
- package/dist/web-guide.js +1 -1
- package/package.json +1 -1
- package/dist/builder/templates.js +0 -526
package/README.md
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
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 — **~
|
|
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
|
-
~
|
|
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
|
-
|
|
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 — **~
|
|
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
|
-
~
|
|
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
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
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 có 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: {
|
|
266
|
-
|
|
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 } = {}) {
|
package/dist/builder/catalog.js
CHANGED
|
@@ -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)
|
package/dist/builder/guide.js
CHANGED
|
@@ -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)\`
|
|
75
|
-
|
|
76
|
-
|
|
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(--
|
|
89
|
-
borderRadius:"8px", fontWeight:"600", height:48 } } }\` (brand background, white label).
|
|
90
|
-
- HERO:
|
|
91
|
-
|
|
92
|
-
|
|
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)
|
|
103
|
-
prices, highlights — consistent accent = looks intentional. (var(--color_00) is WHITE
|
|
104
|
-
for backgrounds / labels
|
|
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)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
207
|
-
(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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.
|
|
241
|
-
|
|
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
|
+
};
|
package/dist/builder/page.js
CHANGED
|
@@ -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
|
|
293
|
-
|
|
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);
|
package/dist/changelog.json
CHANGED
|
@@ -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.
|