webcake-landing-mcp 1.0.24 → 1.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/changelog.json +14 -14
- package/dist/domains/landing/elements/form.js +4 -1
- package/dist/domains/landing/guide.js +4 -3
- package/dist/domains/landing/instructions.js +1 -1
- package/dist/domains/landing/page-schema.json +6 -2
- package/dist/domains/landing/page.js +17 -4
- package/dist/domains/landing/vocab.js +1 -1
- package/dist/http.js +28 -0
- package/dist/og.png +0 -0
- package/dist/web-guide.js +15 -7
- package/package.json +1 -1
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.0.26",
|
|
4
|
+
"d": "07/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "The HTTP server now serves a pre-rendered 1200×630 PNG social card at GET /og.png; the guide page's og:image and twitter:image meta tags now point…",
|
|
7
|
+
"vi": "HTTP server nay phục vụ ảnh social card PNG được render sẵn 1200×630 tại GET /og.png; các meta tag og:image và twitter:image của trang hướng dẫn nay…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.0.25",
|
|
11
|
+
"d": "07/06/2026",
|
|
12
|
+
"type": "Changed",
|
|
13
|
+
"en": "currency has moved from options.currency to settings.currency in the page source model; new_page_skeleton now emits it in the correct location,…",
|
|
14
|
+
"vi": "currency đã được chuyển từ options.currency sang settings.currency trong mô hình nguồn trang; new_page_skeleton giờ xuất đúng vị trí,…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.0.24",
|
|
4
18
|
"d": "07/06/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Added",
|
|
27
41
|
"en": "The HTTP server's GET / guide page is now bilingual (vi/en): append ?lang=en to the URL to switch to English (default is Vietnamese), a language…",
|
|
28
42
|
"vi": "Trang hướng dẫn GET / của HTTP server nay hỗ trợ song ngữ (vi/en): thêm ?lang=en vào URL để chuyển sang tiếng Anh (mặc định là tiếng Việt), kèm nút…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.0.20",
|
|
32
|
-
"d": "07/06/2026",
|
|
33
|
-
"type": "Changed",
|
|
34
|
-
"en": "get_generation_guide now includes a Layout Archetypes block that maps seven page types (sales/COD, lead-gen/service, event/invitation, app/SaaS…",
|
|
35
|
-
"vi": "get_generation_guide now includes a Layout Archetypes block that maps seven page types (sales/COD, lead-gen/service, event/invitation, app/SaaS…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.0.19",
|
|
39
|
-
"d": "07/06/2026",
|
|
40
|
-
"type": "Changed",
|
|
41
|
-
"en": "get_generation_guide now includes a Section Playbook block that lists a common section menu for lead-gen and COD sales pages (header, hero,…",
|
|
42
|
-
"vi": "get_generation_guide nay có thêm khối \"Section Playbook\" liệt kê bộ section thường dùng cho trang thu lead và bán hàng COD (header, hero, tính…"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
@@ -276,6 +276,7 @@ export const FORM = [
|
|
|
276
276
|
field_type: "'postal_code' | absent — 'postal_code' switches validation from phone OTP to a postal-code regex.",
|
|
277
277
|
condition: "'limit_5' | 'limit_6' | 'custom' — postal-code regex selector (active when field_type='postal_code').",
|
|
278
278
|
pattern: "string regex — custom postal-code pattern when condition='custom'.",
|
|
279
|
+
message_otp_wrong: "string — custom error message shown when the entered OTP is wrong (read by the form on submit-error). Optional.",
|
|
279
280
|
},
|
|
280
281
|
seed: (el) => {
|
|
281
282
|
seedPosition(el);
|
|
@@ -296,12 +297,14 @@ export const FORM = [
|
|
|
296
297
|
},
|
|
297
298
|
{
|
|
298
299
|
type: "group-select-item", category: "form", container: false, field: true, defaultName: "Group Select Item",
|
|
299
|
-
summary: "One attribute (or the quantity) inside group-select.
|
|
300
|
+
summary: "One attribute (or the quantity) inside group-select. Attribute items populate options from the product catalog (window.sync.products) at runtime based on attrName + the parent's sprod; the quantity item carries a STATIC options array.",
|
|
300
301
|
useWhen: "Child of group-select only.",
|
|
301
302
|
keySpecials: {
|
|
302
303
|
field_name: "REQUIRED unique data key (becomes 'quantity' when field_quantity=true).",
|
|
304
|
+
field_placeholder: "string — the item's visible label/placeholder (the editor seeds 'AttrName' for attribute items, 'Quantity' for the quantity item).",
|
|
303
305
|
field_quantity: "boolean — when true this item is the quantity selector (value goes to parent._setQuantity), not a product attribute. Only one item per group.",
|
|
304
306
|
attrName: "string — the product attribute name this item maps to (e.g. 'Color','Size'); numeric strings ('1','2') index product_attributes for custom products; 'sprod-name'/'sprod-sku' show the product name/SKU.",
|
|
307
|
+
options: "array [{id,name,value}] — STATIC option list used by the quantity item (field_quantity=true), e.g. 1..4; attribute items leave this empty and populate from the catalog at runtime.",
|
|
305
308
|
default_value: "string — pre-selected option value, or 'default-none'/empty for no default.",
|
|
306
309
|
required: "boolean — require selection when the item and its parent group are visible.",
|
|
307
310
|
},
|
|
@@ -8,12 +8,13 @@ export const GENERATION_GUIDE = `You are generating the JSON source of a Webcake
|
|
|
8
8
|
|
|
9
9
|
OUTPUT (top-level page source — matches the real editor shape)
|
|
10
10
|
- Return ONE JSON object:
|
|
11
|
-
{ "page": [<section>...], "popup": [<popup>...], "settings": {...},
|
|
12
|
-
"options": { "
|
|
11
|
+
{ "page": [<section>...], "popup": [<popup>...], "dynamic_pages": [], "settings": {...},
|
|
12
|
+
"options": { "mobileOnly":false, "versionID":null }, "cartConfigs": { "isActive":false }, "svariations": [] }
|
|
13
|
+
- "dynamic_pages" (dataset-bound sections) and "svariations" (product/variation data) stay [] for a static page — keep them when editing so commerce data isn't dropped. currency lives in settings.currency, NOT options.
|
|
13
14
|
- "page" is an array of SECTIONS stacked vertically (index 0 = top). Each item MUST be type "section" (or "dynamic_page").
|
|
14
15
|
- "popup" is a SEPARATE top-level array of popup elements — do NOT nest popups inside "page". A button opens one via a click event { action:"open_popup", target:"<popup id>" }.
|
|
15
16
|
- All other elements (text, image, button, form…) live inside a section's "children".
|
|
16
|
-
- "settings" carries SEO + page config: title, description, keywords, favicon, fontGeneral, width_section {desktop:960,mobile:420}, country, fb_tracking_code, tiktok_script, extra_css, extra_script (call new_page_skeleton for a ready default).
|
|
17
|
+
- "settings" carries SEO + page config: title, description, keywords, robots, canonical, favicon, fontGeneral, width_section {desktop:960,mobile:420}, country, currency, fb_tracking_code, tiktok_script, extra_css, extra_script, bhet (head code), bbet (body-end code) (call new_page_skeleton for a ready default).
|
|
17
18
|
|
|
18
19
|
ELEMENT NODE (every element)
|
|
19
20
|
{ "id": "<unique ~8-char [A-Za-z0-9_]>", "type": "<type>",
|
|
@@ -14,7 +14,7 @@ RULES (follow for every request):
|
|
|
14
14
|
- Organizations: call list_organizations and ask which to use; default to the is_default org. Endpoints are owner-scoped (only the account's own pages).
|
|
15
15
|
|
|
16
16
|
MODEL (essentials):
|
|
17
|
-
- Top-level: { page:[sections], popup:[popups], settings:{}, options:{
|
|
17
|
+
- Top-level: { page:[sections], popup:[popups], dynamic_pages:[], settings:{}, options:{mobileOnly,versionID}, cartConfigs:{isActive:false}, svariations:[] }. Popups are a SEPARATE top-level array, NOT inside page; currency lives in settings.currency (not options). Leave dynamic_pages/svariations as [] for a static page, but keep them on edit round-trips.
|
|
18
18
|
- Element: { id, type, properties, responsive:{desktop,mobile:{config,styles}}, specials, children, runtime, events }. Absolute canvas: children carry numeric top/left/width/height (px) per breakpoint (canvas width desktop=960, mobile=420); sections own a height.
|
|
19
19
|
- CENTERING (the #1 layout defect — do the math, don't eyeball): to center a box compute left = round((canvas - width)/2) — 960 desktop, 420 mobile. textAlign:center only centers text inside the box, not the box itself. For a row of N items, center the whole row block (startLeft = round((canvas - (N*item + (N-1)*gap))/2)). Keep 0 ≤ left and left+width ≤ canvas on each breakpoint.
|
|
20
20
|
- 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.
|
|
@@ -24,10 +24,9 @@
|
|
|
24
24
|
},
|
|
25
25
|
"options": {
|
|
26
26
|
"type": "object",
|
|
27
|
-
"description": "Editor options.",
|
|
27
|
+
"description": "Editor options: { mobileOnly, versionID }. NOTE: currency lives in settings.currency, not here.",
|
|
28
28
|
"additionalProperties": true,
|
|
29
29
|
"properties": {
|
|
30
|
-
"currency": { "type": "string", "examples": ["VND", "USD"] },
|
|
31
30
|
"mobileOnly": { "type": "boolean", "description": "true = page only renders the mobile breakpoint." },
|
|
32
31
|
"versionID": { "type": ["string", "null"] }
|
|
33
32
|
}
|
|
@@ -51,6 +50,11 @@
|
|
|
51
50
|
"properties": { "desktop": { "type": "number" }, "mobile": { "type": "number" } }
|
|
52
51
|
},
|
|
53
52
|
"country": { "type": "string", "description": "Dialing/locale code, e.g. \"84\"." },
|
|
53
|
+
"currency": { "type": "string", "description": "Page currency (the editor's canonical home for currency).", "examples": ["VND", "USD"] },
|
|
54
|
+
"robots": { "type": "string", "description": "SEO robots meta directive." },
|
|
55
|
+
"canonical": { "type": "string", "description": "SEO canonical URL." },
|
|
56
|
+
"bhet": { "type": "string", "description": "Custom code injected at the end of <head>." },
|
|
57
|
+
"bbet": { "type": "string", "description": "Custom code injected before </body>." },
|
|
54
58
|
"fb_tracking_code": { "type": "string", "description": "Facebook pixel id." },
|
|
55
59
|
"tiktok_script": { "type": "string" },
|
|
56
60
|
"global_track_ids": { "type": "array" },
|
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
/** Default page-level settings (subset of the ~
|
|
1
|
+
/** Default page-level settings (subset of the ~44 real keys; covers the essentials). */
|
|
2
2
|
export function defaultSettings(overrides = {}) {
|
|
3
3
|
return {
|
|
4
4
|
title: "",
|
|
5
5
|
description: "",
|
|
6
6
|
keywords: "",
|
|
7
|
+
robots: "",
|
|
8
|
+
canonical: "",
|
|
7
9
|
favicon: "",
|
|
8
10
|
thumbnail: "",
|
|
9
11
|
fontGeneral: "'Roboto', sans-serif",
|
|
10
12
|
width_section: { desktop: 960, mobile: 420 },
|
|
11
13
|
country: "84",
|
|
14
|
+
// currency lives in settings (the editor's canonical home), NOT in options.
|
|
15
|
+
currency: "VND",
|
|
12
16
|
fb_tracking_code: "",
|
|
13
17
|
tiktok_script: "",
|
|
14
18
|
global_track_ids: [],
|
|
19
|
+
global_tracks: {}, // the editor always re-writes this to {} on save.
|
|
15
20
|
extra_css: "",
|
|
16
21
|
extra_script: "",
|
|
22
|
+
bhet: "", // custom code injected at the end of <head>.
|
|
23
|
+
bbet: "", // custom code injected before </body>.
|
|
24
|
+
global_compress_image: { enable: true, option: 300, keep_solution: false },
|
|
17
25
|
auto_save_draft: true,
|
|
18
26
|
auto_save_info_user: false,
|
|
19
27
|
send_info_to_thank_page: true,
|
|
@@ -22,14 +30,19 @@ export function defaultSettings(overrides = {}) {
|
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
24
32
|
* Build a complete, empty top-level page source matching the real editor shape:
|
|
25
|
-
* { page, popup, settings, options, cartConfigs }.
|
|
33
|
+
* { page, popup, dynamic_pages, settings, options, cartConfigs, svariations }.
|
|
34
|
+
* Fill `page` with sections. `dynamic_pages`/`svariations` stay empty for static,
|
|
35
|
+
* non-commerce pages but are emitted so edit round-trips don't drop them, and
|
|
36
|
+
* `options` carries only { mobileOnly, versionID } (currency lives in settings).
|
|
26
37
|
*/
|
|
27
38
|
export function createPageSource(opts = {}) {
|
|
28
39
|
return {
|
|
29
40
|
page: [],
|
|
30
41
|
popup: [],
|
|
42
|
+
dynamic_pages: [],
|
|
31
43
|
settings: defaultSettings(opts.settings ?? {}),
|
|
32
|
-
options: {
|
|
33
|
-
cartConfigs: {},
|
|
44
|
+
options: { mobileOnly: opts.mobileOnly ?? false, versionID: null },
|
|
45
|
+
cartConfigs: { isActive: false },
|
|
46
|
+
svariations: [],
|
|
34
47
|
};
|
|
35
48
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Landing-page vocabulary: the fixed canvas, the event triggers, and the
|
|
3
3
|
* per-trigger action maps. "Extra:" notes list the action-specific event-object
|
|
4
4
|
* fields the render_v4 dispatcher reads beyond { id, type, action, target }.
|
|
5
|
-
* Derived from assets/render_v4/
|
|
5
|
+
* Derived from assets/render_v4/event/index.js.
|
|
6
6
|
*/
|
|
7
7
|
export const CANVAS = { desktopWidth: 960, mobileWidth: 420, defaultSectionHeight: 800 };
|
|
8
8
|
export const EVENT_TRIGGERS = ["click", "hover", "success", "error", "unset", "delay"];
|
package/dist/http.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* All logging stays on stderr (console.error), same as stdio mode.
|
|
13
13
|
*/
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
15
16
|
import { createServer as createHttpServer } from "node:http";
|
|
16
17
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
17
18
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -19,6 +20,21 @@ import { createServer } from "./server.js";
|
|
|
19
20
|
import { ICON_SVG, ICON_MIME } from "./branding.js";
|
|
20
21
|
import { guideHtml, ogImageSvg, normalizeLang } from "./web-guide.js";
|
|
21
22
|
const MCP_PATH = "/mcp";
|
|
23
|
+
// The raster social card (1200x630), pre-rendered and committed at src/og.png,
|
|
24
|
+
// mirrored to dist/og.png by copy-assets. Served at GET /og.png as the og:image —
|
|
25
|
+
// SVG OG images don't unfurl on Facebook/X/LinkedIn/Zalo. Read once, lazily.
|
|
26
|
+
let OG_PNG = null;
|
|
27
|
+
function ogImagePng() {
|
|
28
|
+
if (OG_PNG)
|
|
29
|
+
return OG_PNG;
|
|
30
|
+
try {
|
|
31
|
+
OG_PNG = readFileSync(new URL("./og.png", import.meta.url));
|
|
32
|
+
return OG_PNG;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
22
38
|
function sendJson(res, status, body) {
|
|
23
39
|
res.writeHead(status, { "content-type": "application/json" });
|
|
24
40
|
res.end(JSON.stringify(body));
|
|
@@ -75,6 +91,18 @@ export async function startHttpServer(port) {
|
|
|
75
91
|
return res.end(ICON_SVG);
|
|
76
92
|
}
|
|
77
93
|
// Social-card image referenced by the landing page's og:image / twitter:image.
|
|
94
|
+
// PNG is the canonical og:image (unfurls everywhere); the SVG stays for clients
|
|
95
|
+
// that prefer it (Slack/Telegram/Discord) and as a fallback if og.png is absent.
|
|
96
|
+
if (req.method === "GET" && path === "/og.png") {
|
|
97
|
+
const png = ogImagePng();
|
|
98
|
+
if (png) {
|
|
99
|
+
res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=86400" });
|
|
100
|
+
return res.end(png);
|
|
101
|
+
}
|
|
102
|
+
// Fall back to the SVG card if the raster asset didn't ship.
|
|
103
|
+
res.writeHead(200, { "content-type": ICON_MIME, "cache-control": "public, max-age=86400" });
|
|
104
|
+
return res.end(ogImageSvg());
|
|
105
|
+
}
|
|
78
106
|
if (req.method === "GET" && path === "/og.svg") {
|
|
79
107
|
res.writeHead(200, { "content-type": ICON_MIME, "cache-control": "public, max-age=86400" });
|
|
80
108
|
return res.end(ogImageSvg());
|
package/dist/og.png
ADDED
|
Binary file
|
package/dist/web-guide.js
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* It's also built to be *shareable*: a full SEO `<head>` (description, canonical,
|
|
17
17
|
* Open Graph, Twitter Card, JSON-LD for SoftwareApplication + WebSite + FAQPage)
|
|
18
18
|
* so links unfurl nicely on social/chat and the page can be indexed. The social
|
|
19
|
-
* card image is
|
|
19
|
+
* card image is a pre-rendered 1200x630 PNG at `/og.png` (committed src/og.png,
|
|
20
|
+
* regenerated by scripts/render-og.mjs from `ogImageSvg`) — PNG because SVG OG
|
|
21
|
+
* images don't unfurl on Facebook/X/LinkedIn/Zalo. `/og.svg` is still served too.
|
|
20
22
|
*
|
|
21
23
|
* Self-contained (inline CSS + the Webcake icon, no external assets/fonts/trackers)
|
|
22
24
|
* so it loads instantly and leaks nothing.
|
|
@@ -333,7 +335,8 @@ export function guideHtml(origin, lang = "vi") {
|
|
|
333
335
|
const m = META[L];
|
|
334
336
|
const faq = FAQ[L];
|
|
335
337
|
const endpoint = `${origin}/mcp`;
|
|
336
|
-
|
|
338
|
+
// PNG (not SVG) so the card unfurls on Facebook/X/LinkedIn/Zalo; served by http.ts.
|
|
339
|
+
const ogImage = `${origin}/og.png`;
|
|
337
340
|
const selfPath = L === "en" ? "?lang=en" : "/";
|
|
338
341
|
const otherLang = L === "vi" ? "en" : "vi";
|
|
339
342
|
const otherHref = otherLang === "en" ? "?lang=en" : "?lang=vi";
|
|
@@ -396,14 +399,17 @@ export function guideHtml(origin, lang = "vi") {
|
|
|
396
399
|
<meta property="og:description" content="${m.desc}">
|
|
397
400
|
<meta property="og:url" content="${canonical}">
|
|
398
401
|
<meta property="og:image" content="${ogImage}">
|
|
402
|
+
<meta property="og:image:type" content="image/png">
|
|
399
403
|
<meta property="og:image:width" content="1200">
|
|
400
404
|
<meta property="og:image:height" content="630">
|
|
405
|
+
<meta property="og:image:alt" content="${m.title}">
|
|
401
406
|
<meta property="og:locale" content="${m.locale}">
|
|
402
407
|
<meta property="og:locale:alternate" content="${META[otherLang].locale}">
|
|
403
408
|
<meta name="twitter:card" content="summary_large_image">
|
|
404
409
|
<meta name="twitter:title" content="${m.title}">
|
|
405
410
|
<meta name="twitter:description" content="${m.desc}">
|
|
406
411
|
<meta name="twitter:image" content="${ogImage}">
|
|
412
|
+
<meta name="twitter:image:alt" content="${m.title}">
|
|
407
413
|
<script type="application/ld+json">${jsonLdScript}</script>
|
|
408
414
|
<style>
|
|
409
415
|
/* Light defaults. Dark applies via OS preference OR a forced [data-theme="dark"]
|
|
@@ -751,11 +757,13 @@ export function guideHtml(origin, lang = "vi") {
|
|
|
751
757
|
</body></html>`;
|
|
752
758
|
}
|
|
753
759
|
/**
|
|
754
|
-
* The social-card
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
760
|
+
* The social-card source: a self-contained 1200×630 branded SVG (the size social
|
|
761
|
+
* scrapers expect) — no external fonts/assets. This is the SOURCE that
|
|
762
|
+
* scripts/render-og.mjs rasterizes into the committed src/og.png used as the
|
|
763
|
+
* canonical `og:image` / `twitter:image` (PNG unfurls on Facebook/X/LinkedIn/Zalo
|
|
764
|
+
* where SVG doesn't). Still served verbatim at `/og.svg` for SVG-friendly clients
|
|
765
|
+
* (Slack/Telegram/Discord) and as the http.ts fallback. EDIT THIS, then re-run
|
|
766
|
+
* scripts/render-og.mjs and commit the regenerated PNG.
|
|
759
767
|
*/
|
|
760
768
|
export function ogImageSvg() {
|
|
761
769
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" fill="none" font-family="system-ui,-apple-system,Segoe UI,Roboto,sans-serif">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
4
4
|
"description": "MCP server exposing Webcake landing-page element schemas + AI usage hints, and persisting LLM-generated page sources to a Webcake backend.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|