webcake-landing-mcp 1.0.3 → 1.0.5
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 +47 -1
- package/README.vi.md +608 -0
- package/dist/factory.js +4 -34
- package/dist/index.js +5 -2
- package/dist/library.js +68 -23
- package/dist/smoke.js +63 -1
- package/dist/validate.js +89 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
|
-
# WebCake Landing MCP
|
|
1
|
+
# 🍰 WebCake Landing MCP
|
|
2
|
+
|
|
3
|
+
**English** · [Tiếng Việt](./README.vi.md)
|
|
4
|
+
|
|
5
|
+
> **Describe a landing page in plain words — your AI builds it, checks it, and ships it straight to WebCake.**
|
|
6
|
+
|
|
7
|
+
> *"Build a landing page for my coffee shop — a hero with a sign-up button, a 3-feature section, and a lead form. Save it to my workspace."*
|
|
8
|
+
|
|
9
|
+
…and a real, **editable** WebCake page appears in your account. No dragging boxes, no learning the schema, no hand-writing JSON.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🧩 How it works
|
|
14
|
+
|
|
15
|
+
This server is the **bridge** between your AI assistant and WebCake. The AI never *guesses* what a WebCake
|
|
16
|
+
page looks like — it asks this MCP, which knows the entire element model, validates the result, and saves it for you.
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
You AI assistant webcake-landing MCP WebCake
|
|
20
|
+
┌──────┐ prompt ┌────────────┐ tools ┌──────────────────────┐ API ┌──────────┐
|
|
21
|
+
│ idea │ ───────► │ Claude / │ ──────► │ • knows the element │ ────► │ a real │
|
|
22
|
+
│ │ │ Cursor / │ │ model + AI hints │ │ editable │
|
|
23
|
+
│ │ ◄─────── │ Windsurf │ ◄────── │ • builds + validates │ ◄──── │ page in │
|
|
24
|
+
└──────┘ page URL └────────────┘ result │ • saves to your acct │ │ WebCake │
|
|
25
|
+
└──────────────────────┘ └──────────┘
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
1. **You ask** in plain language — goal, brand, sections, CTA, form fields.
|
|
29
|
+
2. **The AI learns the model** from the MCP: the element catalog, the absolute-positioning canvas, the event vocabulary — so it builds a *real* WebCake page, not a guess.
|
|
30
|
+
3. **It assembles + validates** the full `{ page, popup, settings, options }` JSON. `validate_page` catches off-canvas boxes, dangling CTAs, and missing form fields **before** anything is saved.
|
|
31
|
+
4. **It persists** to your WebCake account — dry-run preview first, then for real.
|
|
32
|
+
5. **You get an editor link** — open it, tweak, publish. The AI did the heavy lifting.
|
|
33
|
+
|
|
34
|
+
### Why it's reliable
|
|
35
|
+
|
|
36
|
+
| | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| 📚 **Knows the real model** | Serves WebCake's actual element catalog (40+ types — hero, form, countdown, gallery, product list…), each with its exact `specials` and AI hints, drawn straight from the editor's renderers. |
|
|
39
|
+
| ✅ **Validates before saving** | Structural + semantic checks (unique ids, on-canvas layout, working CTAs, unique form fields) so the page isn't broken when it lands. |
|
|
40
|
+
| 🛡️ **Safe by default** | Every write is **dry-run first** (preview the request, token masked) — nothing touches your account until you confirm. |
|
|
41
|
+
| ✏️ **Edits surgically** | Ask for one change ("make the CTA green") and it edits *only* that element — every other id, coordinate, and block stays exactly as it was. |
|
|
42
|
+
|
|
43
|
+
> 💡 **Lead-gen, events, invitations, app promos** — or **selling COD/online**? It speaks WebCake's commerce model too (product lists, variations, cart).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Under the hood
|
|
2
48
|
|
|
3
49
|
MCP (Model Context Protocol) server that teaches AI agents how to build a complete
|
|
4
50
|
**WebCake landing-page source JSON** from a requirement — and persist it to a WebCake backend.
|
package/README.vi.md
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# 🍰 WebCake Landing MCP
|
|
2
|
+
|
|
3
|
+
[English](./README.md) · **Tiếng Việt**
|
|
4
|
+
|
|
5
|
+
> **Mô tả landing page bằng lời nói — AI tự dựng, tự kiểm tra và đẩy thẳng lên WebCake.**
|
|
6
|
+
|
|
7
|
+
> *"Dựng cho tôi một landing page quán cà phê — một hero có nút đăng ký, một mục 3 tính năng, và một form thu lead. Lưu vào workspace của tôi."*
|
|
8
|
+
|
|
9
|
+
…và một trang WebCake **thật, sửa được** hiện ra trong tài khoản của bạn. Không kéo-thả từng khối, không cần học schema, không phải tự viết JSON.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 🧩 Mô hình hoạt động
|
|
14
|
+
|
|
15
|
+
Server này là **cầu nối** giữa trợ lý AI và WebCake. AI không *đoán* trang WebCake trông thế nào —
|
|
16
|
+
nó hỏi MCP (vốn nắm trọn mô hình element), kiểm tra hợp lệ, rồi lưu giúp bạn.
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
Bạn Trợ lý AI webcake-landing MCP WebCake
|
|
20
|
+
┌──────┐ yêu cầu┌────────────┐ tools ┌──────────────────────┐ API ┌──────────┐
|
|
21
|
+
│ ý │ ───────►│ Claude / │ ──────► │ • nắm mô hình element│ ────► │ trang │
|
|
22
|
+
│ tưởng│ │ Cursor / │ │ + gợi ý cho AI │ │ thật, │
|
|
23
|
+
│ │ ◄───────│ Windsurf │ ◄────── │ • dựng + kiểm tra │ ◄──── │ sửa được│
|
|
24
|
+
└──────┘ link └────────────┘ kết quả │ • lưu vào tài khoản │ │ trên │
|
|
25
|
+
trang └──────────────────────┘ │ WebCake │
|
|
26
|
+
└──────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
1. **Bạn yêu cầu** bằng lời — mục tiêu, thương hiệu, các section, nút CTA, trường form.
|
|
30
|
+
2. **AI học mô hình** từ MCP: danh mục element, canvas toạ độ tuyệt đối, bộ sự kiện — nên nó dựng trang WebCake *thật*, không phải đoán.
|
|
31
|
+
3. **Dựng + kiểm tra** trọn JSON `{ page, popup, settings, options }`. `validate_page` bắt lỗi (element lệch khung, CTA trỏ sai, thiếu trường form) **trước khi** lưu.
|
|
32
|
+
4. **Lưu** vào tài khoản WebCake — xem trước dry-run, rồi mới lưu thật.
|
|
33
|
+
5. **Nhận link editor** — mở, chỉnh, publish. AI lo phần nặng.
|
|
34
|
+
|
|
35
|
+
### Vì sao đáng tin
|
|
36
|
+
|
|
37
|
+
| | |
|
|
38
|
+
|---|---|
|
|
39
|
+
| 📚 **Nắm đúng mô hình** | Cung cấp danh mục element thật của WebCake (40+ loại — hero, form, đếm ngược, gallery, danh sách sản phẩm…), mỗi loại kèm `specials` chính xác và gợi ý cho AI, rút thẳng từ renderer của editor. |
|
|
40
|
+
| ✅ **Kiểm tra trước khi lưu** | Kiểm tra cấu trúc + ngữ nghĩa (id duy nhất, layout trong khung, CTA hoạt động, trường form không trùng) để trang không hỏng khi lưu. |
|
|
41
|
+
| 🛡️ **An toàn mặc định** | Mọi thao tác ghi đều **dry-run trước** (xem trước request, token được che) — không đụng tài khoản của bạn cho tới khi bạn xác nhận. |
|
|
42
|
+
| ✏️ **Sửa đúng chỗ** | Yêu cầu một thay đổi ("đổi nút CTA sang xanh") thì nó chỉ sửa *đúng* element đó — mọi id, toạ độ, khối khác giữ nguyên. |
|
|
43
|
+
|
|
44
|
+
> 💡 **Thu lead, sự kiện, thiệp mời, quảng bá app** — hay **bán hàng COD/online**? Nó hiểu cả mô hình thương mại của WebCake (danh sách sản phẩm, biến thể, giỏ hàng).
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Bản chất kỹ thuật
|
|
49
|
+
|
|
50
|
+
Server MCP (Model Context Protocol) dạy AI cách dựng trọn **JSON nguồn (page_source) của landing page WebCake**
|
|
51
|
+
từ một yêu cầu — và lưu nó về backend WebCake.
|
|
52
|
+
|
|
53
|
+
Nó expose danh mục element, gợi ý dùng từng element + `specials`, JSON Schema đầy đủ của trang, skeleton
|
|
54
|
+
element/trang hợp lệ, bộ kiểm tra trang, và các tool để tạo/sửa trang trên backend. AI dựng trọn JSON
|
|
55
|
+
`{ page, popup, settings, options, cartConfigs }`; `create_page` lưu nó (chỉ-source — trang mở trong editor,
|
|
56
|
+
lưu lại sẽ render).
|
|
57
|
+
|
|
58
|
+
## Hai cách chạy
|
|
59
|
+
|
|
60
|
+
| Chế độ | Lệnh | Khi nào |
|
|
61
|
+
|------|------|------|
|
|
62
|
+
| **CDN / npx** (không clone) | `npx -y webcake-landing-mcp` | Khởi động nhanh nhất — npm tự tải & chạy, không cần clone hay build. Tự cập nhật bản mới nhất. |
|
|
63
|
+
| **Local** (clone & build) | `node /abs/path/dist/index.js` | Khi đang sửa server, offline, hoặc cần ghim một bản build cụ thể. Chạy `npm run build` trước. |
|
|
64
|
+
|
|
65
|
+
Cả hai cùng expose y hệt các tool. Mọi cấu hình IDE bên dưới dùng dạng **local**; để dùng **CDN**, chỉ cần
|
|
66
|
+
đổi `command`/`args` sang dạng npx (xem [Chạy không cần clone (npx)](#chạy-không-cần-clone-npx)).
|
|
67
|
+
|
|
68
|
+
## Cài nhanh (Khuyến nghị)
|
|
69
|
+
|
|
70
|
+
Chạy script tự cài — lo trọn gói: clone, cài dependencies, build, và cấu hình IDE của bạn.
|
|
71
|
+
|
|
72
|
+
### macOS / Linux
|
|
73
|
+
|
|
74
|
+
Nếu bạn đã clone repo:
|
|
75
|
+
```bash
|
|
76
|
+
./install.sh
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Hoặc tải & chạy trực tiếp:
|
|
80
|
+
```bash
|
|
81
|
+
curl -fsSL https://raw.githubusercontent.com/vuluu2k/webcake-landing-mcp/main/install.sh -o install.sh && bash install.sh
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Trình cài tương tác: hỏi nơi cài (mặc định `~/.webcake-landing-mcp`), hỏi các biến môi trường
|
|
85
|
+
(`WEBCAKE_API_BASE`, `WEBCAKE_JWT`, `WEBCAKE_ORG_ID` — đều tuỳ chọn, Enter để bỏ qua), rồi cho bạn chọn
|
|
86
|
+
IDE cần cấu hình: `claude-desktop`, `claude-code`, `cursor`, `windsurf`, `augment`, `codex`, hoặc tất cả.
|
|
87
|
+
|
|
88
|
+
Gỡ cài (xoá entry MCP server khỏi mọi IDE đã cấu hình):
|
|
89
|
+
```bash
|
|
90
|
+
./install.sh --uninstall
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Windows (PowerShell)
|
|
94
|
+
|
|
95
|
+
Nếu bạn đã clone repo:
|
|
96
|
+
```powershell
|
|
97
|
+
.\install.ps1
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Hoặc tải & chạy trực tiếp:
|
|
101
|
+
```powershell
|
|
102
|
+
irm https://raw.githubusercontent.com/vuluu2k/webcake-landing-mcp/main/install.ps1 -OutFile install.ps1; .\install.ps1
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Gỡ cài:
|
|
106
|
+
```powershell
|
|
107
|
+
.\install.ps1 --uninstall
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Cập nhật
|
|
113
|
+
|
|
114
|
+
Cập nhật lên bản mới nhất:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
cd ~/.webcake-landing-mcp # hoặc nơi bạn đã cài
|
|
118
|
+
git pull
|
|
119
|
+
npm install
|
|
120
|
+
npm run build
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Rồi khởi động lại IDE.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Chạy không cần clone (npx)
|
|
128
|
+
|
|
129
|
+
Sau khi đã publish lên npm, server chạy thẳng từ registry — không clone, không build:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx -y webcake-landing-mcp
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Hoặc chạy bản mới nhất từ GitHub (npx tự clone + build qua script `prepare`):
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx -y github:vuluu2k/webcake-landing-mcp
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Tự cấu hình IDE (lệnh con `install`)
|
|
142
|
+
|
|
143
|
+
`npx` chỉ **chạy** server — khác với `install.sh`/`install.ps1`, nó không ghi cấu hình MCP vào IDE.
|
|
144
|
+
Lệnh con `install` đi kèm sẽ làm hộ bạn bước đó, không cần clone:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Tương tác — hỏi env + chọn IDE từng bước
|
|
148
|
+
npx -y webcake-landing-mcp install
|
|
149
|
+
|
|
150
|
+
# Không tương tác — cấu hình mọi IDE hỗ trợ cùng lúc
|
|
151
|
+
npx -y webcake-landing-mcp install --ide all --jwt <your-jwt> --api-base http://localhost:5800
|
|
152
|
+
|
|
153
|
+
# Chỉ một IDE
|
|
154
|
+
npx -y webcake-landing-mcp install --ide cursor --jwt <your-jwt>
|
|
155
|
+
|
|
156
|
+
# Gỡ server khỏi mọi cấu hình IDE
|
|
157
|
+
npx -y webcake-landing-mcp uninstall
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Nó ghi entry `webcake-landing` (dùng dạng khởi chạy `npx` bên dưới) vào đúng file cấu hình của từng IDE:
|
|
161
|
+
`claude-desktop`, `claude-code`, `cursor`, `windsurf`, `augment` (VS Code), `codex`, hoặc `all`. Cờ:
|
|
162
|
+
`--ide`, `--api-base`, `--jwt`, `--org-id`, `--host`, `--app-base`, `--npx`/`--local`, `-y`. Chạy
|
|
163
|
+
`npx -y webcake-landing-mcp --help` để xem đầy đủ.
|
|
164
|
+
|
|
165
|
+
### Cấu hình thủ công
|
|
166
|
+
|
|
167
|
+
Cấu hình MCP giống bản local, chỉ khác `command`/`args` trỏ tới `npx` thay vì file đã build:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"mcpServers": {
|
|
172
|
+
"webcake-landing": {
|
|
173
|
+
"command": "npx",
|
|
174
|
+
"args": ["-y", "webcake-landing-mcp"],
|
|
175
|
+
"env": {
|
|
176
|
+
"WEBCAKE_API_BASE": "http://localhost:5800",
|
|
177
|
+
"WEBCAKE_JWT": "<your-jwt>"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> npx cache lại package sau lần chạy đầu, nên các lần sau khởi động nhanh. Dùng phiên bản ghim
|
|
185
|
+
> (`webcake-landing-mcp@1.0.0`) nếu cần build tái lập được.
|
|
186
|
+
|
|
187
|
+
## Cài thủ công (local)
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
git clone https://github.com/vuluu2k/webcake-landing-mcp.git
|
|
191
|
+
cd webcake-landing-mcp
|
|
192
|
+
npm install # postinstall `prepare` tự build dist/
|
|
193
|
+
npm run build # (re)build: tsc -> dist/ + copy page-schema.json
|
|
194
|
+
npm run smoke # self-test offline của factory + validator (in "ALL GOOD")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Các tool tham chiếu/kiểm tra chạy với **zero config**. Biến môi trường chỉ cần cho các tool lưu trữ
|
|
198
|
+
(`create_page`, `update_page`, `list_pages`, `get_page`, `list_organizations`).
|
|
199
|
+
|
|
200
|
+
## Biến môi trường
|
|
201
|
+
|
|
202
|
+
| Biến | Bắt buộc | Mô tả |
|
|
203
|
+
|----------|----------|-------------|
|
|
204
|
+
| `WEBCAKE_API_BASE` | Không* | Base URL backend, ví dụ `http://localhost:5800`. Cần để lưu trang. |
|
|
205
|
+
| `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. |
|
|
206
|
+
| `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. |
|
|
207
|
+
| `WEBCAKE_HOST` | Không | Header `Host` tuỳ chọn (Phoenix route theo host, ví dụ `builder.localhost`). |
|
|
208
|
+
| `WEBCAKE_APP_BASE` | Không | Base tuỳ chọn để dựng URL editor/preview trong kết quả. |
|
|
209
|
+
|
|
210
|
+
> \* `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
|
|
211
|
+
> (`get_generation_guide`, `list_elements`, `get_element`, `validate_page`, …) chạy không cần chúng.
|
|
212
|
+
|
|
213
|
+
> Lưu trang sẽ ghi một trang thật vào nơi `WEBCAKE_API_BASE` trỏ tới, dùng JWT làm tài khoản đó.
|
|
214
|
+
> Hãy bắt đầu với local/staging.
|
|
215
|
+
|
|
216
|
+
### Cách lấy `WEBCAKE_JWT`
|
|
217
|
+
|
|
218
|
+
1. Mở dashboard builder WebCake và đăng nhập
|
|
219
|
+
2. Mở DevTools (`F12` hoặc `Cmd + Option + I`)
|
|
220
|
+
3. Vào tab **Network** > click một trang bất kỳ
|
|
221
|
+
4. Tìm một request API (ví dụ `@me`, `organizations`…)
|
|
222
|
+
5. Trong **Request Headers**, copy giá trị sau `Authorization: Bearer ` → đó là `WEBCAKE_JWT`
|
|
223
|
+
6. Dùng tool `list_organizations` để liệt kê org và chọn `WEBCAKE_ORG_ID`
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Cấu hình theo IDE / công cụ AI
|
|
228
|
+
|
|
229
|
+
> Thay `/absolute-path/webcake-landing-mcp/dist/index.js` bên dưới bằng đường dẫn thật nơi bạn đã
|
|
230
|
+
> clone/build repo. Ví dụ: `/Users/username/webcake-landing-mcp/dist/index.js`.
|
|
231
|
+
> Chạy `npm run build` trước để `dist/` tồn tại.
|
|
232
|
+
|
|
233
|
+
### 1. Claude Desktop
|
|
234
|
+
|
|
235
|
+
Mở Settings > Developer > Edit Config, hoặc sửa file trực tiếp:
|
|
236
|
+
|
|
237
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
238
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
239
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"mcpServers": {
|
|
244
|
+
"webcake-landing": {
|
|
245
|
+
"command": "node",
|
|
246
|
+
"args": ["/absolute-path/webcake-landing-mcp/dist/index.js"],
|
|
247
|
+
"env": {
|
|
248
|
+
"WEBCAKE_API_BASE": "http://localhost:5800",
|
|
249
|
+
"WEBCAKE_JWT": "<your-jwt>",
|
|
250
|
+
"WEBCAKE_HOST": "builder.localhost",
|
|
251
|
+
"WEBCAKE_APP_BASE": "http://builder.localhost:5800"
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Khởi động lại Claude Desktop. Các tool MCP sẽ hiện trong ô chat (biểu tượng búa).
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
### 2. Claude Code (CLI)
|
|
263
|
+
|
|
264
|
+
Chạy trong terminal — bản **local**:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
claude mcp add webcake-landing \
|
|
268
|
+
-e WEBCAKE_API_BASE=http://localhost:5800 \
|
|
269
|
+
-e WEBCAKE_JWT=<your-jwt> \
|
|
270
|
+
-e WEBCAKE_HOST=builder.localhost \
|
|
271
|
+
-- node /absolute-path/webcake-landing-mcp/dist/index.js
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Hoặc **CDN / npx** (không clone):
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
claude mcp add webcake-landing \
|
|
278
|
+
-e WEBCAKE_API_BASE=http://localhost:5800 \
|
|
279
|
+
-e WEBCAKE_JWT=<your-jwt> \
|
|
280
|
+
-- npx -y webcake-landing-mcp
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Hoặc tạo `.claude.json` ở thư mục gốc dự án (hoặc `~/.claude.json` toàn cục):
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"mcpServers": {
|
|
288
|
+
"webcake-landing": {
|
|
289
|
+
"command": "node",
|
|
290
|
+
"args": ["/absolute-path/webcake-landing-mcp/dist/index.js"],
|
|
291
|
+
"env": {
|
|
292
|
+
"WEBCAKE_API_BASE": "http://localhost:5800",
|
|
293
|
+
"WEBCAKE_JWT": "<your-jwt>"
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Kiểm tra:
|
|
301
|
+
```bash
|
|
302
|
+
claude mcp list
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
### 3. Cursor
|
|
308
|
+
|
|
309
|
+
Tạo `.cursor/mcp.json` ở gốc dự án (hoặc `~/.cursor/mcp.json` toàn cục):
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
{
|
|
313
|
+
"mcpServers": {
|
|
314
|
+
"webcake-landing": {
|
|
315
|
+
"command": "node",
|
|
316
|
+
"args": ["/absolute-path/webcake-landing-mcp/dist/index.js"],
|
|
317
|
+
"env": {
|
|
318
|
+
"WEBCAKE_API_BASE": "http://localhost:5800",
|
|
319
|
+
"WEBCAKE_JWT": "<your-jwt>"
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Khởi động lại Cursor và xem Settings > MCP Servers để thấy trạng thái **"Connected"**.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
### 4. Windsurf
|
|
331
|
+
|
|
332
|
+
Tạo `~/.codeium/windsurf/mcp_config.json`:
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"mcpServers": {
|
|
337
|
+
"webcake-landing": {
|
|
338
|
+
"command": "node",
|
|
339
|
+
"args": ["/absolute-path/webcake-landing-mcp/dist/index.js"],
|
|
340
|
+
"env": {
|
|
341
|
+
"WEBCAKE_API_BASE": "http://localhost:5800",
|
|
342
|
+
"WEBCAKE_JWT": "<your-jwt>"
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Khởi động lại Windsurf. Gõ `@` trong chat Cascade để thấy các tool `webcake-landing`.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
### 5. Augment (Extension VS Code)
|
|
354
|
+
|
|
355
|
+
Mở Command Palette: `Cmd + Shift + P` > **"Augment: Edit MCP Settings"**, rồi thêm:
|
|
356
|
+
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"mcpServers": {
|
|
360
|
+
"webcake-landing": {
|
|
361
|
+
"command": "node",
|
|
362
|
+
"args": ["/absolute-path/webcake-landing-mcp/dist/index.js"],
|
|
363
|
+
"env": {
|
|
364
|
+
"WEBCAKE_API_BASE": "http://localhost:5800",
|
|
365
|
+
"WEBCAKE_JWT": "<your-jwt>"
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Khởi động lại VS Code.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
### 6. Codex (OpenAI CLI)
|
|
377
|
+
|
|
378
|
+
Thêm vào `~/.codex/config.toml`:
|
|
379
|
+
|
|
380
|
+
```toml
|
|
381
|
+
[mcp_servers.webcake-landing]
|
|
382
|
+
command = "node"
|
|
383
|
+
args = ["/absolute-path/webcake-landing-mcp/dist/index.js"]
|
|
384
|
+
env = { "WEBCAKE_API_BASE" = "http://localhost:5800", "WEBCAKE_JWT" = "<your-jwt>" }
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Kiểm tra:
|
|
388
|
+
```bash
|
|
389
|
+
codex mcp list
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Ví dụ sử dụng
|
|
395
|
+
|
|
396
|
+
### Ví dụ 1: Dựng landing page mới từ một brief
|
|
397
|
+
|
|
398
|
+
**Prompt:**
|
|
399
|
+
```
|
|
400
|
+
Dựng cho tôi một landing page WebCake cho "Acme Coffee" — một hero có CTA, một mục 3 tính năng,
|
|
401
|
+
và một form đăng ký. Lưu vào org mặc định của tôi.
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**AI sẽ tự động:**
|
|
405
|
+
|
|
406
|
+
**Bước 1** — Gọi `get_generation_guide` để học quy ước (canvas, hệ toạ độ, sự kiện, workflow)
|
|
407
|
+
|
|
408
|
+
**Bước 2** — Gọi `new_page_skeleton` để có source top-level rỗng, rồi `get_element` cho từng loại element nó dùng:
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
get_element({ type: "section" })
|
|
412
|
+
get_element({ type: "text-block" })
|
|
413
|
+
get_element({ type: "button" })
|
|
414
|
+
get_element({ type: "form" })
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Bước 3** — Lắp trọn JSON `{ page, popup, settings, options, cartConfigs }`, rồi kiểm tra:
|
|
418
|
+
|
|
419
|
+
```
|
|
420
|
+
validate_page({ source })
|
|
421
|
+
→ { ok: false, errors: ["BUTTON-2: event target 'POPUP-9' not found"] } # sửa hết lỗi, validate lại
|
|
422
|
+
validate_page({ source })
|
|
423
|
+
→ { ok: true, errors: [] }
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Bước 4** — Lưu (dry-run trước, rồi mới thật):
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
list_organizations({}) → chọn org
|
|
430
|
+
create_page({ source }) → xem trước dry-run (JWT được che)
|
|
431
|
+
create_page({ source, dry_run: false }) → { page_id, editor_url, preview_url }
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Mở trang trong editor và lưu lại để render `app`/`app_css`.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### Ví dụ 2: Sửa một trang có sẵn
|
|
439
|
+
|
|
440
|
+
**Prompt:**
|
|
441
|
+
```
|
|
442
|
+
Trên landing page "Acme Coffee" của tôi, đổi headline hero thành "Freshly Roasted Daily"
|
|
443
|
+
và làm nút CTA màu xanh lá.
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**AI sửa đúng chỗ — không bao giờ dựng lại cả cây:**
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
# Bước 1: tìm trang
|
|
450
|
+
list_pages({})
|
|
451
|
+
→ [{ id: "page_42", name: "Acme Coffee", organization_id: "org_1", ... }]
|
|
452
|
+
|
|
453
|
+
# Bước 2: lấy cây source đã decode
|
|
454
|
+
get_page({ page_id: "page_42" })
|
|
455
|
+
|
|
456
|
+
# Bước 3: chỉ đổi text headline + màu nút, giữ mọi id/toạ độ khác,
|
|
457
|
+
# rồi validate và ghi lại
|
|
458
|
+
validate_page({ source }) → ok
|
|
459
|
+
update_page({ page_id: "page_42", source }) → xem trước dry-run
|
|
460
|
+
update_page({ page_id: "page_42", source, dry_run: false })
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
### Ví dụ 3: Xem chi tiết một loại element trước khi dùng
|
|
466
|
+
|
|
467
|
+
**Prompt:**
|
|
468
|
+
```
|
|
469
|
+
Một element form cần những specials gì, và cho tôi xem một ví dụ hợp lệ.
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**AI gọi:**
|
|
473
|
+
|
|
474
|
+
```
|
|
475
|
+
get_element({ type: "form" })
|
|
476
|
+
→ {
|
|
477
|
+
hints: "Mỗi input cần một specials.field_name duy nhất…",
|
|
478
|
+
specials: { ... },
|
|
479
|
+
skeleton: { ... }, # node mặc định hợp lệ về cấu trúc
|
|
480
|
+
example: { ... } # ví dụ đã điền, thực tế
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Hướng dẫn dùng tool chi tiết
|
|
487
|
+
|
|
488
|
+
Các tool chia thành ba nhóm: **tham chiếu** (học mô hình — không cần config),
|
|
489
|
+
**generation** (dựng node hợp lệ), và **lưu trữ** (ghi về backend — cần biến môi trường).
|
|
490
|
+
|
|
491
|
+
### Bước 1: Đọc guide trước — `get_generation_guide`
|
|
492
|
+
|
|
493
|
+
Luôn gọi cái này **đầu tiên**. Nó trả về hình dạng output, hệ toạ độ (desktop ≈ 960px,
|
|
494
|
+
mobile ≈ 420px), bộ từ vựng sự kiện, và workflow đầu-cuối.
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
get_generation_guide({})
|
|
498
|
+
→ "## Output shape… ## Canvas… ## Events… ## Workflow…"
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Bước 2: Duyệt danh mục element — `list_elements` / `get_element`
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
# Mọi loại element theo nhóm (tóm tắt + khi nào dùng + có phải container?)
|
|
505
|
+
list_elements({})
|
|
506
|
+
→ { categories: { layout: [...], content: [...], form: [...], ... } }
|
|
507
|
+
|
|
508
|
+
# Xem sâu một loại — hints, specials chính, skeleton mặc định, ví dụ đã điền
|
|
509
|
+
get_element({ type: "button" })
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Bước 3: Lấy khối dựng hợp lệ — `new_element` / `new_page_skeleton`
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
# Một node mặc định hợp lệ về cấu trúc cho một loại (id mới)
|
|
516
|
+
new_element({ type: "section" })
|
|
517
|
+
|
|
518
|
+
# Một source top-level rỗng nhưng đầy đủ
|
|
519
|
+
new_page_skeleton({})
|
|
520
|
+
→ { page: [], popup: [], settings: {…}, options: { currency, mobileOnly, versionID }, cartConfigs: {} }
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Bước 4: Xem / kiểm tra — `get_page_schema` / `validate_page`
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
# JSON Schema đầy đủ (Draft 2020-12) của một page source
|
|
527
|
+
get_page_schema({})
|
|
528
|
+
|
|
529
|
+
# Kiểm tra cấu trúc + ngữ nghĩa — sửa hết lỗi trước khi lưu
|
|
530
|
+
validate_page({ source })
|
|
531
|
+
→ { ok: false, errors: [...], warnings: [...] }
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
`validate_page` **errors là chặn**; warnings (event target lửng lơ, thiếu `field_name`) chỉ là khuyến cáo.
|
|
535
|
+
|
|
536
|
+
### Bước 5: Lưu — `list_organizations` / `create_page` / `update_page`
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
# Liệt kê các organization của tài khoản — hỏi dùng cái nào; mặc định = org is_default
|
|
540
|
+
list_organizations({})
|
|
541
|
+
→ [{ id: "org_1", name: "Acme", is_default: true }, ...]
|
|
542
|
+
|
|
543
|
+
# Tạo trang MỚI (chỉ-source). Mặc định dry_run=true.
|
|
544
|
+
create_page({ source, organization_id: "org_1" }) # xem trước
|
|
545
|
+
create_page({ source, dry_run: false }) # tạo thật
|
|
546
|
+
|
|
547
|
+
# Sửa một trang CÓ SẴN
|
|
548
|
+
list_pages({}) # tìm trang
|
|
549
|
+
get_page({ page_id }) # lấy source đã decode
|
|
550
|
+
update_page({ page_id, source, dry_run: false }) # ghi đè (mặc định dry_run=true)
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
`create_page` gọi **`POST {WEBCAKE_API_BASE}/api/v1/ai/create_page_from_source`** trên backend.
|
|
554
|
+
Cả `create_page` và `update_page` đều **mặc định `dry_run=true`** (kiểm tra và trả về request nó *sẽ*
|
|
555
|
+
gửi, JWT được che); đặt `dry_run=false` để ghi thật. Kết quả trả về `page_id` + URL editor/preview.
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## Prompt gợi ý
|
|
560
|
+
|
|
561
|
+
> Dựng cho tôi một landing page WebCake cho <thương hiệu/ưu đãi>. Dùng MCP webcake-landing:
|
|
562
|
+
> gọi `get_generation_guide`, `new_page_skeleton`, rồi `get_element` cho từng loại element bạn dùng,
|
|
563
|
+
> lắp JSON `{ page, popup, settings, options }`, `validate_page` đến khi 0 lỗi,
|
|
564
|
+
> rồi `create_page` (dry-run trước).
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
## Danh sách tool
|
|
569
|
+
|
|
570
|
+
### Tham chiếu (không cần config)
|
|
571
|
+
| Tool | Mô tả |
|
|
572
|
+
|------|-------------|
|
|
573
|
+
| `get_generation_guide` | **Đọc ĐẦU TIÊN.** Hình dạng output, hệ toạ độ, bộ sự kiện, workflow. |
|
|
574
|
+
| `list_elements` | Mọi loại element theo nhóm (tóm tắt + khi nào dùng + container?). |
|
|
575
|
+
| `get_element` | Một loại: hints, `specials` chính, skeleton mặc định, ví dụ đã điền. |
|
|
576
|
+
| `get_page_schema` | JSON Schema đầy đủ (Draft 2020-12) của một page source. |
|
|
577
|
+
|
|
578
|
+
### Generation
|
|
579
|
+
| Tool | Mô tả |
|
|
580
|
+
|------|-------------|
|
|
581
|
+
| `new_element` | Một node mặc định hợp lệ về cấu trúc cho một loại (id mới). |
|
|
582
|
+
| `new_page_skeleton` | Một source top-level rỗng nhưng đầy đủ `{ page, popup, settings, options, cartConfigs }`. |
|
|
583
|
+
| `validate_page` | Kiểm tra cấu trúc + ngữ nghĩa (ids, event targets, containers, `field_name`). |
|
|
584
|
+
|
|
585
|
+
### Lưu trữ (cần `WEBCAKE_API_BASE` + `WEBCAKE_JWT`)
|
|
586
|
+
| Tool | Mô tả |
|
|
587
|
+
|------|-------------|
|
|
588
|
+
| `list_organizations` | Liệt kê organization của tài khoản (id, name, is_default). Mặc định = org `is_default`. |
|
|
589
|
+
| `create_page` | Lưu một source đã sinh thành trang mới (chỉ-source). **Mặc định `dry_run=true`.** |
|
|
590
|
+
| `list_pages` | Liệt kê các trang của tài khoản (id, name, organization_id, updated_at) để chọn cái cần sửa. |
|
|
591
|
+
| `get_page` | Lấy cây source đã decode của một trang có sẵn để sửa. |
|
|
592
|
+
| `update_page` | Ghi đè source của một trang có sẵn bằng cây đã sửa. **Mặc định `dry_run=true`.** |
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Ghi chú về mô hình
|
|
597
|
+
|
|
598
|
+
- **Canvas toạ độ tuyệt đối:** mỗi phần tử con mang `top/left/width/height` dạng số theo từng breakpoint;
|
|
599
|
+
section xếp dọc và tự giữ `height`. Nội dung nằm trong `specials` (`text`, `src`, …), không bao giờ trong `styles`.
|
|
600
|
+
- **Source top-level:** `{ page: [sections], popup: [popups], settings: {…}, options: { currency, mobileOnly, versionID }, cartConfigs: {} }`.
|
|
601
|
+
Popup là một mảng top-level **riêng**, không lồng trong `page`.
|
|
602
|
+
- Animation theo breakpoint nằm trong `config.animation = { name, delay, duration, repeat }`.
|
|
603
|
+
- Màu dạng `rgba()`; `top/left/width/height/fontSize` là số (px); input form cần một `specials.field_name` duy nhất.
|
|
604
|
+
|
|
605
|
+
Tham khảo: [docs/page-element-schema.md](docs/page-element-schema.md),
|
|
606
|
+
[docs/element-specials-reference.md](docs/element-specials-reference.md) (tham chiếu đầy đủ mọi specials/event),
|
|
607
|
+
và [src/page-schema.json](src/page-schema.json) (JSON Schema, Draft 2020-12). Schema phản ánh đúng
|
|
608
|
+
hình dạng `page_source` thật của editor.
|
package/dist/factory.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* the sizes / specials the real editor seeds. Used by the `new_element` tool so
|
|
5
5
|
* Claude always starts from a structurally-correct node.
|
|
6
6
|
*/
|
|
7
|
+
import { CONTAINER_TYPES, FIELD_TYPES } from "./library.js";
|
|
8
|
+
// Re-exported for back-compat: these are defined alongside LIBRARY in library.ts
|
|
9
|
+
// (the single source of truth) but historically imported from factory.
|
|
10
|
+
export { CONTAINER_TYPES, FIELD_TYPES };
|
|
7
11
|
const ALNUM = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
8
12
|
export function randomId(len = 8) {
|
|
9
13
|
let s = "";
|
|
@@ -19,40 +23,6 @@ export function randomId(len = 8) {
|
|
|
19
23
|
export function imgPlaceholder(w = 600, h = 400, label = "Image") {
|
|
20
24
|
return `https://placehold.co/${Math.round(w)}x${Math.round(h)}?text=${encodeURIComponent(label)}`;
|
|
21
25
|
}
|
|
22
|
-
/** Types that carry a `children` array. */
|
|
23
|
-
export const CONTAINER_TYPES = new Set([
|
|
24
|
-
"section",
|
|
25
|
-
"dynamic_page",
|
|
26
|
-
"group",
|
|
27
|
-
"grid",
|
|
28
|
-
"grid-item",
|
|
29
|
-
"carousel",
|
|
30
|
-
"slide",
|
|
31
|
-
"popup",
|
|
32
|
-
"form",
|
|
33
|
-
// NOTE: "gallery" is intentionally NOT here — gallery.js reads specials.media only,
|
|
34
|
-
// it never reads vm.children. gallery is a leaf element.
|
|
35
|
-
"checkbox-group",
|
|
36
|
-
"radio",
|
|
37
|
-
"group-select",
|
|
38
|
-
]);
|
|
39
|
-
/** Form input types that require a unique specials.field_name. */
|
|
40
|
-
export const FIELD_TYPES = new Set([
|
|
41
|
-
"input",
|
|
42
|
-
"textarea",
|
|
43
|
-
"select",
|
|
44
|
-
"checkbox",
|
|
45
|
-
"checkbox-group",
|
|
46
|
-
"radio",
|
|
47
|
-
"address",
|
|
48
|
-
"country-select",
|
|
49
|
-
"quantity_input",
|
|
50
|
-
"input-datetime",
|
|
51
|
-
"input-file",
|
|
52
|
-
"signature",
|
|
53
|
-
"verify-code",
|
|
54
|
-
"group-select-item",
|
|
55
|
-
]);
|
|
56
26
|
/** Default per-breakpoint animation block (matches real page_source). */
|
|
57
27
|
export function defaultAnimation() {
|
|
58
28
|
return { name: "none", delay: 0, duration: 3, repeat: null };
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
18
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import { createElement, createPageSource } from "./factory.js";
|
|
21
|
-
import { LIBRARY, GENERATION_GUIDE, CANVAS, CLICK_ACTIONS, HOVER_ACTIONS, EVENT_TRIGGERS, } from "./library.js";
|
|
21
|
+
import { LIBRARY, GENERATION_GUIDE, CANVAS, CLICK_ACTIONS, HOVER_ACTIONS, SUCCESS_ACTIONS, ERROR_ACTIONS, DELAY_ACTIONS, EVENT_TRIGGERS, } from "./library.js";
|
|
22
22
|
import { validatePage, coercePage, pageSchema } from "./validate.js";
|
|
23
23
|
import { readConfig, buildRequestRedacted, createPage, listOrganizations, listPages, getPageSource, updatePageSource, buildUpdateRequestRedacted, } from "./webcake.js";
|
|
24
24
|
const ALL_TYPES = Object.keys(LIBRARY);
|
|
@@ -52,6 +52,9 @@ server.tool("get_generation_guide", "Read this FIRST. Conventions for building a
|
|
|
52
52
|
event_triggers: EVENT_TRIGGERS,
|
|
53
53
|
click_actions: CLICK_ACTIONS,
|
|
54
54
|
hover_actions: HOVER_ACTIONS,
|
|
55
|
+
success_actions: SUCCESS_ACTIONS,
|
|
56
|
+
error_actions: ERROR_ACTIONS,
|
|
57
|
+
delay_actions: DELAY_ACTIONS,
|
|
55
58
|
}));
|
|
56
59
|
// 2) List elements ------------------------------------------------------------
|
|
57
60
|
server.tool("list_elements", "List every supported element type, grouped by category, with a one-line summary and whether it is a container (can hold children).", async () => {
|
|
@@ -100,7 +103,7 @@ server.tool("new_element", "Return a structurally-valid default element node for
|
|
|
100
103
|
// 5) Page schema --------------------------------------------------------------
|
|
101
104
|
server.tool("get_page_schema", "Return the full JSON Schema (Draft 2020-12) of a Webcake page source object { page: [...], settings: {...} }. Use it to understand the exact structure or for your own validation.", async () => text(pageSchema));
|
|
102
105
|
// 6) Validate page ------------------------------------------------------------
|
|
103
|
-
server.tool("validate_page", "Validate a generated page source against the schema + semantic rules (unique ids, dangling event targets, children only on containers, missing field_name, top-level types). Returns errors (must fix) and warnings. ALWAYS run before returning the final page.", {
|
|
106
|
+
server.tool("validate_page", "Validate a generated page source against the schema + semantic rules (unique ids, dangling event targets, children only on containers, missing field_name, top-level types) plus form-data bindings (duplicate field_name within one form, dangling option-event promoId / connectedSurvey / connectedForm / set_field_value targets). Returns errors (must fix) and warnings. ALWAYS run before returning the final page.", {
|
|
104
107
|
page: z
|
|
105
108
|
.any()
|
|
106
109
|
.describe("The page source object { page:[...], settings:{} } OR a JSON string of it."),
|
package/dist/library.js
CHANGED
|
@@ -6,42 +6,72 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export const CANVAS = { desktopWidth: 960, mobileWidth: 420, defaultSectionHeight: 800 };
|
|
8
8
|
export const EVENT_TRIGGERS = ["click", "hover", "success", "error", "unset", "delay"];
|
|
9
|
+
// Click-trigger actions. "Extra:" lists the action-specific event-object fields
|
|
10
|
+
// the dispatcher reads beyond { id, type, action, target } (render_v4/event/index.js).
|
|
9
11
|
export const CLICK_ACTIONS = {
|
|
10
12
|
none: "Do nothing.",
|
|
11
|
-
open_link: "Open a URL. target = URL (
|
|
12
|
-
open_popup: "Open a popup. target = popup element id.",
|
|
13
|
-
close_popup: "Close a popup. target = popup element id.",
|
|
14
|
-
scroll_to: "Smooth-scroll to an element. target = element/section id.",
|
|
13
|
+
open_link: "Open a URL. target = URL. Extra: targetURL ('_blank'|'_self'), open_link_with_params (bool), send_to_thank_page (bool), delayTime (seconds).",
|
|
14
|
+
open_popup: "Open a popup. target = popup element id. Extra: animation, reverseAnimation.",
|
|
15
|
+
close_popup: "Close a popup. target = popup element id. Extra: animation.",
|
|
16
|
+
scroll_to: "Smooth-scroll to an element. target = element/section id. Extra: scrollMore (bonus px offset).",
|
|
15
17
|
show_section: "Show a hidden section. target = section id.",
|
|
16
18
|
hide_section: "Hide a section. target = section id.",
|
|
17
|
-
show_hide_element: "Toggle element visibility. target = element id.",
|
|
18
|
-
change_tab: "Switch tab. target = id.",
|
|
19
|
-
lightbox: "Open
|
|
20
|
-
copy: "Copy to clipboard. target = the text; OR an element id when copyType='elementValue'.",
|
|
21
|
-
collapse: "Collapse/expand. target = id.",
|
|
22
|
-
set_field_value: "Set a form field value. target = field_name
|
|
19
|
+
show_hide_element: "Toggle element visibility. target = element id (comma-separated list allowed). Extra: onlyMode ('show'|'hide'), animation, animationOut.",
|
|
20
|
+
change_tab: "Switch tab/slide in a gallery/carousel. target = container id. Extra: moveTo ('prev'|'next'|'index'), tabIndex.",
|
|
21
|
+
lightbox: "Open in a lightbox. target = image/video/iframe URL. Extra: typeLightbox ('image'|'video'|'iframe'), alt.",
|
|
22
|
+
copy: "Copy to clipboard. target = the text; OR an element id when copyType='elementValue'. Extra: copyType.",
|
|
23
|
+
collapse: "Collapse/expand. target = element id.",
|
|
24
|
+
set_field_value: "Set a form field value. target = field_name (or w-<element id>). Extra: set_value (the value to set).",
|
|
23
25
|
back_to: "Go back in browser history (history.back()). target = none.",
|
|
24
26
|
share: "Share the current page URL. target = platform name: 'Facebook'|'Twitter'|'Custom'.",
|
|
25
27
|
play_audio: "Play audio. target = audio file URL (NOT an element id).",
|
|
26
28
|
stop_audio: "Stop audio. target = the same audio file URL (NOT an element id).",
|
|
27
|
-
open_sms: "Send SMS. target = phone number
|
|
29
|
+
open_sms: "Send SMS. target = phone number. Extra: smsBody (message body).",
|
|
28
30
|
send_email: "Open mail client. target = email address (mailto:).",
|
|
29
|
-
download_file: "Download a file. target = file URL
|
|
31
|
+
download_file: "Download a file. target = file URL. Extra: nameFile (overrides the saved filename).",
|
|
30
32
|
close_webview: "Close a Facebook/Messenger in-app webview. target = none.",
|
|
31
|
-
open_cart: "Open cart.",
|
|
32
|
-
add_to_cart: "Add product to cart.
|
|
33
|
-
open_app: "Open chat/app. event.appTarget selects the provider (botcake|botcake_dynamic|whatsapp|mess_prefill|tiktok_prefill|line_prefill|others); target = destination URL/phone/ref.",
|
|
34
|
-
change_color: "Change color.",
|
|
35
|
-
custom_js: "Run custom JS.",
|
|
33
|
+
open_cart: "Open the cart drawer (WCart).",
|
|
34
|
+
add_to_cart: "Add a product to the cart. Uses specials.sprod/svariant/squantity (or event.sprod_id/svariant/squantity); target unused.",
|
|
35
|
+
open_app: "Open chat/app. event.appTarget selects the provider (botcake|botcake_dynamic|whatsapp|mess_prefill|tiktok_prefill|line_prefill|others); target = destination URL/phone/ref. Extra: wa_custom_text, line_custom_text, formIdLink (per provider).",
|
|
36
|
+
change_color: "Change a color. Acts on the trigger element, or target_element for a cross-element change. Extra: change_color_type, change_color, target_mode, target_element.",
|
|
37
|
+
custom_js: "Run custom JS. Extra: custom_js (the code string).",
|
|
36
38
|
};
|
|
37
39
|
export const HOVER_ACTIONS = {
|
|
38
|
-
change_color: "Change color on hover.",
|
|
39
|
-
change_background: "Change background on hover.",
|
|
40
|
-
change_text_color: "Change text color on hover.",
|
|
40
|
+
change_color: "Change color on hover. Extra: change_color, change_color_type, hoverText, hoverBorder, target_mode, target_element.",
|
|
41
|
+
change_background: "Change background on hover. Extra: hoverColor (applied via --hover-color).",
|
|
42
|
+
change_text_color: "Change text color on hover. Extra: hoverText.",
|
|
41
43
|
change_underline: "Underline on hover.",
|
|
42
44
|
change_overline: "Overline on hover.",
|
|
43
|
-
animation_hover: "Play a hover animation.",
|
|
44
|
-
show_hide_element: "Reveal/hide a target element on hover.",
|
|
45
|
+
animation_hover: "Play a hover animation. target = none.",
|
|
46
|
+
show_hide_element: "Reveal/hide a target element on hover. target = element id. Extra: animation, animationOut.",
|
|
47
|
+
};
|
|
48
|
+
// Actions on a FORM's own events array, fired AFTER a successful submit (type:"success").
|
|
49
|
+
// target semantics match the click action of the same name.
|
|
50
|
+
export const SUCCESS_ACTIONS = {
|
|
51
|
+
phone_call: "Call a number. target = phone number (tel:).",
|
|
52
|
+
open_sms: "Send SMS. target = phone number. Extra: smsBody.",
|
|
53
|
+
send_email: "Open mail client. target = email address.",
|
|
54
|
+
open_link: "Open a URL. target = URL. Extra: targetURL ('_blank'|'_self').",
|
|
55
|
+
scroll_to: "Scroll to an element. target = element id. Extra: scrollMore.",
|
|
56
|
+
open_popup: "Open a popup. target = popup id.",
|
|
57
|
+
close_popup: "Close a popup. target = popup id.",
|
|
58
|
+
download_file: "Download a file. target = file URL. Extra: nameFile.",
|
|
59
|
+
show_hide_element: "Toggle visibility. target = element id. Extra: onlyMode.",
|
|
60
|
+
show_section: "Show a section. target = section id.",
|
|
61
|
+
hide_section: "Hide a section. target = section id.",
|
|
62
|
+
close_webview: "Close a Facebook/Messenger webview. target = none.",
|
|
63
|
+
change_tab: "Switch tab/slide. target = container id. Extra: moveTo, tabIndex.",
|
|
64
|
+
};
|
|
65
|
+
// Actions on a FORM's events array, fired when validation FAILS (type:"error").
|
|
66
|
+
export const ERROR_ACTIONS = {
|
|
67
|
+
open_popup: "Open a popup. target = popup id.",
|
|
68
|
+
close_popup: "Close a popup. target = popup id.",
|
|
69
|
+
show_hide_element: "Toggle visibility. target = element id. Extra: onlyMode.",
|
|
70
|
+
};
|
|
71
|
+
// Actions on ANY element's events array, fired when it scrolls into view (type:"delay").
|
|
72
|
+
export const DELAY_ACTIONS = {
|
|
73
|
+
show_element: "Reveal this element after a delay. Extra: delay_multiplier (ms, default 1000).",
|
|
74
|
+
hide_element: "Hide this element after a delay. Extra: delay_multiplier (ms, default 1000).",
|
|
45
75
|
};
|
|
46
76
|
export const LIBRARY = {
|
|
47
77
|
// ---------------- layout / containers ----------------
|
|
@@ -671,6 +701,21 @@ export const LIBRARY = {
|
|
|
671
701
|
keySpecials: {},
|
|
672
702
|
},
|
|
673
703
|
};
|
|
704
|
+
/**
|
|
705
|
+
* Structural flags — single source of truth, co-located with LIBRARY so adding or
|
|
706
|
+
* editing an element happens HERE (one entry) instead of being mirrored across
|
|
707
|
+
* files. factory.ts re-exports both; validate.ts consumes them.
|
|
708
|
+
* - CONTAINER_TYPES is DERIVED from each entry's `container` flag (no second list
|
|
709
|
+
* to keep in sync).
|
|
710
|
+
* - FIELD_TYPES lists the form inputs that submit a value and therefore need a
|
|
711
|
+
* unique specials.field_name.
|
|
712
|
+
*/
|
|
713
|
+
export const CONTAINER_TYPES = new Set(Object.keys(LIBRARY).filter((t) => LIBRARY[t].container));
|
|
714
|
+
export const FIELD_TYPES = new Set([
|
|
715
|
+
"input", "textarea", "select", "checkbox", "checkbox-group", "radio",
|
|
716
|
+
"address", "country-select", "quantity_input", "input-datetime",
|
|
717
|
+
"input-file", "signature", "verify-code", "group-select-item",
|
|
718
|
+
]);
|
|
674
719
|
export const GENERATION_GUIDE = `You are generating the JSON source of a Webcake landing page that the editor renders directly.
|
|
675
720
|
|
|
676
721
|
OUTPUT (top-level page source — matches the real editor shape)
|
|
@@ -717,7 +762,7 @@ RULES
|
|
|
717
762
|
- CONTRAST: text must contrast with the section background (dark text on light sections, light text on dark sections). Don't put light-gray text on white or faint text on a dark background.
|
|
718
763
|
- movable:false for section/slide/grid-item/popup; otherwise true. runtime is always {}.
|
|
719
764
|
- Every form input MUST have a unique specials.field_name.
|
|
720
|
-
- events item: { "id", "type"
|
|
765
|
+
- events item: { "id", "type", "action", "target", ...action-specific extra fields }. TRIGGER (type): click & hover on any element; success & error on a FORM (success = after a successful submit, error = on validation failure); delay on any element (when it scrolls into view); unset on init. Action vocab per trigger: click→CLICK_ACTIONS, hover→HOVER_ACTIONS, success→SUCCESS_ACTIONS, error→ERROR_ACTIONS, delay→DELAY_ACTIONS (all returned by get_generation_guide). For element-targeting actions (open_popup, close_popup, scroll_to, show_section, hide_section, show_hide_element, change_tab, collapse) target = the target element's id; open_link/download_file target = URL; open_sms/send_email/phone_call target = phone/email; copy target = text (or element id when copyType='elementValue'); set_field_value target = field_name; target may be null (e.g. animation_hover). Each action also reads extra fields (e.g. open_link→targetURL/delayTime, scroll_to→scrollMore, change_tab→moveTo/tabIndex, lightbox→typeLightbox/alt, show_hide_element→onlyMode, open_app→appTarget+provider fields, set_field_value→set_value) — see the action maps for the full list.
|
|
721
766
|
- ANIMATION: each breakpoint's config has config.animation = { "name":"none", "delay":0, "duration":3, "repeat":null }. Keep "none" unless an entrance animation is wanted.
|
|
722
767
|
- Do NOT invent prices, phone numbers, addresses, or statistics. Output text in the requested language.
|
|
723
768
|
|
package/dist/smoke.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createElement, CONTAINER_TYPES } from "./factory.js";
|
|
6
6
|
import { LIBRARY } from "./library.js";
|
|
7
|
-
import { validatePage } from "./validate.js";
|
|
7
|
+
import { validatePage, pageSchema } from "./validate.js";
|
|
8
8
|
let failures = 0;
|
|
9
9
|
const check = (name, cond, extra) => {
|
|
10
10
|
if (cond) {
|
|
@@ -118,5 +118,67 @@ for (const [type, doc] of Object.entries(LIBRARY)) {
|
|
|
118
118
|
const rr = validatePage(wrapped);
|
|
119
119
|
check(`example ${type} valid`, rr.valid, rr.errors);
|
|
120
120
|
}
|
|
121
|
+
console.log("== schema enum stays in sync with LIBRARY (single source of truth) ==");
|
|
122
|
+
const enumTypes = pageSchema.$defs?.elementType?.enum ?? [];
|
|
123
|
+
const libTypes = Object.keys(LIBRARY);
|
|
124
|
+
check("every LIBRARY type is in the schema enum", libTypes.every((t) => enumTypes.includes(t)), libTypes.filter((t) => !enumTypes.includes(t)));
|
|
125
|
+
check("every schema enum type is in LIBRARY", enumTypes.every((t) => libTypes.includes(t)), enumTypes.filter((t) => !libTypes.includes(t)));
|
|
126
|
+
console.log("== validate: form-data binding checks ==");
|
|
127
|
+
const mkBox = () => ({ desktop: { config: {}, styles: {} }, mobile: { config: {}, styles: {} } });
|
|
128
|
+
const bindingsBad = {
|
|
129
|
+
page: [
|
|
130
|
+
{
|
|
131
|
+
id: "secf", type: "section",
|
|
132
|
+
properties: { name: "F", movable: false, sync: true },
|
|
133
|
+
responsive: { desktop: { config: {}, styles: { position: "relative", height: 800 } }, mobile: { config: {}, styles: { position: "relative", height: 800 } } },
|
|
134
|
+
specials: {}, runtime: {}, events: [],
|
|
135
|
+
children: [
|
|
136
|
+
{
|
|
137
|
+
id: "frm1", type: "form",
|
|
138
|
+
properties: { name: "Form", movable: true, sync: true },
|
|
139
|
+
responsive: mkBox(), specials: {}, runtime: {}, events: [],
|
|
140
|
+
children: [
|
|
141
|
+
{ id: "i1", type: "input", properties: {}, responsive: mkBox(), specials: { field_name: "phone_number" }, events: [] },
|
|
142
|
+
{ id: "i2", type: "input", properties: {}, responsive: mkBox(), specials: { field_name: "phone_number" }, events: [] },
|
|
143
|
+
{ id: "rad1", type: "radio", properties: {}, responsive: mkBox(),
|
|
144
|
+
specials: { field_name: "opt", options: [{ id: "o1", events_option: [{ id: "e", type: "showhide", promoId: "ghost_target" }] }] },
|
|
145
|
+
runtime: {}, events: [], children: [] },
|
|
146
|
+
{ id: "sv1", type: "survey", properties: {}, responsive: mkBox(), specials: { field_name: "sv", connectedForm: "missing_field" }, events: [] },
|
|
147
|
+
{ id: "b1", type: "button", properties: {}, responsive: mkBox(), specials: { text: "X" },
|
|
148
|
+
events: [{ id: "ev", type: "click", action: "set_field_value", target: "w-nope" }] },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
const rbb = validatePage(bindingsBad);
|
|
156
|
+
check("dup field_name in form warned", rbb.warnings.some((w) => w.includes('field_name "phone_number"') && w.includes("used 2")), rbb.warnings);
|
|
157
|
+
check("dangling option promoId warned", rbb.warnings.some((w) => w.includes("promoId") && w.includes("ghost_target")), rbb.warnings);
|
|
158
|
+
check("dangling connectedForm warned", rbb.warnings.some((w) => w.includes("connectedForm") && w.includes("missing_field")), rbb.warnings);
|
|
159
|
+
check("dangling set_field_value element ref warned", rbb.warnings.some((w) => w.includes("set_field_value") && w.includes("w-nope")), rbb.warnings);
|
|
160
|
+
const bindingsGood = {
|
|
161
|
+
page: [
|
|
162
|
+
{
|
|
163
|
+
id: "secg", type: "section",
|
|
164
|
+
properties: { name: "G", movable: false, sync: true },
|
|
165
|
+
responsive: { desktop: { config: {}, styles: { position: "relative", height: 800 } }, mobile: { config: {}, styles: { position: "relative", height: 800 } } },
|
|
166
|
+
specials: {}, runtime: {}, events: [],
|
|
167
|
+
children: [
|
|
168
|
+
{
|
|
169
|
+
id: "frm2", type: "form",
|
|
170
|
+
properties: { name: "Form", movable: true, sync: true },
|
|
171
|
+
responsive: mkBox(), specials: {}, runtime: {}, events: [],
|
|
172
|
+
children: [
|
|
173
|
+
{ id: "n1", type: "input", properties: {}, responsive: mkBox(), specials: { field_name: "full_name" }, events: [] },
|
|
174
|
+
{ id: "p1", type: "input", properties: {}, responsive: mkBox(), specials: { field_name: "phone_number" }, events: [] },
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
const rbg = validatePage(bindingsGood);
|
|
182
|
+
check("clean form has no binding warnings", rbg.warnings.length === 0, rbg.warnings);
|
|
121
183
|
console.log(`\n${failures === 0 ? "ALL GOOD" : failures + " FAILURE(S)"}`);
|
|
122
184
|
process.exit(failures === 0 ? 0 : 1);
|
package/dist/validate.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Page validation: JSON-Schema structural check (ajv, draft 2020-12) plus
|
|
3
3
|
* semantic checks the schema can't express (unique ids, dangling event targets,
|
|
4
|
-
* children only on containers, missing field_name, top-level types).
|
|
4
|
+
* children only on containers, missing field_name, top-level types). Also checks
|
|
5
|
+
* form-data bindings: duplicate field_name within a single form, and dangling
|
|
6
|
+
* option-level event targets (specials.options[].events_option promoId) and
|
|
7
|
+
* survey/field cross-wiring (connectedSurvey / connectedForm / set_field_value).
|
|
5
8
|
*/
|
|
6
9
|
import { readFileSync } from "node:fs";
|
|
7
10
|
import Ajv2020Module from "ajv/dist/2020.js";
|
|
@@ -66,6 +69,12 @@ export function validatePage(input) {
|
|
|
66
69
|
// 2) Semantic
|
|
67
70
|
const ids = new Map();
|
|
68
71
|
const eventTargets = [];
|
|
72
|
+
// option-level events (specials.options[].events_option) targeting an element id
|
|
73
|
+
const optionTargets = [];
|
|
74
|
+
// survey/field cross-wiring (specials.connectedSurvey / connectedForm)
|
|
75
|
+
const connectRefs = [];
|
|
76
|
+
// form nodes — used to check field_name uniqueness within each form's scope
|
|
77
|
+
const forms = [];
|
|
69
78
|
let elementCount = 0;
|
|
70
79
|
const topList = Array.isArray(page?.page)
|
|
71
80
|
? page.page
|
|
@@ -113,6 +122,33 @@ export function validatePage(input) {
|
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
}
|
|
125
|
+
// collect form-data bindings: option-level events (showhide/collapse promoId)
|
|
126
|
+
// and survey/field cross-wiring; and remember form scopes for field_name checks.
|
|
127
|
+
const sp = node.specials;
|
|
128
|
+
if (sp && typeof sp === "object") {
|
|
129
|
+
if (Array.isArray(sp.options)) {
|
|
130
|
+
for (const opt of sp.options) {
|
|
131
|
+
if (!opt || !Array.isArray(opt.events_option))
|
|
132
|
+
continue;
|
|
133
|
+
for (const ev of opt.events_option) {
|
|
134
|
+
if (ev &&
|
|
135
|
+
(ev.type === "showhide" || ev.type === "collapse") &&
|
|
136
|
+
typeof ev.promoId === "string" &&
|
|
137
|
+
ev.promoId.trim() !== "") {
|
|
138
|
+
optionTargets.push({ from: node.id ?? path, kind: ev.type, target: ev.promoId });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const key of ["connectedSurvey", "connectedForm"]) {
|
|
144
|
+
const v = sp[key];
|
|
145
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
146
|
+
connectRefs.push({ from: node.id ?? path, key, target: v });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (type === "form")
|
|
151
|
+
forms.push(node);
|
|
116
152
|
if (Array.isArray(node.children)) {
|
|
117
153
|
node.children.forEach((c, idx) => walk(c, `${path}.children[${idx}]`));
|
|
118
154
|
}
|
|
@@ -133,14 +169,64 @@ export function validatePage(input) {
|
|
|
133
169
|
if (count > 1)
|
|
134
170
|
errors.push(`Duplicate id "${id}" used ${count} times — ids must be unique.`);
|
|
135
171
|
}
|
|
172
|
+
// Does `target` fail to resolve to any element id? (ids may be stored with or
|
|
173
|
+
// without the runtime `w-`/`#w-` prefix.)
|
|
174
|
+
const danglesId = (target) => {
|
|
175
|
+
const cleaned = target.replace(/^#?w-/, "");
|
|
176
|
+
return !ids.has(target) && !ids.has(cleaned);
|
|
177
|
+
};
|
|
136
178
|
// dangling element-target events
|
|
137
179
|
for (const t of eventTargets) {
|
|
138
180
|
if (ELEMENT_TARGET_ACTIONS.has(t.action)) {
|
|
139
|
-
|
|
140
|
-
if (!ids.has(t.target) && !ids.has(cleaned)) {
|
|
181
|
+
if (danglesId(t.target)) {
|
|
141
182
|
warnings.push(`event on "${t.from}" action="${t.action}" target="${t.target}" does not match any element id.`);
|
|
142
183
|
}
|
|
143
184
|
}
|
|
185
|
+
else if (t.action === "set_field_value" && /^#?w-/.test(t.target) && danglesId(t.target)) {
|
|
186
|
+
// set_field_value target is a field_name OR an element id; only an explicit
|
|
187
|
+
// element ref (w- prefix) can dangle — a bare field_name is not an id.
|
|
188
|
+
warnings.push(`event on "${t.from}" action="set_field_value" target="${t.target}" looks like an element id but matches none.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// dangling option-level event targets (specials.options[].events_option promoId)
|
|
192
|
+
for (const t of optionTargets) {
|
|
193
|
+
if (danglesId(t.target)) {
|
|
194
|
+
warnings.push(`option event on "${t.from}" type="${t.kind}" promoId="${t.target}" does not match any element id.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// dangling survey/field cross-wiring
|
|
198
|
+
for (const r of connectRefs) {
|
|
199
|
+
if (danglesId(r.target)) {
|
|
200
|
+
warnings.push(`"${r.from}" specials.${r.key}="${r.target}" does not match any element id.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// field_name uniqueness WITHIN each form — duplicate names collide in the
|
|
204
|
+
// submitted data. (A nested form is its own data scope, so stop at one.)
|
|
205
|
+
const collectFieldNames = (n, acc) => {
|
|
206
|
+
if (!n || !Array.isArray(n.children))
|
|
207
|
+
return;
|
|
208
|
+
for (const c of n.children) {
|
|
209
|
+
if (!c || typeof c !== "object")
|
|
210
|
+
continue;
|
|
211
|
+
if (c.type === "form")
|
|
212
|
+
continue;
|
|
213
|
+
const fn = c.specials?.field_name;
|
|
214
|
+
if (typeof fn === "string" && fn.trim() !== "")
|
|
215
|
+
acc.push(fn.trim());
|
|
216
|
+
collectFieldNames(c, acc);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
for (const form of forms) {
|
|
220
|
+
const names = [];
|
|
221
|
+
collectFieldNames(form, names);
|
|
222
|
+
const counts = new Map();
|
|
223
|
+
for (const fn of names)
|
|
224
|
+
counts.set(fn, (counts.get(fn) || 0) + 1);
|
|
225
|
+
for (const [fn, count] of counts) {
|
|
226
|
+
if (count > 1) {
|
|
227
|
+
warnings.push(`form "${form.id ?? "?"}": field_name "${fn}" used ${count} times — inputs in one form need a unique field_name (data collides on submit).`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
144
230
|
}
|
|
145
231
|
// 3) Layout bounds — flag children that fall off their container's canvas (a
|
|
146
232
|
// common cause of "off-center / misaligned" pages). Warnings only.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-landing-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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": {
|