webcake-landing-mcp 1.0.21 → 1.0.23

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 CHANGED
@@ -221,7 +221,8 @@ lands in logs). Any header that's missing falls back to the matching env var:
221
221
  | `x-webcake-env` | `WEBCAKE_ENV` | named environment (`local`/`staging`/`prod`) |
222
222
  | `x-webcake-org-id` | `WEBCAKE_ORG_ID` | default org |
223
223
  | `x-webcake-api-base` | `WEBCAKE_API_BASE` | overrides the env preset's API base |
224
- | `x-webcake-app-base` | `WEBCAKE_APP_BASE` | overrides the env preset's app base |
224
+ | `x-webcake-app-base` | `WEBCAKE_APP_BASE` | overrides the env preset's SPA base (login connect page) |
225
+ | `x-webcake-builder-base` | `WEBCAKE_BUILDER_BASE` | overrides the builder host used for editor/preview links |
225
226
 
226
227
  > The reference + generation tools (`get_generation_guide`, `list_elements`, `validate_page`, …) need **no
227
228
  > token** — only the persistence tools (`create_page`, `update_page`, …) use it. Without a JWT, those return
@@ -290,7 +291,8 @@ flow can also be done entirely in the SPA, no backend route needed.)
290
291
  | `WEBCAKE_API_BASE` | No* | Backend base URL, e.g. `http://localhost:5800`. Required to persist (or set `WEBCAKE_ENV`). |
291
292
  | `WEBCAKE_JWT` | No* | Account JWT (dashboard auth). Required to persist — expires, refresh when needed. |
292
293
  | `WEBCAKE_ORG_ID` | No | Default organization id for `create_page` (overridden by its `organization_id` arg). Omit → personal page. |
293
- | `WEBCAKE_APP_BASE` | No | Optional base used to build editor/preview URLs in the result. |
294
+ | `WEBCAKE_APP_BASE` | No | Optional SPA base used for the browser `login` connect page. |
295
+ | `WEBCAKE_BUILDER_BASE` | No | Optional builder host for the editor/preview links in the result. Defaults to the env preset, else derived from the API host (`api.x`→`builder.x`). |
294
296
  | `WEBCAKE_CONFIG_DIR` | No | Dir for the saved `auth.json` written by `login` (default `~/.webcake-landing-mcp`). |
295
297
 
296
298
  > \* `WEBCAKE_API_BASE` and `WEBCAKE_JWT` are only needed for the persistence tools. The reference and
@@ -304,11 +306,13 @@ flow can also be done entirely in the SPA, no backend route needed.)
304
306
  Instead of setting both base URLs by hand, pick a named environment — one source of
305
307
  truth for the API + app bases:
306
308
 
307
- | `--env` / `WEBCAKE_ENV` | API base (`WEBCAKE_API_BASE`) | App base (`WEBCAKE_APP_BASE`) |
308
- |-------------------------|-------------------------------|-------------------------------|
309
- | `local` | `http://localhost:5800` | `http://localhost:5173` |
310
- | `staging` | `https://api.staging.webcake.io` | `https://staging.webcake.io` |
311
- | `prod` | `https://api.webcake.io` | `https://webcake.io` |
309
+ | `--env` / `WEBCAKE_ENV` | API base (`WEBCAKE_API_BASE`) | App base (`WEBCAKE_APP_BASE`) | Builder base (`WEBCAKE_BUILDER_BASE`) |
310
+ |-------------------------|-------------------------------|-------------------------------|----------------------------------------|
311
+ | `local` | `http://localhost:5800` | `http://localhost:5173` | `http://builder.localhost:5800` |
312
+ | `staging` | `https://api.staging.webcake.io` | `https://staging.webcake.io` | `https://builder.staging.webcake.io` |
313
+ | `prod` | `https://api.webcake.io` | `https://webcake.io` | `https://builder.webcake.io` |
314
+
315
+ > The **editor/preview link** returned after `create_page`/`update_page` opens on the **builder host** (above), not the API or SPA base.
312
316
 
313
317
  ```bash
314
318
  npx -y webcake-landing-mcp login --env local # connect against your local SPA + API
package/README.vi.md CHANGED
@@ -221,7 +221,8 @@ Header nào thiếu sẽ fallback về biến env tương ứng:
221
221
  | `x-webcake-env` | `WEBCAKE_ENV` | môi trường có tên (`local`/`staging`/`prod`) |
222
222
  | `x-webcake-org-id` | `WEBCAKE_ORG_ID` | org mặc định |
223
223
  | `x-webcake-api-base` | `WEBCAKE_API_BASE` | ghi đè API base của preset |
224
- | `x-webcake-app-base` | `WEBCAKE_APP_BASE` | ghi đè app base của preset |
224
+ | `x-webcake-app-base` | `WEBCAKE_APP_BASE` | ghi đè SPA base của preset (trang connect khi `login`) |
225
+ | `x-webcake-builder-base` | `WEBCAKE_BUILDER_BASE` | ghi đè host builder dùng cho link editor/preview |
225
226
 
226
227
  > Tool tham chiếu + generation (`get_generation_guide`, `list_elements`, `validate_page`, …) **không cần
227
228
  > token** — chỉ tool lưu trữ (`create_page`, `update_page`, …) mới dùng. Không có JWT thì các tool đó trả
@@ -285,7 +286,8 @@ SPA cũng được, khỏi cần route backend.)
285
286
  | `WEBCAKE_API_BASE` | Không* | Base URL backend, ví dụ `http://localhost:5800`. Cần để lưu trang (hoặc đặt `WEBCAKE_ENV`). |
286
287
  | `WEBCAKE_JWT` | Không* | JWT tài khoản (auth dashboard). Cần để lưu trang — sẽ hết hạn, làm mới khi cần. |
287
288
  | `WEBCAKE_ORG_ID` | Không | Organization mặc định cho `create_page` (bị ghi đè bởi tham số `organization_id`). Bỏ trống → trang cá nhân. |
288
- | `WEBCAKE_APP_BASE` | Không | Base tuỳ chọn để dựng URL editor/preview trong kết quả. |
289
+ | `WEBCAKE_APP_BASE` | Không | SPA base tuỳ chọn dùng cho trang connect khi `login` qua trình duyệt. |
290
+ | `WEBCAKE_BUILDER_BASE` | Không | Host builder tuỳ chọn cho link editor/preview trong kết quả. Mặc định lấy theo preset môi trường, nếu không thì suy ra từ host API (`api.x`→`builder.x`). |
289
291
  | `WEBCAKE_CONFIG_DIR` | Không | Thư mục chứa `auth.json` do `login` ghi (mặc định `~/.webcake-landing-mcp`). |
