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.
@@ -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. Options are NOT static — they are populated from the product catalog (window.sync.products) at runtime based on attrName + the parent's sprod.",
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": { "currency":"VND", "mobileOnly":false, "versionID":null }, "cartConfigs": {} }
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:{currency,mobileOnly,versionID}, cartConfigs:{} }. Popups are a SEPARATE top-level array, NOT inside page.
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 ~40 real keys; covers the essentials). */
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 }. Fill `page` with sections.
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: { currency: "VND", mobileOnly: opts.mobileOnly ?? false, versionID: null },
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/src/event/index.js.
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 served separately at `/og.svg` (see `ogImageSvg`, wired in http.ts).
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
- const ogImage = `${origin}/og.svg`;
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 image served at `/og.svg` and referenced by the page's
755
- * `og:image` / `twitter:image`. A self-contained 1200×630 branded SVG (the size
756
- * social scrapers expect) no external fonts/assets. Slack, Telegram, LinkedIn,
757
- * Discord render SVG OG images; a few (older Twitter/Facebook) may skip it, which
758
- * is acceptable for a dependency-free server.
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.24",
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": {