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 +11 -7
- package/README.vi.md +11 -7
- package/dist/changelog.json +14 -14
- package/dist/http.js +1 -0
- package/dist/persistence/config.js +40 -7
- package/dist/persistence/webcake-client.js +31 -6
- package/dist/smoke.js +16 -1
- package/dist/web-guide.js +151 -17
- package/package.json +1 -1
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
|
|
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
|
|
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 đè
|
|
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 |
|
|
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
|
package/dist/changelog.json
CHANGED
|
@@ -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
|
@@ -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
|
|
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 (
|
|
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:
|
|
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
|
|
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:
|
|
109
|
-
preview_url:
|
|
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:
|
|
208
|
-
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 <TOKEN>";
|
|
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:
|
|
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:
|
|
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);--
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
.
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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
|
-
<
|
|
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
|
|
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.
|
|
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": {
|