290
292
 
291
293
  > \* `WEBCAKE_API_BASE` và `WEBCAKE_JWT` chỉ cần cho các tool lưu trữ. Các tool tham chiếu và kiểm tra
@@ -299,11 +301,13 @@ SPA cũng được, khỏi cần route backend.)
299
301
  Thay vì đặt thủ công cả hai base URL, hãy chọn một môi trường có tên — một nguồn sự thật duy nhất
300
302
  cho API + app base (mặc định là `prod`):
301
303
 
302
- | `--env` / `WEBCAKE_ENV` | API base (`WEBCAKE_API_BASE`) | App base (`WEBCAKE_APP_BASE`) |
303
- |-------------------------|-------------------------------|-------------------------------|
304
- | `local` | `http://localhost:5800` | `http://localhost:5173` |
305
- | `staging` | `https://api.staging.webcake.io` | `https://staging.webcake.io` |
306
- | `prod` *(mặc định)* | `https://api.webcake.io` | `https://webcake.io` |
304
+ | `--env` / `WEBCAKE_ENV` | API base (`WEBCAKE_API_BASE`) | App base (`WEBCAKE_APP_BASE`) | Builder base (`WEBCAKE_BUILDER_BASE`) |
305
+ |-------------------------|-------------------------------|-------------------------------|----------------------------------------|
306
+ | `local` | `http://localhost:5800` | `http://localhost:5173` | `http://builder.localhost:5800` |
307
+ | `staging` | `https://api.staging.webcake.io` | `https://staging.webcake.io` | `https://builder.staging.webcake.io` |
308
+ | `prod` *(mặc định)* | `https://api.webcake.io` | `https://webcake.io` | `https://builder.webcake.io` |
309
+
310
+ > **Link editor/preview** trả về sau `create_page`/`update_page` mở trên **host builder** (bảng trên), không phải API hay SPA base.
307
311
 
