webcake-landing-mcp 1.0.60 → 1.0.61

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,11 @@
1
1
  [
2
+ {
3
+ "v": "1.0.61",
4
+ "d": "11/06/2026",
5
+ "type": "Added",
6
+ "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…",
7
+ "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…"
8
+ },
2
9
  {
3
10
  "v": "1.0.60",
4
11
  "d": "11/06/2026",
@@ -33,12 +40,5 @@
33
40
  "type": "Added",
34
41
  "en": "The expand pipeline now automatically derives styles.background from specials.src for every image-block node; the live published renderer reads only…",
35
42
  "vi": "Pipeline expand nay tự động tính styles.background từ specials.src cho mọi node image-block; renderer trên trang published chỉ đọc…"
36
- },
37
- {
38
- "v": "1.0.55",
39
- "d": "10/06/2026",
40
- "type": "Fixed",
41
- "en": "The install command now correctly locates claude_desktop_config.json on Windows when Claude Desktop was installed from the Microsoft Store: the…",
42
- "vi": "Lệnh install nay xác định đúng đường dẫn claude_desktop_config.json trên Windows khi Claude Desktop được cài từ Microsoft Store: bản Store bị…"
43
43
  }
44
44
  ]
@@ -154,5 +154,6 @@ EDITING an existing page
154
154
 
155
155
  REFERENCE INPUT (HTML page to clone or adapt)
156
156
  - ingest_html(html, detail:'full') / ingest_url(url, detail:'full') returns a richer AST for clone-quality rebuilds: CSS custom-property palette (design tokens by name), background_images from stylesheets (hero/CTA bg images invisible to a plain img scan), per-section blocks (repeating card/tile/step structures with title/body/image/cta), li lists, gradients, and images as { src, alt } objects. Use detail:'compact' (default) for a quick layout-only reference.
