webcake-landing-mcp 1.0.66 → 1.0.68
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 +1 -1
- package/dist/changelog.json +14 -14
- package/dist/domains/landing/guide.js +2 -2
- package/dist/domains/landing/instructions.js +1 -1
- package/dist/http.js +1 -1
- package/dist/persistence/draft-cache.js +11 -5
- package/dist/persistence/html-ingest.js +567 -25
- package/dist/persistence/webcake-client.js +54 -0
- package/dist/server.js +8 -2
- package/dist/smoke.js +192 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/ingest.js +11 -5
- package/dist/tools/media.js +192 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,7 +187,7 @@ server, the `login` browser flow (+ backend contract), and how to grab a JWT by
|
|
|
187
187
|
|-------|-------|-------|
|
|
188
188
|
| **Reference** | `get_generation_guide` · `list_elements` · `get_element` · `get_page_schema` | nothing |
|
|
189
189
|
| **Generation** | `new_element` · `new_page_skeleton` · `validate_page` | nothing |
|
|
190
|
-
| **Media** | `search_images` (real Pexels stock photos) · `upload_images` (re-host external images) | nothing |
|
|
190
|
+
| **Media** | `search_images` (real Pexels stock photos) · `upload_images` (re-host external images, data: URIs, or local file paths from the user's machine) | nothing |
|
|
191
191
|
| **Ingest** | `ingest_html` · `ingest_url` (recreate an existing page) | nothing |
|
|
192
192
|
| **Persistence** | `list_organizations` · `create_page` · `list_pages` · `find_pages` · `get_page` · `update_page` · `add_section` · `patch_page` · `publish_page` | `WEBCAKE_API_BASE` + `WEBCAKE_JWT` |
|
|
193
193
|
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.68",
|
|
4
|
+
"d": "12/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "ingest_html and ingest_url now auto-detect absolute-canvas builder exports (LadiPage-family pages and Webcake-published HTML): bare positioned-div…",
|
|
7
|
+
"vi": "ingest_html và ingest_url nay tự động phát hiện các bản export từ builder absolute-canvas (trang LadiPage-family và HTML đã publish của Webcake):…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.0.67",
|
|
11
|
+
"d": "12/06/2026",
|
|
12
|
+
"type": "Added",
|
|
13
|
+
"en": "upload_images now accepts local file paths in the urls parameter — absolute POSIX paths (/…), home-directory paths (~/…), file:// URIs, and Windows…",
|
|
14
|
+
"vi": "upload_images nay chấp nhận đường dẫn file cục bộ trong tham số urls — đường dẫn POSIX tuyệt đối (/…), đường dẫn thư mục home (~/…), URI file://, và…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.0.66",
|
|
4
18
|
"d": "12/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Added",
|
|
27
41
|
"en": "create_page now auto-publishes after a successful create: builds the rendered app via the build host and calls the editor's publish_html route so…",
|
|
28
42
|
"vi": "create_page nay tự động publish sau khi tạo thành công: build rendered app qua build host rồi gọi route publish_html của editor để preview trang mới…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.0.62",
|
|
32
|
-
"d": "11/06/2026",
|
|
33
|
-
"type": "Added",
|
|
34
|
-
"en": "validate_page now warns when a text-block's estimated rendered height overflows onto a sibling element placed directly below its declared box; the…",
|
|
35
|
-
"vi": "validate_page nay cảnh báo khi chiều cao render ước tính của text-block tràn xuống phần tử anh em đặt ngay phía dưới khung khai báo; cảnh báo nêu…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.61",
|
|
39
|
-
"d": "11/06/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "ingest_html and ingest_url now return a size_hint field ({ height, basis, css? }) on every AST section, providing the desktop section height in px…",
|
|
42
|
-
"vi": "ingest_html và ingest_url nay trả về trường size_hint ({ height, basis, css? }) trên mỗi section trong AST, cung cấp chiều cao desktop của section…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -114,7 +114,7 @@ SECTION BUILD HINTS (apply to whichever sections the chosen archetype uses)
|
|
|
114
114
|
RULES
|
|
115
115
|
- Visible content goes in "specials" (text-block.specials.text, image-block.specials.src…), NEVER in "styles".
|
|
116
116
|
- Colors as rgba(r,g,b,a). fontSize/borderWidth/top/left/width/height are NUMBERS (px). borderRadius is a STRING with CSS units ("8px", "50%", "16px 16px 0 0") — a bare number or unit-less string is auto-coerced to px by the server, but write the unit explicitly to avoid surprises.
|
|
117
|
-
- IMAGES: a real landing page has images (hero/product shot, feature icons, about photo). SOURCE PRIORITY: (1) images the user supplied or that exist in the reference HTML/URL (ingest AST images/background_images/og_image) → re-host them via upload_images and use those EXACT images in their slots — never swap them for stock photos; (2) only for slots with NO source image → call search_images with a short English subject (e.g. 'fresh coffee cup', 'modern office team') and put a returned URL into image-block specials.src — use src.large for a hero/banner, src.medium for a card/thumb (avg_color helps pick a matching section background); (3) if search_images returns ok:false, is unreachable, or has NO photo that fits the slot → find a real image YOURSELF using whatever web search/fetch capability you have (the brand's own site, the product page, a free-to-use source), then re-host it via upload_images and use the returned URL; (4) a PLACEHOLDER is the LAST resort, ONLY after (2) AND (3) both failed — sized to the box: "https://placehold.co/<width>x<height>". Never jump straight from a failed search to a placeholder without trying (3). NEVER leave src empty — it renders blank on the live page. The server automatically derives styles.background from specials.src on every expand (create/update/validate) using the editor's exact format: 'center center/ cover no-repeat scroll content-box url(<src>) border-box' — you do NOT need to set styles.background manually; if you do hand-write it, it must contain url(...). A SECTION background may layer a gradient overlay over an image: 'linear-gradient(...), center center/ cover no-repeat scroll content-box url(<src>) border-box' — the server canonicalises any url() layer into that exact editor shorthand on expand (other url() formats survive the first save but get mangled to 'undefined/ undefined/ …' the moment the page is edited in the Webcake editor). gallery.media = array of OBJECTS {type:'image', link:'<real-or-placeholder-url>', linkVideo:'', typeVideo:'youtube', imageCompression:true} (NOT plain URL strings — the gallery reads item.link); video.specials.img = a poster image (real photo, else placeholder). Do NOT set a flat (no url()) styles.background on a video element — it suppresses the poster image.
|
|
117
|
+
- IMAGES: a real landing page has images (hero/product shot, feature icons, about photo). SOURCE PRIORITY: (1) images the user supplied — including local file paths from their computer (pass the path directly in upload_images 'urls'; NEVER upload a user's local file to a third-party host like catbox or imgur to obtain a URL first) — or that exist in the reference HTML/URL (ingest AST images/background_images/og_image) → re-host them via upload_images and use those EXACT images in their slots — never swap them for stock photos; (2) only for slots with NO source image → call search_images with a short English subject (e.g. 'fresh coffee cup', 'modern office team') and put a returned URL into image-block specials.src — use src.large for a hero/banner, src.medium for a card/thumb (avg_color helps pick a matching section background); (3) if search_images returns ok:false, is unreachable, or has NO photo that fits the slot → find a real image YOURSELF using whatever web search/fetch capability you have (the brand's own site, the product page, a free-to-use source), then re-host it via upload_images and use the returned URL; (4) a PLACEHOLDER is the LAST resort, ONLY after (2) AND (3) both failed — sized to the box: "https://placehold.co/<width>x<height>". Never jump straight from a failed search to a placeholder without trying (3). NEVER leave src empty — it renders blank on the live page. The server automatically derives styles.background from specials.src on every expand (create/update/validate) using the editor's exact format: 'center center/ cover no-repeat scroll content-box url(<src>) border-box' — you do NOT need to set styles.background manually; if you do hand-write it, it must contain url(...). A SECTION background may layer a gradient overlay over an image: 'linear-gradient(...), center center/ cover no-repeat scroll content-box url(<src>) border-box' — the server canonicalises any url() layer into that exact editor shorthand on expand (other url() formats survive the first save but get mangled to 'undefined/ undefined/ …' the moment the page is edited in the Webcake editor). gallery.media = array of OBJECTS {type:'image', link:'<real-or-placeholder-url>', linkVideo:'', typeVideo:'youtube', imageCompression:true} (NOT plain URL strings — the gallery reads item.link); video.specials.img = a poster image (real photo, else placeholder). Do NOT set a flat (no url()) styles.background on a video element — it suppresses the poster image.
|
|
118
118
|
- CONTRAST (check EVERY text element against the band it sits on, especially SATURATED / mid-tone bands like yellow, orange, teal, pink — there "light vs dark text" is not obvious, so decide by the band's luminance): light bands → near-black text (e.g. rgba(20,30,25,1)); dark bands → near-white text; a saturated/mid-tone band → whichever of near-black or near-white actually reads (for a bright yellow/amber band that means DARK text, not white/grey). NEVER use muted-grey, low-alpha (alpha < ~0.85), or near-white text on a colored band — that is exactly what makes labels look faded/sunken. Muted-grey is ONLY for secondary text on a white/very-light band. Icons and their captions follow the SAME rule as the text beside them.
|
|
119
119
|
- movable:false for section/slide/grid-item/popup; otherwise true. runtime is always {}.
|
|
120
120
|
- Every form input MUST have a unique specials.field_name.
|
|
@@ -146,7 +146,7 @@ WORKFLOW (recommended)
|
|
|
146
146
|
1. Call get_generation_guide (this) once, then new_page_skeleton for the top-level shape.
|
|
147
147
|
2. For each element type you'll use, call get_element to learn its specials & see an example.
|
|
148
148
|
3. Optionally call new_element to get a correct skeleton, then fill specials + coordinates.
|
|
149
|
-
3b. For every image the page needs (hero, product, about, feature, gallery): if the slot has a source image (user-supplied or from the reference HTML/URL), upload_images it and use the returned Webcake URL; otherwise call search_images and put a returned URL into specials.src / gallery item.link. If search_images fails or has no fitting photo, find a real image yourself (web search/fetch → upload_images). Use placehold.co ONLY as the last resort when both search_images AND your own search failed.
|
|
149
|
+
3b. For every image the page needs (hero, product, about, feature, gallery): if the slot has a source image (user-supplied — including local file paths from their computer, pass the path directly in upload_images 'urls'; NEVER relay the user's local file through a third-party host like catbox or imgur — or from the reference HTML/URL), upload_images it and use the returned Webcake URL; otherwise call search_images and put a returned URL into specials.src / gallery item.link. If search_images fails or has no fitting photo, find a real image yourself (web search/fetch → upload_images). Use placehold.co ONLY as the last resort when both search_images AND your own search failed.
|
|
150
150
|
4. Assemble { page, popup, settings, options, cartConfigs }.
|
|
151
151
|
5. Call validate_page and fix every error AND every warning — warnings are visible defects (text spilling onto the element below, off-canvas boxes, empty bands at a section's bottom, missing field_name, dead event targets), not advisory polish. Re-validate until the warning list is empty; only a demonstrably false positive may remain (tell the user which and why).
|
|
152
152
|
6. To save: call list_organizations. If the account has EXACTLY ONE organization, create_page will auto-select it — no need to ask. If there are MULTIPLE organizations, show them to the user and ask which to use (highlight is_default as the suggested default); pass the chosen organization_id to create_page. If the user explicitly wants to save without any organization, pass organization_id:"personal". Then create_page (dry_run first, then dry_run:false). Note: create_page itself enforces this — it refuses to guess between multiple orgs and returns the org list asking you to pick.
|
|
@@ -37,7 +37,7 @@ MODEL (essentials):
|
|
|
37
37
|
- PREMIUM CRAFT (read "sang"): generous whitespace (don't cram; ~48–72px above each band's first element, ≥16–24px between elements); clear type scale (H1 40–56 / body 16–18, big jump); ONE accent used sparingly + neutrals; snap spacing to an 8px grid; reuse the same content width / margin / card+button radius across sections.
|
|
38
38
|
- STICKY HEADER: a sticky/fixed header (config.sticky) OVERLAYS the page — it does NOT push sections below it down. Offset the first section's top content DOWN by the header height (~60–72px) so nothing hides behind it, and do NOT duplicate the shop name in both the header and the top of the hero. A non-sticky header stacks normally and needs no offset.
|
|
39
39
|
- Visible content lives in specials (text, src, field_name…), never in styles. Colors as rgba(). Animation in config.animation={name,delay,duration,repeat}. Form inputs need a unique specials.field_name (use canonical keys: full_name, phone_number, email, address, quantity).
|
|
40
|
-
- IMAGES: include them (hero/product, feature icons, about photo). SOURCE PRIORITY: (1) images the user
|
|
40
|
+
- IMAGES: include them (hero/product, feature icons, about photo). SOURCE PRIORITY: (1) images the user supplied (including local file paths from their computer — pass the path directly in upload_images urls; NEVER upload a user's local file to a third-party host like catbox or imgur first) or that exist in the reference HTML/URL → re-host via upload_images and use those exact images; (2) only for slots with NO source image → call search_images with a short English subject (e.g. 'fresh coffee cup') and put a returned URL (src.large for a hero/banner, src.medium for a card/thumb) into the image-block specials.src; it works out of the box (a shared proxy supplies images); (3) if search_images returns ok:false / is unreachable / has no fitting photo → find a real image YOURSELF using whatever web search/fetch capability you have (brand site, product page, free-to-use source) and re-host it via upload_images; (4) a PLACEHOLDER sized to the box (https://placehold.co/<width>x<height>) is the LAST resort, only after (2) AND (3) both failed. (gallery.media = array of OBJECTS {type:'image',link:'<url>',linkVideo:'',typeVideo:'youtube',imageCompression:true} — NOT plain strings, the gallery reads item.link; video.specials.img = poster). NEVER leave src empty (renders blank). Ensure text contrasts with its section background.
|
|
41
41
|
|
|
42
42
|
- PREVIEW vs PUBLISH: for review share the EDITOR url (the builder renders the raw source). The editor_url SIGNS THE BROWSER IN automatically (it routes through the builder's /transport with the account token), so it works even when the user isn't logged in — but for the same reason it must go to the PAGE OWNER ONLY, never into anything public. create_page AUTO-PUBLISHES on success (builds the rendered app + publish_html), so a fresh page's preview_url renders right away — but the preview host (preview.localhost:5800 local / staging.webcake.me staging / www.webcake.me prod) only serves it for ~10 MINUTES after the last publish, then shows "Preview page is expired". The EDIT routes (update_page/add_section/patch_page) store source only — after finishing a round of edits, run publish_page({ page_id, dry_run:false }) to rebuild the rendered app (else the preview shows the STALE pre-edit build). ONLY a custom_domain (publish_page({ page_id, custom_domain, dry_run:false })) gives a permanent public URL; without one the page has just the ephemeral preview link — say so and suggest attaching a domain the user already points at Webcake.
|
|
43
43
|
|
package/dist/http.js
CHANGED
|
@@ -203,7 +203,7 @@ export async function startHttpServer(port) {
|
|
|
203
203
|
if (transport.sessionId)
|
|
204
204
|
transports.delete(transport.sessionId);
|
|
205
205
|
};
|
|
206
|
-
const server = createServer();
|
|
206
|
+
const server = createServer({ allowLocalFiles: false });
|
|
207
207
|
await server.connect(transport);
|
|
208
208
|
await transport.handleRequest(req, res, body);
|
|
209
209
|
return;
|
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
* Commit path: update_page({draft_id, dry_run:false}) or
|
|
24
24
|
* patch_page({draft_id, patches?, dry_run:false}).
|
|
25
25
|
*
|
|
26
|
-
* Bounded + TTL'd
|
|
27
|
-
*
|
|
26
|
+
* Bounded + TTL'd (SLIDING: every get/update refreshes the clock, so a draft being
|
|
27
|
+
* actively worked on never expires mid-workflow); a lost draft (process restart,
|
|
28
|
+
* eviction, expiry) just means the model falls back to re-sending the full source —
|
|
29
|
+
* never a failure.
|
|
28
30
|
* Process-global, but draft_ids are random/unguessable AND persisting still uses the
|
|
29
31
|
* CALLER's own creds, so a draft only ever yields a page in the caller's account.
|
|
30
32
|
*/
|
|
@@ -71,10 +73,14 @@ export function updateDraft(id, source) {
|
|
|
71
73
|
d.created = Date.now();
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
|
-
/** Fetch a live (non-expired) draft, or null if missing/expired. */
|
|
76
|
+
/** Fetch a live (non-expired) draft, or null if missing/expired. Refreshes the TTL (sliding expiration) so an in-progress workflow never loses its draft. */
|
|
75
77
|
export function getDraft(id) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
sweep(now);
|
|
80
|
+
const d = store.get(id);
|
|
81
|
+
if (d)
|
|
82
|
+
d.created = now;
|
|
83
|
+
return d ?? null;
|
|
78
84
|
}
|
|
79
85
|
export function deleteDraft(id) {
|
|
80
86
|
store.delete(id);
|