308
312
  ```bash
309
313
  npx -y webcake-landing-mcp login --env local # đăng nhập vào SPA + API local
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.0.23",
4
+ "d": "07/06/2026",
5
+ "type": "Added",
6
+ "en": "The GET / guide page now includes a dark/light theme toggle button in the header; the preference is saved in localStorage and applied before the…",
7
+ "vi": "Trang hướng dẫn GET / nay có nút chuyển chế độ sáng/tối trong header; lựa chọn được lưu vào localStorage và áp dụng trước khi trang hiển thị để…"
8
+ },
9
+ {
10
+ "v": "1.0.22",
11
+ "d": "07/06/2026",
12
+ "type": "Added",
13
+ "en": "New WEBCAKE_BUILDER_BASE environment variable, x-webcake-builder-base HTTP header, and ?builder_base= query parameter set the page-builder host used…",
14
+ "vi": "Biến môi trường WEBCAKE_BUILDER_BASE mới, HTTP header x-webcake-builder-base, và tham số truy vấn ?builder_base= cho phép đặt host của page-builder…"
15
+ },
2
16
  {
3
17
  "v": "1.0.21",
4
18
  "d": "07/06/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Changed",
27
41
  "en": "new_element for list-product now seeds a default styles.colorBtn (rgba(246,4,87,1)) so generated product-list button labels have a visible accent…",
28
42
  "vi": "new_element cho list-product nay gán sẵn styles.colorBtn mặc định (rgba(246,4,87,1)) để nhãn nút của danh sách sản phẩm có màu nhấn nhìn thấy được…"
29
- },
30
- {
31
- "v": "1.0.17",
32
- "d": "07/06/2026",
33
- "type": "Fixed",
34
- "en": "get_element and get_generation_guide no longer suggest https://picsum.photos as an alternative image placeholder; agents are now directed to use…",
35
- "vi": "get_element và get_generation_guide không còn gợi ý https://picsum.photos làm ảnh placeholder thay thế; agent nay được hướng dẫn chỉ dùng…"
36
- },
37
- {
38
- "v": "1.0.16",
39
- "d": "06/06/2026",
40
- "type": "Changed",
41
- "en": "The server icon (served at /favicon.svg and embedded in serverInfo.icons) has been refined to the official Webcake brand mark: a green-gradient…",
42
- "vi": "Icon server (phục vụ tại /favicon.svg và nhúng trong serverInfo.icons) đã được tinh chỉnh về đúng logo thương hiệu Webcake: ô bo góc gradient xanh…"
43
43
  }
44
44
  ]
package/dist/http.js CHANGED
@@ -45,6 +45,7 @@ const QUERY_AUTH = {
45
45
  api_base: "x-webcake-api-base",
46
46
  org_id: "x-webcake-org-id",
47
47
  app_base: "x-webcake-app-base",
48
+ builder_base: "x-webcake-builder-base",
48
49
  };
49
50
  function applyQueryAuth(req) {
50
51
  const q = (req.url ?? "").indexOf("?");
@@ -18,7 +18,9 @@
18
18
  * WEBCAKE_API_BASE e.g. http://localhost:5800 (required to call the backend)
19
19
  * WEBCAKE_JWT the account JWT (required to call the backend)
20
20
  * WEBCAKE_ORG_ID optional default organization id for create_page
21
- * WEBCAKE_APP_BASE optional base for editor/preview URLs in the result
21
+ * WEBCAKE_APP_BASE optional SPA base (used for the login connect page)
22
+ * WEBCAKE_BUILDER_BASE optional builder host for editor/preview URLs in the result
23
+ * (defaults to the env preset, else derived from the API host)
22
24
  * WEBCAKE_CONFIG_DIR optional dir for the saved auth.json (default ~/.webcake-landing-mcp)
23
25
  */
24
26
  import { homedir } from "node:os";
@@ -29,12 +31,14 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
29
31
  * base URLs. Selecting one (via the `--env` flag, WEBCAKE_ENV, the `x-webcake-env`
30
32
  * header, or `?env=` in the URL) fills in both bases so callers don't repeat them.
31
33
  * Explicit WEBCAKE_API_BASE / WEBCAKE_APP_BASE (or per-request overrides) win over
32
- * the preset. `apiBase` is the backend; `appBase` is the SPA (editor/preview/connect).
34
+ * the preset. `apiBase` is the backend; `appBase` is the SPA (login connect page);
35
+ * `builderBase` is the page builder host that serves the `/editor/v2` URL returned
36
+ * after create/update (a distinct host — NOT the API and NOT the SPA).
33
37
  */
34
38
  export const ENVIRONMENTS = {
35
- local: { apiBase: "http://localhost:5800", appBase: "http://localhost:5173" },
36
- staging: { apiBase: "https://api.staging.webcake.io", appBase: "https://staging.webcake.io" },
37
- prod: { apiBase: "https://api.webcake.io", appBase: "https://webcake.io" },
39
+ local: { apiBase: "http://localhost:5800", appBase: "http://localhost:5173", builderBase: "http://builder.localhost:5800" },
40
+ staging: { apiBase: "https://api.staging.webcake.io", appBase: "https://staging.webcake.io", builderBase: "https://builder.staging.webcake.io" },
41
+ prod: { apiBase: "https://api.webcake.io", appBase: "https://webcake.io", builderBase: "https://builder.webcake.io" },
38
42
  };
39
43
  export const ENV_NAMES = Object.keys(ENVIRONMENTS);
40
44
  /** True when `v` names a known environment (local|staging|prod). */
@@ -45,6 +49,23 @@ export function isEnvName(v) {
45
49
  export function resolveEnv(name) {
46
50
  return isEnvName(name) ? ENVIRONMENTS[name] : undefined;
47
51
  }
52
+ /**
53
+ * Derive the page-builder host from the API base when no preset / explicit value is
54
+ * given: `api.<domain>` → `builder.<domain>`, otherwise `builder.<host>` (so
55
+ * `http://localhost:5800` → `http://builder.localhost:5800`, matching the presets).
56
+ */
57
+ export function deriveBuilderBase(apiBase) {
58
+ if (!apiBase)
59
+ return undefined;
60
+ try {
61
+ const u = new URL(apiBase);
62
+ u.hostname = u.hostname.startsWith("api.") ? `builder.${u.hostname.slice(4)}` : `builder.${u.hostname}`;
63
+ return u.origin;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
48
69
  export function readConfig(overrides = {}) {
49
70
  const saved = readSavedConfig();
50
71
  // A named environment supplies default base URLs; explicit values still win.
@@ -58,12 +79,22 @@ export function readConfig(overrides = {}) {
58
79
  missing.push("WEBCAKE_JWT");
59
80
  if (missing.length)
60
81
  return { config: null, missing };
82
+ const cleanBase = base.replace(/\/+$/, "");
83
+ // The editor/preview URL lives on the builder host (e.g. builder.localhost:5800),
84
+ // not the API base (5800) nor the SPA (5173). Resolve it explicitly so the link
85
+ // returned to the user opens in the page builder.
86
+ const builderBase = (overrides.builderBase ??
87
+ process.env.WEBCAKE_BUILDER_BASE ??
88
+ preset?.builderBase ??
89
+ saved.builderBase ??
90
+ deriveBuilderBase(cleanBase))?.replace(/\/+$/, "");
61
91
  return {
62
92
  config: {
63
- base: base.replace(/\/+$/, ""),
93
+ base: cleanBase,
64
94
  jwt: jwt,
65
95
  orgId: overrides.orgId ?? process.env.WEBCAKE_ORG_ID ?? saved.orgId,
66
96
  appBase: (overrides.appBase ?? process.env.WEBCAKE_APP_BASE ?? preset?.appBase ?? saved.appBase)?.replace(/\/+$/, ""),
97
+ builderBase,
67
98
  },
68
99
  missing: [],
69
100
  };
@@ -79,7 +110,8 @@ function header(headers, name) {
79
110
  * x-webcake-org-id organization id
80
111
  * x-webcake-env named environment (local|staging|prod) for the base URLs
81
112
  * x-webcake-api-base backend base URL (overrides the env preset)
82
- * x-webcake-app-base editor/preview URL base (overrides the env preset)
113
+ * x-webcake-app-base SPA base used for the login connect page (overrides the preset)
114
+ * x-webcake-builder-base builder host for editor/preview URLs (overrides the preset)
83
115
  * Any header that is absent falls back to the corresponding env var in readConfig.
84
116
  */
85
117
  export function configFromHeaders(headers) {
@@ -90,6 +122,7 @@ export function configFromHeaders(headers) {
90
122
  jwt: header(headers, "x-webcake-jwt") ?? bearer,
91
123
  orgId: header(headers, "x-webcake-org-id"),
92
124
  appBase: header(headers, "x-webcake-app-base"),
125
+ builderBase: header(headers, "x-webcake-builder-base"),
93
126
  env: header(headers, "x-webcake-env"),
94
127
  };
95
128
  }
@@ -15,6 +15,33 @@ function authHeaders(config, orgId) {
15
15
  headers["x-org-id"] = `${org}`;
16
16
  return headers;
17
17
  }
18
+ /**
19
+ * Resolve the editor/preview link the backend returns onto the page-builder host
20
+ * (config.builderBase, e.g. builder.localhost:5800), NOT the API base. The backend
21
+ * may return either a path (`/editor/v2/<id>`) or an absolute URL on its own host
22
+ * (`http://localhost:5800/editor/v2/<id>`) — in both cases we keep only the
23
+ * path+query and re-root it on the builder host.
24
+ */
25
+ export function toEditorUrl(config, raw) {
26
+ if (!raw)
27
+ return raw;
28
+ const builder = config.builderBase ?? config.appBase;
29
+ if (!builder)
30
+ return raw;
31
+ let pathQuery = raw;
32
+ if (/^https?:\/\//i.test(raw)) {
33
+ try {
34
+ const u = new URL(raw);
35
+ pathQuery = u.pathname + u.search + u.hash;
36
+ }
37
+ catch {
38
+ /* not a parseable URL — use as-is */
39
+ }
40
+ }
41
+ if (!pathQuery.startsWith("/"))
42
+ pathQuery = `/${pathQuery}`;
43
+ return `${builder.replace(/\/+$/, "")}${pathQuery}`;
44
+ }
18
45
  /** Build (but do not send) the create request — used for dry-run previews. */
19
46
  export function buildRequest(config, name, source, orgId) {
20
47
  return {
@@ -88,7 +115,6 @@ export async function createPage(config, name, source, orgId) {
88
115
  const pageId = data?.page_id;
89
116
  const editorPath = data?.editor_url;
90
117
  const previewPath = data?.preview_url;
91
- const app = config.appBase;
92
118
  if (!res.ok || !pageId) {
93
119
  // The backend's failure envelope is { success:false, message } on 422; auth
94
120
  // plugs return plain-text 401/403 (json is null). Surface the real reason
@@ -105,8 +131,8 @@ export async function createPage(config, name, source, orgId) {
105
131
  ok: true,
106
132
  status: res.status,
107
133
  page_id: pageId,
108
- editor_url: app && editorPath ? `${app}${editorPath}` : editorPath,
109
- preview_url: app && previewPath ? `${app}${previewPath}` : previewPath,
134
+ editor_url: toEditorUrl(config, editorPath),
135
+ preview_url: toEditorUrl(config, previewPath),
110
136
  organization_id: (orgId ?? config.orgId) ?? null,
111
137
  raw: data,
112
138
  };
@@ -190,7 +216,6 @@ export async function updatePageSource(config, pageId, source) {
190
216
  }
191
217
  const data = json?.data ?? json;
192
218
  const pageIdOut = data?.page_id;
193
- const app = config.appBase;
194
219
  if (!res.ok || !pageIdOut) {
195
220
  const backendMsg = json?.message ?? json?.reason ?? (json ? undefined : text.slice(0, 200));
196
221
  return {
@@ -204,8 +229,8 @@ export async function updatePageSource(config, pageId, source) {
204
229
  ok: true,
205
230
  status: res.status,
206
231
  page_id: pageIdOut,
207
- editor_url: app && data?.editor_url ? `${app}${data.editor_url}` : data?.editor_url,
208
- preview_url: app && data?.preview_url ? `${app}${data.preview_url}` : data?.preview_url,
232
+ editor_url: toEditorUrl(config, data?.editor_url),
233
+ preview_url: toEditorUrl(config, data?.preview_url),
209
234
  organization_id: data?.organization_id ?? null,
210
235
  raw: data,
211
236
  };
package/dist/smoke.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { createElement, CONTAINER_TYPES, FIELD_TYPES, LIBRARY, ELEMENT_TYPES, ELEMENTS, } from "./domains/landing/elements/index.js";
6
6
  import { validatePage, pageSchema } from "./domains/landing/validate.js";
7
7
  import { readConfig, resolveEnv, ENV_NAMES } from "./persistence/config.js";
8
+ import { toEditorUrl } from "./persistence/webcake-client.js";
8
9
  let failures = 0;
9
10
  const check = (name, cond, extra) => {
10
11
  if (cond) {
@@ -212,7 +213,7 @@ check("clean form has no binding warnings", rbg.warnings.length === 0, rbg.warni
212
213
  console.log("== config: named environment presets (local/staging/prod) ==");
213
214
  {
214
215
  // Deterministic: isolate from any ambient WEBCAKE_* and the saved auth.json on the dev box.
215
- for (const k of ["WEBCAKE_API_BASE", "WEBCAKE_APP_BASE", "WEBCAKE_ENV", "WEBCAKE_JWT", "WEBCAKE_ORG_ID"])
216
+ for (const k of ["WEBCAKE_API_BASE", "WEBCAKE_APP_BASE", "WEBCAKE_BUILDER_BASE", "WEBCAKE_ENV", "WEBCAKE_JWT", "WEBCAKE_ORG_ID"])
216
217
  delete process.env[k];
217
218
  process.env.WEBCAKE_CONFIG_DIR = "/nonexistent/webcake-smoke";
218
219
  check("env names are local/staging/prod", setEq(new Set(ENV_NAMES), ["local", "staging", "prod"]), ENV_NAMES);
@@ -224,6 +225,20 @@ console.log("== config: named environment presets (local/staging/prod) ==");
224
225
  check("readConfig(env=local) fills api+app base", local?.base === "http://localhost:5800" && local?.appBase === "http://localhost:5173", local);
225
226
  check("explicit base overrides the preset", readConfig({ env: "prod", base: "http://x:1", jwt: "t" }).config?.base === "http://x:1");
226
227
  check("unknown env leaves base missing", readConfig({ env: "bogus", jwt: "t" }).missing.includes("WEBCAKE_API_BASE"));
228
+ // builder host for editor/preview URLs (distinct from the api + app bases)
229
+ check("env presets carry builder bases", resolveEnv("prod")?.builderBase === "https://builder.webcake.io" && resolveEnv("local")?.builderBase === "http://builder.localhost:5800");
230
+ check("readConfig(env=prod) sets builderBase", prod?.builderBase === "https://builder.webcake.io", prod);
231
+ check("readConfig(env=local) sets builderBase", local?.builderBase === "http://builder.localhost:5800", local);
232
+ check("readConfig(env=staging) sets builderBase", readConfig({ env: "staging", jwt: "t" }).config?.builderBase === "https://builder.staging.webcake.io");
233
+ check("builderBase derives from a custom api base (api. → builder.)", readConfig({ base: "https://api.example.com", jwt: "t" }).config?.builderBase === "https://builder.example.com");
234
+ check("builderBase derives from a localhost api base", readConfig({ base: "http://localhost:5800", jwt: "t" }).config?.builderBase === "http://builder.localhost:5800");
235
+ check("explicit builderBase overrides the preset", readConfig({ env: "prod", builderBase: "https://b.test", jwt: "t" }).config?.builderBase === "https://b.test");
236
+ // editor/preview link is re-rooted on the builder host, whether the backend
237
+ // returns a path or an absolute URL on its own host.
238
+ const localCfg = readConfig({ env: "local", jwt: "t" }).config;
239
+ check("editor url from a path → builder host", toEditorUrl(localCfg, "/editor/v2/abc") === "http://builder.localhost:5800/editor/v2/abc");
240
+ check("editor url from an absolute api url → builder host", toEditorUrl(localCfg, "http://localhost:5800/editor/v2/abc?x=1") === "http://builder.localhost:5800/editor/v2/abc?x=1");
241
+ check("editor url passthrough when empty", toEditorUrl(localCfg, undefined) === undefined);
227
242
  }
228
243
  console.log(`\n${failures === 0 ? "ALL GOOD" : failures + " FAILURE(S)"}`);
229
244
  process.exit(failures === 0 ? 0 : 1);
package/dist/web-guide.js CHANGED
@@ -24,6 +24,9 @@
24
24
  import { readFileSync } from "node:fs";
25
25
  import { ICON_SVG } from "./branding.js";
26
26
  const MCP_REMOTE_URL = "https://webcake.io/mcp-remote";
27
+ // The "configure every IDE" one-liner — rendered as a code block (with a copy
28
+ // button) rather than long inline code, which wrapped messily on mobile.
29
+ const INSTALL_ALL_CMD = "npx -y webcake-landing-mcp install --ide all --env prod --jwt &lt;TOKEN&gt;";
27
30
  const GITHUB_URL = "https://github.com/vuluu2k/webcake-landing-mcp";
28
31
  const NPM_URL = "https://www.npmjs.com/package/webcake-landing-mcp";
29
32
  const DOCS_URL = `${GITHUB_URL}#readme`;
@@ -58,6 +61,11 @@ const ICONS = {
58
61
  arrow: '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
59
62
  clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
60
63
  globe: '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>',
64
+ bulb: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
65
+ server: '<rect width="20" height="8" x="2" y="2" rx="2"/><rect width="20" height="8" x="2" y="14" rx="2"/><path d="M6 6h.01"/><path d="M6 18h.01"/>',
66
+ window: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 9h20"/><path d="M6 6.5h.01"/><path d="M9 6.5h.01"/>',
67
+ moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
68
+ sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
61
69
  };
62
70
  function icon(name) {
63
71
  return `<svg class="i" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[name] ?? ""}</svg>`;
@@ -136,6 +144,14 @@ const T = {
136
144
  leadPost: " của bạn. Không kéo thả, không cài server — kết nối một lần là xong.",
137
145
  ctaStart: "Bắt đầu kết nối",
138
146
  ctaStar: "Star trên GitHub",
147
+ flowH2: "Mô hình hoạt động",
148
+ flow: [
149
+ { icon: "bulb", t: "Bạn", s: "ý tưởng" },
150
+ { icon: "brain", t: "Trợ lý AI", s: "Claude · Cursor" },
151
+ { icon: "server", t: "MCP", s: "webcake-landing" },
152
+ { icon: "window", t: "WebCake", s: "trang thật" },
153
+ ],
154
+ flowCap: "Bạn mô tả bằng lời → AI học mô hình thật từ MCP → MCP dựng JSON + kiểm tra → lưu thành trang thật trên WebCake. Bạn nhận link, mở editor, publish.",
139
155
  howH2: "Cách hoạt động",
140
156
  how: [
141
157
  {
@@ -172,7 +188,7 @@ const T = {
172
188
  '<b>Làm theo hỏi đáp:</b> chọn môi trường (<code class="inl">prod</code>) → đăng nhập Webcake qua trình duyệt (hoặc dán JWT) → chọn IDE (Claude Desktop / Cursor / Claude Code…).',
173
189
  '<b>Khởi động lại IDE</b> → thấy <code class="inl">webcake-landing</code> trong danh sách MCP là xong.',
174
190
  ],
175
- m1Note: 'Muốn cấu hình mọi IDE một phát: <code class="inl">npx -y webcake-landing-mcp install --ide all --env prod --jwt &lt;TOKEN&gt;</code>',
191
+ m1Note: "Muốn cấu hình mọi IDE một phát:",
176
192
  m2Tag: "Cách ② · URL remote — không cần cài gì",
177
193
  m2Sub: "Hợp khi máy không có Node.js, dùng theo nhóm, hoặc dùng claude.ai trên web.",
178
194
  m2Steps: [
@@ -206,6 +222,14 @@ const T = {
206
222
  leadPost: ". No drag-and-drop, no server to host — connect once and you're set.",
207
223
  ctaStart: "Get connected",
208
224
  ctaStar: "Star on GitHub",
225
+ flowH2: "How it flows",
226
+ flow: [
227
+ { icon: "bulb", t: "You", s: "your idea" },
228
+ { icon: "brain", t: "AI assistant", s: "Claude · Cursor" },
229
+ { icon: "server", t: "MCP", s: "webcake-landing" },
230
+ { icon: "window", t: "WebCake", s: "a real page" },
231
+ ],
232
+ flowCap: "You describe it in words → the AI learns the real model from the MCP → the MCP builds the JSON + validates → it's saved as a real WebCake page. You get a link, open the editor, publish.",
209
233
  howH2: "How it works",
210
234
  how: [
211
235
  {
@@ -242,7 +266,7 @@ const T = {
242
266
  '<b>Follow the prompts:</b> pick an environment (<code class="inl">prod</code>) → sign in to Webcake in the browser (or paste a JWT) → pick your IDE (Claude Desktop / Cursor / Claude Code…).',
243
267
  '<b>Restart your IDE</b> → see <code class="inl">webcake-landing</code> in the MCP list and you\'re done.',
244
268
  ],
245
- m1Note: 'Configure every IDE at once: <code class="inl">npx -y webcake-landing-mcp install --ide all --env prod --jwt &lt;TOKEN&gt;</code>',
269
+ m1Note: "Configure every IDE at once:",
246
270
  m2Tag: "Way ② · Remote URL — nothing to install",
247
271
  m2Sub: "Best when you have no Node.js, work in a team, or use claude.ai on the web.",
248
272
  m2Steps: [
@@ -354,6 +378,7 @@ export function guideHtml(origin, lang = "vi") {
354
378
  <html lang="${L}"><head>
355
379
  <meta charset="utf-8">
356
380
  <meta name="viewport" content="width=device-width,initial-scale=1">
381
+ <script>(function(){try{var t=localStorage.getItem('wc-theme');if(t==='dark'||t==='light')document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
357
382
  <title>${m.title}</title>
358
383
  <meta name="description" content="${m.desc}">
359
384
  <meta name="keywords" content="${m.keywords}">
@@ -381,10 +406,15 @@ export function guideHtml(origin, lang = "vi") {
381
406
  <meta name="twitter:image" content="${ogImage}">
382
407
  <script type="application/ld+json">${jsonLdScript}</script>
383
408
  <style>
409
+ /* Light defaults. Dark applies via OS preference OR a forced [data-theme="dark"]
410
+ (the toggle); [data-theme="light"] forces light even on a dark OS. */
384
411
  :root{--g:#1DB954;--g7:#178f43;--ink:#11231b;--mut:#5e6d65;--bg:#f5f9f7;--card:#ffffff;
385
- --line:rgba(16,40,30,.09);--glass-hi:transparent;--shadow:0 1px 2px rgba(16,40,30,.05),0 6px 20px -12px rgba(16,40,30,.18);--code:#0e1714}
386
- @media(prefers-color-scheme:dark){:root{--ink:#e8f0ec;--mut:#9aaba2;--bg:#0b110e;--card:#141b17;
387
- --line:rgba(255,255,255,.07);--glass-hi:transparent;--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px -14px rgba(0,0,0,.7);--code:#070f0b;--g7:#5ee08a}}
412
+ --line:rgba(16,40,30,.09);--shadow:0 1px 2px rgba(16,40,30,.05),0 6px 20px -12px rgba(16,40,30,.18);--code:#0e1714;
413
+ --ic-fg:#178f43;--btn-hover:#178f43}
414
+ @media(prefers-color-scheme:dark){:root:not([data-theme="light"]){--ink:#e8f0ec;--mut:#9aaba2;--bg:#0b110e;--card:#141b17;
415
+ --line:rgba(255,255,255,.07);--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px -14px rgba(0,0,0,.7);--code:#070f0b;--g7:#5ee08a;--ic-fg:#6fe79a;--btn-hover:#21c264}}
416
+ :root[data-theme="dark"]{--ink:#e8f0ec;--mut:#9aaba2;--bg:#0b110e;--card:#141b17;
417
+ --line:rgba(255,255,255,.07);--shadow:0 1px 2px rgba(0,0,0,.3),0 8px 24px -14px rgba(0,0,0,.7);--code:#070f0b;--g7:#5ee08a;--ic-fg:#6fe79a;--btn-hover:#21c264}
388
418
  *{box-sizing:border-box}
389
419
  html{scroll-behavior:smooth}
390
420
  body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:var(--ink);
@@ -405,9 +435,15 @@ export function guideHtml(origin, lang = "vi") {
405
435
  header .logo{width:50px;height:50px;border-radius:14px;overflow:hidden;flex:0 0 auto;
406
436
  box-shadow:0 6px 16px -4px rgba(29,185,84,.4)}
407
437
  header .logo svg{width:100%;height:100%;display:block}
408
- .langsw{margin-left:auto;align-self:flex-start;font-size:.82rem;font-weight:700;color:var(--g7);text-decoration:none;
438
+ .hgrow{flex:1 1 auto;min-width:0}
439
+ .controls{margin-left:auto;flex:0 0 auto;display:flex;align-items:center;gap:8px}
440
+ .langsw{font-size:.82rem;font-weight:700;color:var(--g7);text-decoration:none;white-space:nowrap;
409
441
  border:1px solid var(--line);background:var(--card);padding:7px 12px;border-radius:999px;display:inline-flex;align-items:center;gap:6px}
410
442
  .langsw:hover{border-color:var(--g)}
443
+ .iconbtn{width:36px;height:36px;flex:0 0 auto;display:grid;place-items:center;cursor:pointer;color:var(--g7);
444
+ border:1px solid var(--line);background:var(--card);border-radius:10px;transition:border-color .15s ease,color .15s ease}
445
+ .iconbtn:hover{border-color:var(--g)}
446
+ .iconbtn svg{width:17px;height:17px}
411
447
  h1{font-size:1.78rem;margin:0;font-weight:800;letter-spacing:-.02em}
412
448
  .sub{color:var(--mut);margin:3px 0 0;font-size:.98rem}
413
449
  .lead{font-size:1.16rem;margin:20px 0 18px;max-width:60ch}
@@ -420,9 +456,8 @@ export function guideHtml(origin, lang = "vi") {
420
456
  .dot{width:8px;height:8px;border-radius:50%;background:var(--g);box-shadow:0 0 0 0 rgba(29,185,84,.5);animation:pulse 2s infinite}
421
457
  @keyframes pulse{70%{box-shadow:0 0 0 7px rgba(29,185,84,0)}100%{box-shadow:0 0 0 0 rgba(29,185,84,0)}}
422
458
  h2{font-size:1.32rem;margin:46px 0 16px;font-weight:800;letter-spacing:-.01em}
423
- .ic{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;flex:0 0 auto;color:var(--g7);
459
+ .ic{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;flex:0 0 auto;color:var(--ic-fg);
424
460
  background:rgba(29,185,84,.11);border:1px solid var(--line);transition:transform .2s ease}
425
- @media(prefers-color-scheme:dark){.ic{color:#6fe79a}}
426
461
  .ic .i{width:22px;height:22px}
427
462
  .grid{display:grid;gap:16px;grid-template-columns:1fr 1fr 1fr}
428
463
  @media(max-width:720px){.grid{grid-template-columns:1fr}}
@@ -431,21 +466,50 @@ export function guideHtml(origin, lang = "vi") {
431
466
  .card h3{margin:0 0 6px;font-size:1.04rem}
432
467
  .card p{color:var(--mut);font-size:.93rem;margin:0}
433
468
  .tag{display:inline-flex;align-items:center;gap:9px;font-size:.82rem;font-weight:800;color:var(--g7);
434
- text-transform:uppercase;letter-spacing:.04em}
469
+ text-transform:uppercase;letter-spacing:.04em;flex-wrap:wrap}
435
470
  .tag .ic{width:30px;height:30px;border-radius:9px}
436
471
  .tag .ic .i{width:16px;height:16px}
437
472
  pre{margin:0;background:var(--code);color:#e8f0ec;border-radius:11px;padding:12px 14px;overflow-x:auto;
438
473
  border:1px solid rgba(255,255,255,.06);font:600 .82rem/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}
474
+ /* Copy button injected onto every <pre> by the inline script */
475
+ .codewrap{position:relative}
476
+ .codewrap pre{padding-right:46px}
477
+ .copy{position:absolute;top:8px;right:8px;width:30px;height:30px;display:grid;place-items:center;cursor:pointer;
478
+ border:1px solid rgba(255,255,255,.15);border-radius:8px;background:rgba(255,255,255,.06);color:#cfe9d8;
479
+ transition:background .15s ease,color .15s ease,border-color .15s ease}
480
+ .copy:hover{background:rgba(255,255,255,.13);color:#fff}
481
+ .copy svg{width:15px;height:15px}
482
+ .copy.done{color:#5ee08a;border-color:rgba(94,224,138,.55)}
439
483
  .feat{list-style:none;padding:0;margin:0;display:grid;gap:12px}
440
484
  .feat li{display:flex;gap:13px;align-items:center;font-size:.97rem;padding:13px 16px}
441
485
  .feat li b{color:var(--ink)}
442
486
  .cta-row{display:flex;gap:12px;flex-wrap:wrap;margin:22px 0 6px}
487
+ /* Flow diagram: nodes connected by wires with a traveling "packet" */
488
+ .flow{display:flex;align-items:flex-start;gap:0;padding:24px 18px 18px;overflow-x:auto}
489
+ .flow .node{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;width:104px}
490
+ .flow .node .ic{width:54px;height:54px;border-radius:16px}
491
+ .flow .node .ic .i{width:27px;height:27px}
492
+ .flow .node b{font-size:.93rem}
493
+ .flow .node span{font-size:.75rem;color:var(--mut)}
494
+ .flow .wire{flex:1 1 auto;min-width:30px;position:relative;height:2px;margin-top:27px;
495
+ background:linear-gradient(90deg,var(--line),rgba(29,185,84,.45),var(--line))}
496
+ .flow .wire .pkt{position:absolute;top:50%;left:0;width:9px;height:9px;margin:-5px 0 0 -4px;border-radius:50%;
497
+ background:var(--g);box-shadow:0 0 9px 1px rgba(29,185,84,.7)}
498
+ .flow .wire::after{content:"";position:absolute;right:-1px;top:50%;width:7px;height:7px;margin-top:-4px;
499
+ border-top:2px solid var(--g7);border-right:2px solid var(--g7);transform:rotate(45deg)}
500
+ .flow-cap{color:var(--mut);font-size:.9rem;margin:2px 2px 0;max-width:68ch}
501
+ @media(prefers-reduced-motion:no-preference){
502
+ .flow .wire .pkt{animation:pkt 2.4s ease-in-out infinite}
503
+ @keyframes pkt{0%{left:0;opacity:0}12%{opacity:1}88%{opacity:1}100%{left:100%;opacity:0}}
504
+ .flow .node .ic{animation:nodepop 2.4s ease-in-out infinite}
505
+ }
506
+ @media(prefers-reduced-motion:reduce){.flow .wire .pkt{display:none}}
507
+ @keyframes nodepop{0%,100%{box-shadow:none}50%{box-shadow:0 0 0 4px rgba(29,185,84,.12)}}
443
508
  .btn{display:inline-flex;align-items:center;gap:9px;padding:11px 19px;border-radius:11px;cursor:pointer;
444
509
  background:var(--g);color:#fff;text-decoration:none;font-weight:700;font-size:.93rem;
445
510
  box-shadow:0 4px 12px -4px rgba(29,185,84,.5);transition:transform .15s ease,background .15s ease}
446
511
  .btn .i{width:18px;height:18px}
447
- .btn:hover{transform:translateY(-1px);background:var(--g7)}
448
- @media(prefers-color-scheme:dark){.btn:hover{background:#21c264}}
512
+ .btn:hover{transform:translateY(-1px);background:var(--btn-hover)}
449
513
  .btn.ghost{background:var(--card);color:var(--ink);border:1px solid var(--line);box-shadow:none}
450
514
  .btn.ghost:hover{border-color:var(--g);background:var(--card)}
451
515
  .uses{display:grid;gap:14px;grid-template-columns:1fr 1fr;padding:0;margin:0;list-style:none}
@@ -462,15 +526,16 @@ export function guideHtml(origin, lang = "vi") {
462
526
  .msub{color:var(--mut);font-size:.92rem;margin:.5rem 0 1.2rem}
463
527
  .steps{list-style:none;margin:0;padding:0;display:grid;gap:16px}
464
528
  .steps li{display:flex;gap:14px}
465
- .steps .n{flex:0 0 auto;width:28px;height:28px;border-radius:50%;color:var(--g7);
529
+ .steps .n{flex:0 0 auto;width:28px;height:28px;border-radius:50%;color:var(--ic-fg);
466
530
  background:rgba(29,185,84,.12);border:1px solid var(--line);
467
531
  font:800 .85rem/1 system-ui;display:flex;align-items:center;justify-content:center}
468
- @media(prefers-color-scheme:dark){.steps .n{color:#6fe79a}}
469
532
  .steps .body{flex:1;min-width:0;font-size:.95rem}
470
533
  .steps .body pre{margin-top:9px}
471
534
  .steps .body .btn{margin-top:10px}
472
- code.inl{background:rgba(29,185,84,.13);color:var(--g7);padding:1px 6px;border-radius:6px;font-size:.85em;font-weight:600}
535
+ code.inl{background:rgba(29,185,84,.13);color:var(--g7);padding:1px 6px;border-radius:6px;font-size:.85em;font-weight:600;
536
+ overflow-wrap:anywhere;word-break:break-word}
473
537
  .note{font-size:.86rem;color:var(--mut);margin-top:10px}
538
+ .note + pre,.note + .codewrap{margin-top:9px}
474
539
  details{padding:2px 18px;margin-bottom:11px}
475
540
  details summary{cursor:pointer;font-weight:600;padding:15px 0;list-style:none;display:flex;align-items:center;gap:10px}
476
541
  details summary::-webkit-details-marker{display:none}
@@ -509,6 +574,31 @@ export function guideHtml(origin, lang = "vi") {
509
574
  @keyframes blink{50%{opacity:.55}}
510
575
  .cl-more{display:inline-flex;align-items:center;gap:6px;margin-top:6px;font-size:.86rem;font-weight:600;color:var(--g7);text-decoration:none}
511
576
  .cl-more:hover{gap:9px}
577
+ @media(max-width:640px){
578
+ .wrap{padding:30px 15px 56px}
579
+ /* Header: logo + controls on the top row, title drops to its own full line */
580
+ header{flex-wrap:wrap;gap:12px}
581
+ .hgrow{order:2;flex:1 1 100%}
582
+ h1{font-size:1.4rem}
583
+ h2{font-size:1.2rem;margin:34px 0 14px}
584
+ .lead{font-size:1.05rem}
585
+ .method{padding:18px 15px}
586
+ .card{padding:18px}
587
+ .cl-wrap{padding:18px 16px 10px}
588
+ .langsw{padding:6px 10px}
589
+ .uses li,.feat li{padding:14px}
590
+ /* Flow diagram: stack vertically (the horizontal row overflows narrow screens) */
591
+ .flow{flex-direction:column;align-items:stretch;overflow:visible;padding:16px}
592
+ .flow .node{flex-direction:row;width:auto;align-items:center;gap:13px;text-align:left}
593
+ .flow .node .ic{width:44px;height:44px;border-radius:13px}
594
+ .flow .node .ic .i{width:22px;height:22px}
595
+ .flow .node b{font-size:.95rem}
596
+ .flow .node span{font-size:.8rem}
597
+ .flow .wire{flex:0 0 auto;width:2px;height:20px;min-width:0;margin:3px 0 3px 21px;
598
+ background:linear-gradient(var(--line),var(--g))}
599
+ .flow .wire::after{content:none}
600
+ .flow .wire .pkt{display:none}
601
+ }
512
602
  @media(prefers-reduced-motion:no-preference){
513
603
  @supports (animation-timeline:view()){
514
604
  .reveal{animation:rise linear both;animation-timeline:view();animation-range:entry 0% entry 32%}
@@ -524,11 +614,14 @@ export function guideHtml(origin, lang = "vi") {
524
614
 
525
615
  <header class="hero-in">
526
616
  <span class="logo">${ICON_SVG}</span>
527
- <div>
617
+ <div class="hgrow">
528
618
  <h1>Webcake Landing MCP</h1>
529
619
  <p class="sub">${t.sub}</p>
530
620
  </div>
531
- <a class="langsw" href="${otherHref}" hreflang="${otherLang}" rel="alternate">${icon("globe")} ${t.switchLabel}</a>
621
+ <div class="controls">
622
+ <button class="iconbtn" id="theme" type="button" aria-label="Toggle theme" title="Theme">${icon("moon")}</button>
623
+ <a class="langsw" href="${otherHref}" hreflang="${otherLang}" rel="alternate">${icon("globe")} ${t.switchLabel}</a>
624
+ </div>
532
625
  </header>
533
626
 
534
627
  <p class="hero-in" style="display:flex;gap:9px;flex-wrap:wrap"><span class="pill"><span class="dot"></span> ${t.running}</span>${CHANGELOG[0] ? `<span class="pill">v${CHANGELOG[0].v}</span>` : ""}</p>
@@ -540,6 +633,17 @@ export function guideHtml(origin, lang = "vi") {
540
633
  <a class="btn ghost" href="${GITHUB_URL}">${icon("star")} ${t.ctaStar}</a>
541
634
  </div>
542
635
 
636
+ <h2 class="reveal">${t.flowH2}</h2>
637
+ <div class="glass flow reveal">
638
+ ${t.flow
639
+ .map((n, i) => `<div class="node"><span class="ic" style="animation-delay:${(i * 0.8).toFixed(1)}s">${icon(n.icon)}</span><b>${n.t}</b><span>${n.s}</span></div>` +
640
+ (i < t.flow.length - 1
641
+ ? `<div class="wire"><i class="pkt" style="animation-delay:${(i * 0.8).toFixed(1)}s"></i></div>`
642
+ : ""))
643
+ .join("\n ")}
644
+ </div>
645
+ <p class="flow-cap reveal">${t.flowCap}</p>
646
+
543
647
  <h2 class="reveal">${t.howH2}</h2>
544
648
  <div class="grid">
545
649
  ${t.how
@@ -563,6 +667,7 @@ export function guideHtml(origin, lang = "vi") {
563
667
  ${steps(t.m1Steps.map(fill))}
564
668
  </ol>
565
669
  <p class="note">${t.m1Note}</p>
670
+ <pre>${INSTALL_ALL_CMD}</pre>
566
671
  </div>
567
672
 
568
673
  <div class="glass card method reveal">
@@ -608,7 +713,36 @@ export function guideHtml(origin, lang = "vi") {
608
713
  <a href="${selfPath === "/" ? "/health" : "/health"}">Health</a>
609
714
  </footer>
610
715
 
611
- </div></body></html>`;
716
+ </div>
717
+ <script>
718
+ (function(){
719
+ var COPY='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
720
+ var DONE='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>';
721
+ function copyText(t){
722
+ if(navigator.clipboard&&navigator.clipboard.writeText){return navigator.clipboard.writeText(t);}
723
+ return new Promise(function(res,rej){try{var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);res();}catch(e){rej(e);}});
724
+ }
725
+ document.querySelectorAll('pre').forEach(function(pre){
726
+ var w=document.createElement('div');w.className='codewrap';
727
+ pre.parentNode.insertBefore(w,pre);w.appendChild(pre);
728
+ var b=document.createElement('button');b.type='button';b.className='copy';b.title='Copy';b.setAttribute('aria-label','Copy');b.innerHTML=COPY;
729
+ b.addEventListener('click',function(){
730
+ copyText(pre.innerText).then(function(){b.classList.add('done');b.innerHTML=DONE;setTimeout(function(){b.classList.remove('done');b.innerHTML=COPY;},1400);}).catch(function(){});
731
+ });
732
+ w.appendChild(b);
733
+ });
734
+
735
+ // Dark / light toggle — overrides the OS preference and persists the choice.
736
+ var SUN='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>';
737
+ var MOON='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>';
738
+ var html=document.documentElement, tBtn=document.getElementById('theme');
739
+ function effective(){return html.getAttribute('data-theme')||(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');}
740
+ function paint(){if(tBtn)tBtn.innerHTML=effective()==='dark'?SUN:MOON;}
741
+ paint();
742
+ if(tBtn)tBtn.addEventListener('click',function(){var next=effective()==='dark'?'light':'dark';html.setAttribute('data-theme',next);try{localStorage.setItem('wc-theme',next);}catch(e){}paint();});
743
+ })();
744
+ </script>
745
+ </body></html>`;
612
746
  }
613
747
  /**
614
748
  * The social-card image served at `/og.svg` and referenced by the page's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-landing-mcp",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
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": {