157
- - Map AST section roles to Webcake elements: hero section (background image/overlay) + text-block H1 + text-block subheading + button; features group per card (icon rectangle + text-block title + text-block body); stats bar group with text-block per stat; pricing group with text-block list + button; footer section (dark bg) + text-block + links. When the ingested page contains a composite widget (phone/device mockup, chat thread, mini dashboard, browser frame) rebuild it as ONE html-box (clone its HTML with all styles inlined), NOT as element soup.
157
+ - SECTION HEIGHTS: every AST section carries size_hint = { height, basis, css? } the section's desktop height on the 960px canvas (basis:'css' = an explicit height/min-height found in the source, css holds the raw value e.g. "100vh"; basis:'estimate' = content-volume math). Set each rebuilt section's desktop height FROM its size_hint (±15% to fit your actual element placement) instead of the 800px default, so the page's vertical rhythm tracks the source: a 72px header stays a slim bar, a 1200px pricing band stays tall. Mobile is NOT hinted redo the height per the mobile text math (stacked content is taller).
158
+ - Map AST section roles to Webcake elements: hero → section (background image/overlay) + text-block H1 + text-block subheading + button; features → group per card (icon rectangle + text-block title + text-block body); stats bar → group with text-block per stat; pricing → group with text-block list + button; footer → section (dark bg) + text-block + links. When the ingested page contains a composite widget (phone/device mockup, chat thread, mini dashboard, browser frame) → rebuild it as ONE html-box, NOT as element soup. The full AST hands you the SOURCE for this: section.widgets = [{ hint, html, css? }] — the widget's cleaned outerHTML plus the stylesheet rules that style it. Build the html-box FROM that html verbatim: inline each css rule into the matching elements' style="" attributes (an html-box has no <style> scope), wrap in a root div with width:100%;height:100%;box-sizing:border-box;overflow:hidden, and size the html-box to the widget's box — do NOT re-imagine the widget's markup from the summary fields.
158
159
  - Reference images are the user's assets — for BOTH intents (adapt AND clone), re-host every real image URL found in the AST (images, background_images, og_image) via upload_images and reuse it in the matching slot; never hotlink and never replace it with a search_images stock photo. intent='adapt' rewrites TEXT for the user's brand, not the imagery; search_images only fills slots that have no source image.`;
@@ -165,7 +165,16 @@ export function parseHtml(html, detail = "compact") {
165
165
  };
166
166
  }
167
167
  const sectionEls = findSections(body);
168
- const sections = sectionEls.map((el) => classifySection(el, detail));
168
+ const sections = sectionEls.map((el) => {
169
+ const sec = classifySection(el, detail);
170
+ sec.size_hint = computeSizeHint(el, sec, styleBlocks);
171
+ if (detail === "full") {
172
+ const widgets = detectWidgets(el, styleBlocks);
173
+ if (widgets.length)
174
+ sec.widgets = widgets;
175
+ }
176
+ return sec;
177
+ });
169
178
  // Brand hints from inline styles (both modes).
170
179
  const styleAttrs = [];
171
180
  body.querySelectorAll("[style]").forEach((el) => {
@@ -204,25 +213,33 @@ export function parseHtml(html, detail = "compact") {
204
213
  ...base,
205
214
  gradients: gradients.length ? gradients : undefined,
206
215
  };
207
- // Size-cap: drop blocks[].body first, then truncate lists.
208
- const serialized = JSON.stringify(result);
209
- if (serialized.length > FULL_SIZE_CAP) {
210
- // Strip block bodies.
216
+ // Size-cap shedding order: blocks[].body widgets[].css lists → widgets
217
+ // (widget html goes last — it's the clone-fidelity payload of full mode).
218
+ if (JSON.stringify(result).length > FULL_SIZE_CAP) {
211
219
  for (const sec of result.sections) {
212
- if (sec.blocks) {
220
+ if (sec.blocks)
213
221
  for (const blk of sec.blocks)
214
222
  delete blk.body;
223
+ }
224
+ if (JSON.stringify(result).length > FULL_SIZE_CAP) {
225
+ for (const sec of result.sections) {
226
+ if (sec.widgets)
227
+ for (const w of sec.widgets)
228
+ delete w.css;
215
229
  }
216
230
  }
217
- const s2 = JSON.stringify(result);
218
- if (s2.length > FULL_SIZE_CAP) {
219
- // Truncate lists.
231
+ if (JSON.stringify(result).length > FULL_SIZE_CAP) {
220
232
  for (const sec of result.sections) {
221
233
  if (sec.lists && sec.lists.length > 5)
222
234
  sec.lists = sec.lists.slice(0, 5);
223
235
  }
224
236
  result.truncated = true;
225
237
  }
238
+ if (JSON.stringify(result).length > FULL_SIZE_CAP) {
239
+ for (const sec of result.sections)
240
+ delete sec.widgets;
241
+ result.truncated = true;
242
+ }
226
243
  }
227
244
  return result;
228
245
  }
@@ -616,6 +633,202 @@ function pickLists(el, _blocks) {
616
633
  .filter((t) => t.length > 3 && t.length < 200)
617
634
  .slice(0, 15);
618
635
  }
636
+ // ─── size hint (desktop section height) ──────────────────────────────────────
637
+ const SIZE_DECL_RE = /(?:^|[;{\s])(min-height|height)\s*:\s*([\d.]+)(px|vh|rem|em)\b/gi;
638
+ const VH_PX = 8; // 1vh ≈ 8px (~800px viewport), so a 100vh hero lands near the editor's 800px default band
639
+ function clamp(v, lo, hi) {
640
+ return Math.min(hi, Math.max(lo, v));
641
+ }
642
+ /** lines ≈ ceil(chars × fontSize × glyphFactor / width) — same model as the generation guide's text math. */
643
+ function textLines(chars, fontSize, width, factor = 0.55) {
644
+ if (chars <= 0)
645
+ return 0;
646
+ return Math.max(1, Math.ceil((chars * fontSize * factor) / width));
647
+ }
648
+ /** Parse height/min-height declarations out of a CSS declaration string. */
649
+ function sizeDecls(decl) {
650
+ const out = [];
651
+ SIZE_DECL_RE.lastIndex = 0;
652
+ let m;
653
+ while ((m = SIZE_DECL_RE.exec(decl)) !== null) {
654
+ const kind = m[1].toLowerCase();
655
+ const v = parseFloat(m[2]);
656
+ const unit = m[3].toLowerCase();
657
+ const px = unit === "px" ? v : unit === "vh" ? v * VH_PX : v * 16; // rem/em ≈ 16px
658
+ if (px >= 40 && px <= 2400)
659
+ out.push({ kind, px: Math.round(px), raw: `${m[2]}${unit}` });
660
+ }
661
+ return out;
662
+ }
663
+ function escapeRe(s) {
664
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
665
+ }
666
+ /**
667
+ * Explicit height/min-height for a section element: its inline style, plus any
668
+ * stylesheet rule whose selector mentions the element's #id or one of its
669
+ * classes (whole-token match — `.hero` doesn't match `.hero-card`). Regex-level
670
+ * on purpose; full selector matching needs a renderer.
671
+ */
672
+ function cssSizeDecls(el, styleBlocks) {
673
+ const out = [];
674
+ const inline = el.getAttribute("style");
675
+ if (inline)
676
+ out.push(...sizeDecls(inline));
677
+ const id = el.getAttribute("id");
678
+ const classes = (el.getAttribute("class") ?? "").trim().split(/\s+/).filter(Boolean).slice(0, 4);
679
+ const tokens = [...(id ? ["#" + id] : []), ...classes.map((c) => "." + c)];
680
+ if (tokens.length) {
681
+ const matchers = tokens.map((t) => new RegExp(escapeRe(t) + "(?![\\w-])"));
682
+ const ruleRe = /([^{}]+)\{([^{}]*)\}/g;
683
+ for (const css of styleBlocks) {
684
+ ruleRe.lastIndex = 0;
685
+ let m;
686
+ while ((m = ruleRe.exec(css)) !== null) {
687
+ if (matchers.some((re) => re.test(m[1])))
688
+ out.push(...sizeDecls(m[2]));
689
+ }
690
+ }
691
+ }
692
+ return out;
693
+ }
694
+ /**
695
+ * Content-volume estimate of the section's desktop height (960px canvas,
696
+ * ~800px content column) so the rebuilt band is proportional to the source
697
+ * instead of a flat 800px default.
698
+ */
699
+ function estimateSectionHeight(el, sec) {
700
+ const role = sec.role;
701
+ if (role === "header")
702
+ return 72;
703
+ if (role === "footer") {
704
+ const linkRows = Math.ceil((sec.links?.length ?? 0) / 4);
705
+ return clamp(120 + linkRows * 32 + (sec.paragraphs?.length ?? 0) * 24, 140, 480);
706
+ }
707
+ let h = 140; // band padding (top + bottom)
708
+ if (sec.heading) {
709
+ const fs = role === "hero" ? 48 : 36;
710
+ h += textLines(sec.heading.length, fs, 800, 0.6) * Math.round(fs * 1.2) + 20;
711
+ }
712
+ if (sec.subheading)
713
+ h += textLines(sec.subheading.length, 18, 640) * 27 + 16;
714
+ for (const p of sec.paragraphs ?? [])
715
+ h += textLines(p.length, 16, 640) * 24 + 12;
716
+ if (sec.ctas?.length)
717
+ h += 76;
718
+ if (sec.form_fields?.length)
719
+ h += sec.form_fields.length * 64 + 24;
720
+ // Card/tile rows: full mode carries blocks; compact recounts from the DOM.
721
+ let cards = sec.blocks?.length ?? 0;
722
+ if (!cards && (role === "features" || role === "pricing" || role === "testimonials")) {
723
+ cards = Math.min(countFeatureBlocks(el), 12);
724
+ }
725
+ if (cards)
726
+ h += Math.ceil(cards / 3) * (role === "pricing" ? 420 : 260) + 24;
727
+ else if (sec.lists?.length)
728
+ h += sec.lists.length * 30;
729
+ const imgCount = sec.images?.length ?? 0;
730
+ if (role === "gallery")
731
+ h += Math.ceil(imgCount / 3) * 260;
732
+ else if (role === "hero")
733
+ h = Math.max(h, imgCount ? 560 : 480);
734
+ else if (imgCount)
735
+ h += 320; // a content image alongside/below the text
736
+ return clamp(Math.round(h / 10) * 10, 160, 1600);
737
+ }
738
+ function computeSizeHint(el, sec, styleBlocks) {
739
+ const estimate = estimateSectionHeight(el, sec);
740
+ const decls = cssSizeDecls(el, styleBlocks);
741
+ const fixed = decls.filter((d) => d.kind === "height").sort((a, b) => b.px - a.px)[0];
742
+ // An explicit height pins the band; min-height grows with content, so take the larger.
743
+ if (fixed)
744
+ return { height: fixed.px, basis: "css", css: fixed.raw };
745
+ const min = decls.filter((d) => d.kind === "min-height").sort((a, b) => b.px - a.px)[0];
746
+ if (min)
747
+ return { height: Math.max(min.px, estimate), basis: "css", css: min.raw };
748
+ return { height: estimate, basis: "estimate" };
749
+ }
750
+ // ─── full-mode: composite-widget extraction (html-box source) ────────────────
751
+ // Class/id keywords that mark a composite visual the guide rebuilds as ONE
752
+ // html-box. Conservative on purpose — generic words (card, window, slider)
753
+ // over-match ordinary content.
754
+ const WIDGET_HINT_RE = /\b(mockup|phone|device|browser|terminal|console|dashboard|chat|inbox|player)\b/i;
755
+ const WIDGET_HTML_CAP = 8000;
756
+ const WIDGET_CSS_CAP = 4000;
757
+ const MAX_WIDGETS_PER_SECTION = 2;
758
+ /** outerHTML cleaned for html-box reuse: scripts/styles stripped, whitespace collapsed. */
759
+ function cleanWidgetHtml(el) {
760
+ // Re-parse a copy so removals don't mutate the tree other pickers read.
761
+ const frag = parse(el.toString(), { lowerCaseTagName: true });
762
+ frag.querySelectorAll("script, style, noscript").forEach((n) => n.remove());
763
+ return frag
764
+ .toString()
765
+ .replace(/<!--[\s\S]*?-->/g, "")
766
+ .replace(/>\s+</g, "><")
767
+ .replace(/\s{2,}/g, " ")
768
+ .trim();
769
+ }
770
+ /** Stylesheet rules whose selector mentions a class/id used inside the widget HTML. */
771
+ function widgetCss(html, styleBlocks) {
772
+ const tokens = new Set();
773
+ for (const m of html.matchAll(/class="([^"]+)"/g)) {
774
+ for (const c of m[1].trim().split(/\s+/))
775
+ if (c)
776
+ tokens.add("." + c);
777
+ }
778
+ for (const m of html.matchAll(/id="([^"]+)"/g)) {
779
+ if (m[1].trim())
780
+ tokens.add("#" + m[1].trim());
781
+ }
782
+ if (!tokens.size)
783
+ return undefined;
784
+ const matchers = [...tokens].map((t) => new RegExp(escapeRe(t) + "(?![\\w-])"));
785
+ const parts = [];
786
+ let total = 0;
787
+ const ruleRe = /([^{}]+)\{([^{}]*)\}/g;
788
+ for (const css of styleBlocks) {
789
+ ruleRe.lastIndex = 0;
790
+ let m;
791
+ while ((m = ruleRe.exec(css)) !== null) {
792
+ const sel = m[1].trim();
793
+ if (!matchers.some((re) => re.test(sel)))
794
+ continue;
795
+ const rule = `${sel.replace(/\s+/g, " ")}{${m[2].trim().replace(/\s+/g, " ")}}`;
796
+ if (total + rule.length > WIDGET_CSS_CAP)
797
+ return parts.join("");
798
+ parts.push(rule);
799
+ total += rule.length;
800
+ }
801
+ }
802
+ return parts.length ? parts.join("") : undefined;
803
+ }
804
+ /**
805
+ * Find composite-widget candidates inside a section: OUTERMOST descendants
806
+ * whose class/id matches WIDGET_HINT_RE and that have real internal structure
807
+ * (≥3 descendant elements). Emits the cleaned HTML + matching CSS so the
808
+ * model's html-box reproduces the source instead of approximating it.
809
+ */
810
+ function detectWidgets(el, styleBlocks) {
811
+ const out = [];
812
+ const walk = (node) => {
813
+ for (const k of elementChildren(node)) {
814
+ if (out.length >= MAX_WIDGETS_PER_SECTION)
815
+ return;
816
+ const idCls = (k.getAttribute("id") ?? "") + " " + (k.getAttribute("class") ?? "");
817
+ const m = idCls.match(WIDGET_HINT_RE);
818
+ if (m && k.querySelectorAll("*").length >= 3) {
819
+ const html = cleanWidgetHtml(k);
820
+ if (html && html.length <= WIDGET_HTML_CAP) {
821
+ const css = widgetCss(html, styleBlocks);
822
+ out.push(css ? { hint: m[1].toLowerCase(), html, css } : { hint: m[1].toLowerCase(), html });
823
+ continue; // outermost only — don't descend into an emitted widget
824
+ }
825
+ }
826
+ walk(k);
827
+ }
828
+ };
829
+ walk(el);
830
+ return out;
831
+ }
619
832
  // ─── color / font helpers ────────────────────────────────────────────────────
620
833
  function elText(el) {
621
834
  const t = el?.text?.trim();
package/dist/smoke.js CHANGED
@@ -236,6 +236,28 @@ check("ingest: hero image captured", (hero?.images?.length ?? 0) > 0, hero?.imag
236
236
  const form = ast.sections.find((s) => s.role === "form");
237
237
  check("ingest: form fields captured", (form?.form_fields?.length ?? 0) >= 2, form?.form_fields);
238
238
  check("ingest: form submit CTA captured", !!form?.ctas?.[0]?.text?.includes("Place"), form?.ctas);
239
+ console.log("== ingest: size_hint (desktop section heights) ==");
240
+ check("ingest: every section has a size_hint", ast.sections.every((s) => (s.size_hint?.height ?? 0) > 0), ast.sections.map((s) => s.size_hint));
241
+ const headerHint = ast.sections.find((s) => s.role === "header")?.size_hint;
242
+ check("ingest: header size_hint is a slim bar", !!headerHint && headerHint.height <= 120, headerHint);
243
+ const heroHint = ast.sections.find((s) => s.role === "hero")?.size_hint;
244
+ check("ingest: hero size_hint is a tall band", !!heroHint && heroHint.height >= 400, heroHint);
245
+ const footerHint = ast.sections.find((s) => s.role === "footer")?.size_hint;
246
+ check("ingest: footer size_hint shorter than hero", !!footerHint && !!heroHint && footerHint.height < heroHint.height, { footerHint, heroHint });
247
+ const cssHintHtml = `<!DOCTYPE html><html><head><style>
248
+ .hero { min-height: 100vh; }
249
+ #promo { height: 560px; }
250
+ </style></head><body>
251
+ <section class="hero"><h1>Big hero</h1><p>Tagline goes here for the hero band.</p><button>Go</button></section>
252
+ <section id="promo"><h2>Promo</h2><p>Limited time offer on all plans this week.</p><button>Claim</button></section>
253
+ <section style="min-height: 75vh"><h2>Inline</h2><p>Inline-styled band with its own height.</p></section>
254
+ </body></html>`;
255
+ const cssAst = parseHtml(cssHintHtml);
256
+ const cssHints = cssAst.sections.map((s) => s.size_hint);
257
+ check("ingest: 100vh class rule → css basis ~800px", cssHints[0]?.basis === "css" && cssHints[0]?.height === 800 && cssHints[0]?.css === "100vh", cssHints[0]);
258
+ check("ingest: explicit px height by #id → css basis exact", cssHints[1]?.basis === "css" && cssHints[1]?.height === 560, cssHints[1]);
259
+ check("ingest: inline min-height vh → css basis", cssHints[2]?.basis === "css" && cssHints[2]?.height === 600, cssHints[2]);
260
+ check("ingest: .hero rule does not leak into #promo", cssHints[1]?.css !== "100vh", cssHints[1]);
239
261
  console.log("== ingest: tolerates empty/CSR-shell HTML ==");
240
262
  const empty = parseHtml("");
241
263
  check("ingest: empty input → warning", (empty.warnings?.length ?? 0) > 0, empty.warnings);
@@ -296,6 +318,27 @@ check("ingest: compact has no blocks", compactAst.sections.every((s) => s.blocks
296
318
  check("ingest: compact has no gradients", compactAst.gradients === undefined, compactAst.gradients);
297
319
  check("ingest: compact images are plain strings", compactAst.sections.every((s) => !s.images || s.images.every((i) => typeof i === "string")), compactAst.sections.map((s) => s.images));
298
320
  check("ingest: default (no detail arg) is compact-compatible", parseHtml(sampleHtml).sections.every((s) => s.blocks === undefined));
321
+ console.log("== ingest: full mode — composite widget extraction (html-box source) ==");
322
+ const widgetHtml = `<!DOCTYPE html><html><head><style>
323
+ .phone-mockup { width: 320px; border-radius: 24px; background: #111; }
324
+ .chat-bubble { padding: 8px 12px; border-radius: 12px; background: #f1f1f1; }
325
+ .chat-bubble.right { background: #0A7C6E; color: #fff; }
326
+ </style></head><body>
327
+ <section><h1>Talk to us</h1><p>Our assistant answers around the clock for you.</p><button>Start chat</button>
328
+ <div class="phone-mockup"><div class="screen"><div class="chat-bubble left">Xin chào!</div><div class="chat-bubble right">Chào bạn</div><div class="chat-input">Type a message…</div></div><script>track()</script></div>
329
+ </section>
330
+ <section><h2>About</h2><p>Plain prose section with no composite widget at all.</p></section>
331
+ </body></html>`;
332
+ const widgetAst = parseHtml(widgetHtml, "full");
333
+ const wSec = widgetAst.sections[0];
334
+ check("widgets: detected on the mockup section", (wSec?.widgets?.length ?? 0) === 1, wSec?.widgets?.map((w) => w.hint));
335
+ const w0 = wSec?.widgets?.[0];
336
+ check("widgets: hint from class keyword", w0?.hint === "phone" || w0?.hint === "mockup", w0?.hint);
337
+ check("widgets: html keeps inner structure", !!w0?.html.includes("chat-bubble"), w0?.html);
338
+ check("widgets: scripts stripped from html", !(w0?.html ?? "").includes("<script"), w0?.html);
339
+ check("widgets: matching css rules attached", !!w0?.css?.includes(".phone-mockup") && !!w0?.css?.includes(".chat-bubble"), w0?.css);
340
+ check("widgets: none on plain sections", widgetAst.sections[1]?.widgets === undefined, widgetAst.sections[1]);
341
+ check("widgets: compact mode emits none", parseHtml(widgetHtml, "compact").sections.every((s) => s.widgets === undefined));
299
342
  console.log("== ingest: nested-grid block detection (depth > 1) ==");
300
343
  // section > .grid-wrapper > .card — blocks must be found even though cards are not direct children
301
344
  const nestedGridHtml = `<!DOCTYPE html><html lang="en"><head><title>T</title></head><body>
@@ -24,9 +24,9 @@ import { parseHtml, fetchHtml } from "../persistence/html-ingest.js";
24
24
  const detailParam = z
25
25
  .enum(["compact", "full"])
26
26
  .optional()
27
- .describe("Level of detail in the returned AST. 'compact' (default) — backward-compatible ~2-5 KB shape with top colors/fonts from inline styles. 'full' — richer AST: CSS custom-property palette (design tokens by name), background_images from stylesheets, gradients, per-section blocks (repeating card/tile/step structures with title/body/image/cta), li lists, extended paragraphs, and images as { src, alt } objects. Use 'full' for clone-faithful rebuilds. Image URLs found in the result (images, background_images, og_image) are the user's assets: re-host them via upload_images and reuse them in the generated page for BOTH intents (never hotlink, never replace them with search_images stock photos).");
27
+ .describe("Level of detail in the returned AST. 'compact' (default) — backward-compatible ~2-5 KB shape with top colors/fonts from inline styles. 'full' — richer AST: CSS custom-property palette (design tokens by name), background_images from stylesheets, gradients, per-section blocks (repeating card/tile/step structures with title/body/image/cta), li lists, extended paragraphs, images as { src, alt } objects, and per-section widgets = { hint, html, css? } — the cleaned source HTML + matching CSS of composite visuals (phone/device mockup, chat thread, dashboard, browser frame) to rebuild VERBATIM as ONE html-box (inline the css; don't re-imagine the markup). Use 'full' for clone-faithful rebuilds. Image URLs found in the result (images, background_images, og_image) are the user's assets: re-host them via upload_images and reuse them in the generated page for BOTH intents (never hotlink, never replace them with search_images stock photos).");
28
28
  export function registerIngestTools(server) {
29
- server.tool("ingest_html", "Parses an HTML string into a reference AST: title, description, og_image, language, and sections classified by role (header, hero, features, about, form, cta, gallery, testimonials, pricing, faq, footer, unknown) with headings, subheadings, paragraphs, images, ctas, links, form fields — plus top colors, fonts, CSS custom-property palette, and background_images pulled from both inline styles and <style> blocks. Returns ~2-5KB (compact) or up to ~25KB (full). Use detail:'full' for clone-faithful rebuilds — it adds per-section blocks (cards/tiles/steps), li lists, gradients, and images as { src, alt } objects. Image URLs in the result (images, background_images, og_image) are the user's assets — re-host them via upload_images and reuse them for BOTH intents; use search_images only for slots with no source image.", {
29
+ server.tool("ingest_html", "Parses an HTML string into a reference AST: title, description, og_image, language, and sections classified by role (header, hero, features, about, form, cta, gallery, testimonials, pricing, faq, footer, unknown) with headings, subheadings, paragraphs, images, ctas, links, form fields, and a size_hint (desktop section height in px from the source CSS when explicit, else a content-volume estimate; set the rebuilt section's desktop height from it) — plus top colors, fonts, CSS custom-property palette, and background_images pulled from both inline styles and <style> blocks. Returns ~2-5KB (compact) or up to ~25KB (full). Use detail:'full' for clone-faithful rebuilds — it adds per-section blocks (cards/tiles/steps), li lists, gradients, images as { src, alt } objects, and widgets (the source HTML + CSS of composite mockups, to paste into ONE html-box). Image URLs in the result (images, background_images, og_image) are the user's assets — re-host them via upload_images and reuse them for BOTH intents; use search_images only for slots with no source image.", {
30
30
  html: z.string().describe("Raw HTML of a page or a section."),
31
31
  intent: z
32
32
  .enum(["adapt", "clone"])
@@ -34,7 +34,7 @@ export function registerIngestTools(server) {
34
34
  .describe("How the caller intends to use the result. 'adapt' (default) — use as a layout reference and rewrite the TEXT for the user's brand (images from the reference are still re-hosted via upload_images and reused). 'clone' — keep text and images close to the original."),
35
35
  detail: detailParam,
36
36
  }, { title: "Ingest HTML Reference", readOnlyHint: true, openWorldHint: false }, async ({ html, intent, detail }) => text({ intent: intent ?? "adapt", ...parseHtml(html, detail ?? "compact") }));
37
- server.tool("ingest_url", "Fetches a public webpage (GET, 10s timeout, 2MB cap) and parses it into the same reference AST as ingest_html. Returns a warning when the page appears client-rendered (empty <body>) so the caller can fall back to a screenshot — Claude can analyze a screenshot natively without this tool. Does not execute JavaScript; sites built with React/Vue/Next.js may return little content. Use detail:'full' for clone-faithful rebuilds — adds CSS palette, background_images, per-section blocks, lists, and images as { src, alt } objects. Image URLs in the result are the user's assets — re-host them via upload_images and reuse them for BOTH intents; use search_images only for slots with no source image.", {
37
+ server.tool("ingest_url", "Fetches a public webpage (GET, 10s timeout, 2MB cap) and parses it into the same reference AST as ingest_html (including per-section size_hint desktop heights). Returns a warning when the page appears client-rendered (empty <body>) so the caller can fall back to a screenshot — Claude can analyze a screenshot natively without this tool. Does not execute JavaScript; sites built with React/Vue/Next.js may return little content. Use detail:'full' for clone-faithful rebuilds — adds CSS palette, background_images, per-section blocks, lists, images as { src, alt } objects, and widgets (source HTML + CSS of composite mockups for html-box rebuilds). Image URLs in the result are the user's assets — re-host them via upload_images and reuse them for BOTH intents; use search_images only for slots with no source image.", {
38
38
  url: z.string().describe("Public HTTP(S) URL of the page to fetch."),
39
39
  intent: z
40
40
  .enum(["adapt", "clone"])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-landing-mcp",
3
- "version": "1.0.60",
3
+ "version": "1.0.61",
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
  "mcpName": "io.github.vuluu2k/webcake-landing-mcp",
6
6
  "type": "module",