sbuilder-mcp 0.53.0 → 0.55.0

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/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ All notable changes to this project are documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.55.0] - 2026-09-15
10
+
11
+ ### Added
12
+ - `sb_page_create` now opens an About, policy, or FAQ page with a real layout instead of a blank canvas, matched from the page's `name` against the same keyword table `sb_page_list` already uses to flag a missing one; the result reports `would_open_as` (dry run) or `seeded: { layout, nodes }` (real run) when this applies, and `seed:false` still opts out.
13
+
14
+ ## [0.54.0] - 2026-09-15
15
+
16
+ ### Added
17
+ - `sb_review` reports a `mergedAuthPage` readiness gap when a single page carries two or more account forms (login, register, forgot-password, reset, or verify), since none of them then has an address a header can link to or a password-reset email can point at; the fix names `sb_store action:"form"` with a `page_name` to give each its own page.
18
+
9
19
  ## [0.53.0] - 2026-09-15
10
20
 
11
21
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,16 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
6
6
  Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.55.0] - 2026-09-15
10
+
11
+ ### Added
12
+ - `sb_page_create` giờ mở trang Giới thiệu, chính sách hoặc FAQ với một layout thực sự thay vì canvas trống, được xác định bằng cách so khớp `name` của trang với đúng bảng từ khoá mà `sb_page_list` đã dùng để báo thiếu trang; kết quả trả về `would_open_as` (khi dry run) hoặc `seeded: { layout, nodes }` (khi chạy thật) trong trường hợp này, và `seed:false` vẫn dùng để bỏ qua tính năng này.
13
+
14
+ ## [0.54.0] - 2026-09-15
15
+
16
+ ### Added
17
+ - `sb_review` giờ báo cáo lỗi sẵn sàng `mergedAuthPage` khi một trang duy nhất chứa từ hai form tài khoản trở lên (đăng nhập, đăng ký, quên mật khẩu, đặt lại, hoặc xác thực), vì khi đó không form nào có địa chỉ riêng để header liên kết tới hay để email đặt lại mật khẩu trỏ đến; hướng khắc phục nêu tên `sb_store action:"form"` kèm `page_name` để tách mỗi form ra trang riêng của nó.
18
+
9
19
  ## [0.53.0] - 2026-09-15
10
20
 
11
21
  ### Added
@@ -1,6 +1,6 @@
1
1
  export const DEAD_KEY_SOURCE = {
2
- "files": 3974,
3
- "identifiers": 76582,
2
+ "files": 4247,
3
+ "identifiers": 77158,
4
4
  "seededKeys": 384,
5
5
  "dead": 0
6
6
  };
@@ -4,7 +4,7 @@ export const SHAPE_SOURCE = {
4
4
  "fromHandlers": 175,
5
5
  "fromSwaggerOnly": 0,
6
6
  "withReadOnly": 26,
7
- "structsRead": 1661
7
+ "structsRead": 1671
8
8
  };
9
9
  export const REQUEST_SHAPES = {
10
10
  "post:/api/sites/{siteId}/api-keys": {
@@ -38,6 +38,577 @@ export const COMPLETION_HEADLINE = {
38
38
  * end up looking at the same thing. A type absent here starts blank, which is
39
39
  * the right default for `page` itself.
40
40
  */
41
+ export const PAGE_LAYOUT_SEEDS = {
42
+ "about": {
43
+ "schema_version": 2,
44
+ "root_node_id": "plabo_1",
45
+ "nodes": {
46
+ "plabo_1": {
47
+ "id": "plabo_1",
48
+ "data": {
49
+ "type": "root",
50
+ "parent": null,
51
+ "nodes": [
52
+ "plabo_5"
53
+ ],
54
+ "isCanvas": true,
55
+ "hidden": false,
56
+ "custom": {}
57
+ },
58
+ "style": {},
59
+ "config": {},
60
+ "specials": {},
61
+ "responsive": {},
62
+ "events": [],
63
+ "bindings": []
64
+ },
65
+ "plabo_2": {
66
+ "id": "plabo_2",
67
+ "data": {
68
+ "type": "heading",
69
+ "parent": "plabo_5",
70
+ "nodes": [],
71
+ "isCanvas": false,
72
+ "hidden": false,
73
+ "custom": {}
74
+ },
75
+ "style": {
76
+ "width": "100%",
77
+ "height": "fit-content"
78
+ },
79
+ "config": {
80
+ "textGlobalStyle": "heading-1"
81
+ },
82
+ "specials": {
83
+ "htmlTag": "h2",
84
+ "text": "Heading",
85
+ "stylePreset": "heading-default"
86
+ },
87
+ "responsive": {},
88
+ "events": [],
89
+ "bindings": []
90
+ },
91
+ "plabo_3": {
92
+ "id": "plabo_3",
93
+ "data": {
94
+ "type": "text",
95
+ "parent": "plabo_5",
96
+ "nodes": [],
97
+ "isCanvas": false,
98
+ "hidden": false,
99
+ "custom": {}
100
+ },
101
+ "style": {
102
+ "width": "100%",
103
+ "height": "fit-content"
104
+ },
105
+ "config": {
106
+ "textGlobalStyle": "text-1"
107
+ },
108
+ "specials": {
109
+ "htmlTag": "p",
110
+ "text": "Enter your text here",
111
+ "stylePreset": "text-default"
112
+ },
113
+ "responsive": {},
114
+ "events": [],
115
+ "bindings": []
116
+ },
117
+ "plabo_4": {
118
+ "id": "plabo_4",
119
+ "data": {
120
+ "type": "image",
121
+ "parent": "plabo_5",
122
+ "nodes": [],
123
+ "isCanvas": false,
124
+ "hidden": false,
125
+ "custom": {}
126
+ },
127
+ "style": {
128
+ "width": "100%",
129
+ "height": "fit-content"
130
+ },
131
+ "config": {
132
+ "imageRatio": "auto",
133
+ "customImageRatioWidth": 16,
134
+ "customImageRatioHeight": 9,
135
+ "imageEager": false
136
+ },
137
+ "specials": {
138
+ "src": "",
139
+ "alt": "",
140
+ "stylePreset": "image-default"
141
+ },
142
+ "responsive": {},
143
+ "events": [],
144
+ "bindings": []
145
+ },
146
+ "plabo_5": {
147
+ "id": "plabo_5",
148
+ "data": {
149
+ "type": "flex-section",
150
+ "parent": "plabo_1",
151
+ "nodes": [
152
+ "plabo_2",
153
+ "plabo_3",
154
+ "plabo_4"
155
+ ],
156
+ "isCanvas": true,
157
+ "hidden": false,
158
+ "custom": {}
159
+ },
160
+ "style": {
161
+ "display": "flex",
162
+ "flexDirection": "column",
163
+ "width": "100%",
164
+ "height": "fit-content",
165
+ "gap": "24px",
166
+ "paddingTop": "48px",
167
+ "paddingBottom": "48px"
168
+ },
169
+ "config": {
170
+ "backgroundSceneSource": "",
171
+ "backgroundSceneEffect": "gradient-mesh",
172
+ "backgroundSceneGallery": "podium",
173
+ "backgroundSceneSpeed": "normal",
174
+ "backgroundSceneIntensity": "normal",
175
+ "backgroundSceneColors": "theme",
176
+ "backgroundSceneColor1": "#171717",
177
+ "backgroundSceneColor2": "#ffffff",
178
+ "backgroundSceneColor3": "#6b7280",
179
+ "backgroundSceneUrl": "",
180
+ "backgroundSceneModelUrl": "",
181
+ "backgroundSceneVeil": 0,
182
+ "backgroundSceneVeilColor": "#000000"
183
+ },
184
+ "specials": {
185
+ "stylePreset": "container-section"
186
+ },
187
+ "responsive": {},
188
+ "events": [],
189
+ "bindings": []
190
+ }
191
+ }
192
+ },
193
+ "policy": {
194
+ "schema_version": 2,
195
+ "root_node_id": "plpol_1",
196
+ "nodes": {
197
+ "plpol_1": {
198
+ "id": "plpol_1",
199
+ "data": {
200
+ "type": "root",
201
+ "parent": null,
202
+ "nodes": [
203
+ "plpol_4"
204
+ ],
205
+ "isCanvas": true,
206
+ "hidden": false,
207
+ "custom": {}
208
+ },
209
+ "style": {},
210
+ "config": {},
211
+ "specials": {},
212
+ "responsive": {},
213
+ "events": [],
214
+ "bindings": []
215
+ },
216
+ "plpol_2": {
217
+ "id": "plpol_2",
218
+ "data": {
219
+ "type": "heading",
220
+ "parent": "plpol_4",
221
+ "nodes": [],
222
+ "isCanvas": false,
223
+ "hidden": false,
224
+ "custom": {}
225
+ },
226
+ "style": {
227
+ "width": "100%",
228
+ "height": "fit-content"
229
+ },
230
+ "config": {
231
+ "textGlobalStyle": "heading-1"
232
+ },
233
+ "specials": {
234
+ "htmlTag": "h2",
235
+ "text": "Heading",
236
+ "stylePreset": "heading-default"
237
+ },
238
+ "responsive": {},
239
+ "events": [],
240
+ "bindings": []
241
+ },
242
+ "plpol_3": {
243
+ "id": "plpol_3",
244
+ "data": {
245
+ "type": "text",
246
+ "parent": "plpol_4",
247
+ "nodes": [],
248
+ "isCanvas": false,
249
+ "hidden": false,
250
+ "custom": {}
251
+ },
252
+ "style": {
253
+ "width": "100%",
254
+ "height": "fit-content"
255
+ },
256
+ "config": {
257
+ "textGlobalStyle": "text-1"
258
+ },
259
+ "specials": {
260
+ "htmlTag": "p",
261
+ "text": "Enter your text here",
262
+ "stylePreset": "text-default"
263
+ },
264
+ "responsive": {},
265
+ "events": [],
266
+ "bindings": []
267
+ },
268
+ "plpol_4": {
269
+ "id": "plpol_4",
270
+ "data": {
271
+ "type": "flex-section",
272
+ "parent": "plpol_1",
273
+ "nodes": [
274
+ "plpol_2",
275
+ "plpol_3"
276
+ ],
277
+ "isCanvas": true,
278
+ "hidden": false,
279
+ "custom": {}
280
+ },
281
+ "style": {
282
+ "display": "flex",
283
+ "flexDirection": "column",
284
+ "width": "100%",
285
+ "height": "fit-content",
286
+ "gap": "24px",
287
+ "paddingTop": "48px",
288
+ "paddingBottom": "48px"
289
+ },
290
+ "config": {
291
+ "backgroundSceneSource": "",
292
+ "backgroundSceneEffect": "gradient-mesh",
293
+ "backgroundSceneGallery": "podium",
294
+ "backgroundSceneSpeed": "normal",
295
+ "backgroundSceneIntensity": "normal",
296
+ "backgroundSceneColors": "theme",
297
+ "backgroundSceneColor1": "#171717",
298
+ "backgroundSceneColor2": "#ffffff",
299
+ "backgroundSceneColor3": "#6b7280",
300
+ "backgroundSceneUrl": "",
301
+ "backgroundSceneModelUrl": "",
302
+ "backgroundSceneVeil": 0,
303
+ "backgroundSceneVeilColor": "#000000"
304
+ },
305
+ "specials": {
306
+ "stylePreset": "container-section"
307
+ },
308
+ "responsive": {},
309
+ "events": [],
310
+ "bindings": []
311
+ }
312
+ }
313
+ },
314
+ "faq": {
315
+ "schema_version": 2,
316
+ "root_node_id": "plfaq_1",
317
+ "nodes": {
318
+ "plfaq_1": {
319
+ "id": "plfaq_1",
320
+ "data": {
321
+ "type": "root",
322
+ "parent": null,
323
+ "nodes": [
324
+ "plfaq_8"
325
+ ],
326
+ "isCanvas": true,
327
+ "hidden": false,
328
+ "custom": {}
329
+ },
330
+ "style": {},
331
+ "config": {},
332
+ "specials": {},
333
+ "responsive": {},
334
+ "events": [],
335
+ "bindings": []
336
+ },
337
+ "plfaq_2": {
338
+ "id": "plfaq_2",
339
+ "data": {
340
+ "type": "heading",
341
+ "parent": "plfaq_8",
342
+ "nodes": [],
343
+ "isCanvas": false,
344
+ "hidden": false,
345
+ "custom": {}
346
+ },
347
+ "style": {
348
+ "width": "100%",
349
+ "height": "fit-content"
350
+ },
351
+ "config": {
352
+ "textGlobalStyle": "heading-1"
353
+ },
354
+ "specials": {
355
+ "htmlTag": "h2",
356
+ "text": "Heading",
357
+ "stylePreset": "heading-default"
358
+ },
359
+ "responsive": {},
360
+ "events": [],
361
+ "bindings": []
362
+ },
363
+ "plfaq_3": {
364
+ "id": "plfaq_3",
365
+ "data": {
366
+ "type": "accordion",
367
+ "parent": "plfaq_8",
368
+ "nodes": [
369
+ "plfaq_4",
370
+ "plfaq_5",
371
+ "plfaq_6"
372
+ ],
373
+ "isCanvas": true,
374
+ "hidden": false,
375
+ "custom": {}
376
+ },
377
+ "style": {
378
+ "display": "block",
379
+ "width": "100%",
380
+ "height": "fit-content",
381
+ "padding": "0px",
382
+ "margin": "0px"
383
+ },
384
+ "config": {
385
+ "openItems": [
386
+ 0
387
+ ],
388
+ "accordionItemId": "plfaq_7"
389
+ },
390
+ "specials": {},
391
+ "responsive": {},
392
+ "events": [],
393
+ "bindings": []
394
+ },
395
+ "plfaq_4": {
396
+ "id": "plfaq_4",
397
+ "data": {
398
+ "type": "accordion-content",
399
+ "parent": "plfaq_3",
400
+ "nodes": [],
401
+ "isCanvas": true,
402
+ "hidden": false,
403
+ "custom": {}
404
+ },
405
+ "style": {
406
+ "display": "flex",
407
+ "flexDirection": "column",
408
+ "justifyContent": "flex-start",
409
+ "alignItems": "flex-start",
410
+ "minWidth": "0px",
411
+ "width": "100%",
412
+ "height": "fit-content",
413
+ "gap": "16px",
414
+ "padding": "16px",
415
+ "margin": "0px"
416
+ },
417
+ "config": {},
418
+ "specials": {
419
+ "label": "Accordion 1"
420
+ },
421
+ "responsive": {
422
+ "tablet": {
423
+ "style": {
424
+ "padding": "14px"
425
+ }
426
+ },
427
+ "mobile": {
428
+ "style": {
429
+ "padding": "12px"
430
+ }
431
+ }
432
+ },
433
+ "events": [],
434
+ "bindings": []
435
+ },
436
+ "plfaq_5": {
437
+ "id": "plfaq_5",
438
+ "data": {
439
+ "type": "accordion-content",
440
+ "parent": "plfaq_3",
441
+ "nodes": [],
442
+ "isCanvas": true,
443
+ "hidden": false,
444
+ "custom": {}
445
+ },
446
+ "style": {
447
+ "display": "flex",
448
+ "flexDirection": "column",
449
+ "justifyContent": "flex-start",
450
+ "alignItems": "flex-start",
451
+ "minWidth": "0px",
452
+ "width": "100%",
453
+ "height": "fit-content",
454
+ "gap": "16px",
455
+ "padding": "16px",
456
+ "margin": "0px"
457
+ },
458
+ "config": {},
459
+ "specials": {
460
+ "label": "Accordion 2"
461
+ },
462
+ "responsive": {
463
+ "tablet": {
464
+ "style": {
465
+ "padding": "14px"
466
+ }
467
+ },
468
+ "mobile": {
469
+ "style": {
470
+ "padding": "12px"
471
+ }
472
+ }
473
+ },
474
+ "events": [],
475
+ "bindings": []
476
+ },
477
+ "plfaq_6": {
478
+ "id": "plfaq_6",
479
+ "data": {
480
+ "type": "accordion-content",
481
+ "parent": "plfaq_3",
482
+ "nodes": [],
483
+ "isCanvas": true,
484
+ "hidden": false,
485
+ "custom": {}
486
+ },
487
+ "style": {
488
+ "display": "flex",
489
+ "flexDirection": "column",
490
+ "justifyContent": "flex-start",
491
+ "alignItems": "flex-start",
492
+ "minWidth": "0px",
493
+ "width": "100%",
494
+ "height": "fit-content",
495
+ "gap": "16px",
496
+ "padding": "16px",
497
+ "margin": "0px"
498
+ },
499
+ "config": {},
500
+ "specials": {
501
+ "label": "Accordion 3"
502
+ },
503
+ "responsive": {
504
+ "tablet": {
505
+ "style": {
506
+ "padding": "14px"
507
+ }
508
+ },
509
+ "mobile": {
510
+ "style": {
511
+ "padding": "12px"
512
+ }
513
+ }
514
+ },
515
+ "events": [],
516
+ "bindings": []
517
+ },
518
+ "plfaq_7": {
519
+ "id": "plfaq_7",
520
+ "data": {
521
+ "type": "accordion-item",
522
+ "parent": "plfaq_3",
523
+ "nodes": [],
524
+ "isCanvas": false,
525
+ "hidden": false,
526
+ "custom": {}
527
+ },
528
+ "style": {
529
+ "backgroundColor": "#f5f5f5",
530
+ "color": "#171717",
531
+ "textAlign": "left",
532
+ "fontFamily": "Inter",
533
+ "fontSize": "14px",
534
+ "padding": "12px 16px"
535
+ },
536
+ "config": {
537
+ "textGlobalStyle": "text-2"
538
+ },
539
+ "specials": {},
540
+ "responsive": {
541
+ "mobile": {
542
+ "style": {
543
+ "padding": "12px"
544
+ }
545
+ }
546
+ },
547
+ "states": {
548
+ "hover": {
549
+ "style": {
550
+ "backgroundColor": "#ebebeb"
551
+ }
552
+ }
553
+ },
554
+ "events": [],
555
+ "bindings": []
556
+ },
557
+ "plfaq_8": {
558
+ "id": "plfaq_8",
559
+ "data": {
560
+ "type": "flex-section",
561
+ "parent": "plfaq_1",
562
+ "nodes": [
563
+ "plfaq_2",
564
+ "plfaq_3"
565
+ ],
566
+ "isCanvas": true,
567
+ "hidden": false,
568
+ "custom": {}
569
+ },
570
+ "style": {
571
+ "display": "flex",
572
+ "flexDirection": "column",
573
+ "width": "100%",
574
+ "height": "fit-content",
575
+ "gap": "24px",
576
+ "paddingTop": "48px",
577
+ "paddingBottom": "48px"
578
+ },
579
+ "config": {
580
+ "backgroundSceneSource": "",
581
+ "backgroundSceneEffect": "gradient-mesh",
582
+ "backgroundSceneGallery": "podium",
583
+ "backgroundSceneSpeed": "normal",
584
+ "backgroundSceneIntensity": "normal",
585
+ "backgroundSceneColors": "theme",
586
+ "backgroundSceneColor1": "#171717",
587
+ "backgroundSceneColor2": "#ffffff",
588
+ "backgroundSceneColor3": "#6b7280",
589
+ "backgroundSceneUrl": "",
590
+ "backgroundSceneModelUrl": "",
591
+ "backgroundSceneVeil": 0,
592
+ "backgroundSceneVeilColor": "#000000"
593
+ },
594
+ "specials": {
595
+ "stylePreset": "container-section"
596
+ },
597
+ "responsive": {},
598
+ "events": [],
599
+ "bindings": []
600
+ }
601
+ }
602
+ }
603
+ };
604
+ /**
605
+ * The document an ordinary page opens with for a chosen LAYOUT.
606
+ *
607
+ * Keyed by purpose, not by type: every content page is type `page`, so the
608
+ * table above cannot tell an About page from a policy. Same palette cards, same
609
+ * module, same reason — an author who picks the layout in the editor and an
610
+ * agent that asks for it here must land on the same page.
611
+ */
41
612
  export const STORE_PAGE_SEEDS = {
42
613
  "product": {
43
614
  "schema_version": 2,
@@ -53,6 +53,7 @@ export const USUAL_PAGES = [
53
53
  key: 'about',
54
54
  match: ['about', 'gioi-thieu', 'giới thiệu', 've-chung-toi'],
55
55
  why: 'Nothing says who the shop is, which is the page a first-time buyer opens before paying.',
56
+ layout: 'about',
56
57
  },
57
58
  // TWO POLICIES, NOT ONE BUCKET. These were a single entry, and it read a shop
58
59
  // carrying only "Chính sách giao hàng & đổi trả" as complete — which is
@@ -67,6 +68,7 @@ export const USUAL_PAGES = [
67
68
  ],
68
69
  why: 'No delivery or return terms a shopper can read before paying. This is the page a buyer ' +
69
70
  'looks for when the parcel is late and the one a dispute is settled against.',
71
+ layout: 'policy',
70
72
  },
71
73
  {
72
74
  key: 'policy-privacy',
@@ -76,12 +78,14 @@ export const USUAL_PAGES = [
76
78
  ],
77
79
  why: 'No privacy policy or terms of use. Payment providers and marketplaces ask for both ' +
78
80
  'before they will list a shop, and a checkout form collects personal data either way.',
81
+ layout: 'policy',
79
82
  },
80
83
  {
81
84
  key: 'faq',
82
85
  match: ['faq', 'cau-hoi', 'câu hỏi', 'hoi-dap', 'hỏi đáp', 'help', 'tro-giup', 'trợ giúp'],
83
86
  why: 'The same handful of questions reach support one message at a time, with no page to link ' +
84
87
  'an answer to. The accordion element is what this page is built from.',
88
+ layout: 'faq',
85
89
  },
86
90
  {
87
91
  key: 'blog',
@@ -113,3 +117,21 @@ export function missingUsualPages(pages) {
113
117
  .join('\n');
114
118
  return USUAL_PAGES.filter((u) => (u.when ? u.when(pages) : true) && !u.match.some((m) => hay.includes(m)));
115
119
  }
120
+ /**
121
+ * The layout a page called `name` should open with, or undefined.
122
+ *
123
+ * READ OFF THE SAME KEYWORDS that decide a page is missing, so the two answers
124
+ * cannot drift: a name this module reads as "the about page" is the name that
125
+ * gets the about layout. That is the whole reason it lives here rather than in
126
+ * a second table beside sb_page_create.
127
+ *
128
+ * ONLY FOR TYPE `page`. Every other type already opens with the seed its own
129
+ * type describes, and a name-based guess on top of it would contradict a
130
+ * decision the platform has already made.
131
+ */
132
+ export function layoutForPageName(name, type) {
133
+ if (type && type !== 'page')
134
+ return undefined;
135
+ const hay = name.toLowerCase();
136
+ return USUAL_PAGES.find((u) => u.layout && u.match.some((m) => hay.includes(m)))?.layout;
137
+ }
@@ -24,7 +24,7 @@ export async function gatherReadiness(ctx, siteId, pageNodes) {
24
24
  }
25
25
  };
26
26
  const site = encodeURIComponent(siteId);
27
- const [pageList, gateways, shipping, globals, productList, categoryList, pageLinks] = await Promise.all([
27
+ const [pageList, gateways, shipping, globals, productList, categoryList, pageLinks, formList] = await Promise.all([
28
28
  get(`/api/sites/${site}/pages`),
29
29
  get(`/api/sites/${site}/payment-gateways`),
30
30
  get(`/api/sites/${site}/shipping-methods`),
@@ -42,6 +42,12 @@ export async function gatherReadiness(ctx, siteId, pageNodes) {
42
42
  // right, and the rest list the whole catalogue.
43
43
  get(`/api/sites/${site}/product-categories`),
44
44
  get(`/api/sites/${site}/page-links`),
45
+ // THE FORMS, BY TYPE. A page document carries only `specials.formId` — which
46
+ // form a node shows, never what KIND of form it is — so nothing reading a
47
+ // page can tell a login form from a register form without this list. That
48
+ // blindness is what let one page quietly become the site's whole account
49
+ // area, which is the shape `mergedAuthPage` reports.
50
+ get(`/api/sites/${site}/forms`),
45
51
  ]);
46
52
  // A gateway counts only when it is BOTH enabled and configured — the editor's
47
53
  // `live` getter also filters by the store's currency, which is a narrowing:
@@ -87,5 +93,6 @@ export async function gatherReadiness(ctx, siteId, pageNodes) {
87
93
  globalKinds,
88
94
  categories,
89
95
  categoryPageLinks,
96
+ forms: formList?.forms ?? null,
90
97
  };
91
98
  }
@@ -132,6 +132,52 @@ export function readinessGaps(input) {
132
132
  'footer and a link back to the home page.',
133
133
  });
134
134
  }
135
+ // ONE PAGE QUIETLY BECAME THE WHOLE ACCOUNT AREA.
136
+ //
137
+ // This server used to tell agents, in as many words, to put the login and the
138
+ // register form behind a member-gate on /account. They did. The result is a
139
+ // page carrying two or three different jobs, and — the part that is not a
140
+ // matter of taste — NEITHER JOB HAS AN ADDRESS. A header can link to one
141
+ // thing, an email that says "reset your password" has nowhere specific to
142
+ // point, and a visitor who came to register meets a login form first because
143
+ // that is what was stacked on top.
144
+ //
145
+ // The rule was fixed; the pages it already built were not, and nothing could
146
+ // see them: a page document carries `specials.formId` and never the KIND of
147
+ // form, so only the site's own form list can tell these apart.
148
+ //
149
+ // COUNTED BY DISTINCT TYPE, not by how many form nodes there are. A form split
150
+ // across segments is several nodes of ONE type and is not this defect.
151
+ const authTypes = new Set(['login', 'register', 'forgot', 'reset', 'verify']);
152
+ if (input.forms && input.forms.length > 0) {
153
+ const typeOf = new Map(input.forms
154
+ .filter((f) => !!f?.id && !!f?.type)
155
+ .map((f) => [f.id, f.type]));
156
+ const onPage = new Set();
157
+ for (const n of input.pageNodes) {
158
+ if (n.data.type !== 'form')
159
+ continue;
160
+ const id = n.specials?.formId;
161
+ const t = typeof id === 'string' ? typeOf.get(id) : undefined;
162
+ if (t && authTypes.has(t))
163
+ onPage.add(t);
164
+ }
165
+ if (onPage.size >= 2) {
166
+ const named = [...onPage].sort().join(', ');
167
+ gaps.push({
168
+ id: 'mergedAuthPage',
169
+ draft: false,
170
+ problem: `This page carries ${onPage.size} different account forms (${named}). Neither has an ` +
171
+ 'address of its own, so a header can link to only one of them, a password-reset mail ' +
172
+ 'has nowhere specific to point, and whoever came to do the second thing meets the ' +
173
+ 'first one stacked on top.',
174
+ fix: 'Give each its own page: sb_store action:"form" with template "login", "register" or ' +
175
+ '"forgot" and a page_name builds the form AND the page in one call. Leave /account ' +
176
+ 'showing the profile behind a member-gate with audience "members", and a short ' +
177
+ 'sign-in prompt LINKING to the login page behind audience "guests".',
178
+ });
179
+ }
180
+ }
135
181
  if (!isStore(input))
136
182
  return gaps;
137
183
  if (pages && !published(pages, 'checkout')) {
@@ -1,4 +1,4 @@
1
- import { COMPLETION_HEADLINE, COMPLETION_HEADLINE_SENTINEL, STORE_PAGE_SEEDS, } from '../../catalog/storepages.generated.js';
1
+ import { COMPLETION_HEADLINE, COMPLETION_HEADLINE_SENTINEL, PAGE_LAYOUT_SEEDS, STORE_PAGE_SEEDS, } from '../../catalog/storepages.generated.js';
2
2
  /**
3
3
  * WHAT A NEW STORE PAGE OPENS WITH — for an agent, as for a merchant.
4
4
  *
@@ -58,3 +58,23 @@ export function seedSummary(type) {
58
58
  const seed = STORE_PAGE_SEEDS[type];
59
59
  return seed ? { nodes: Object.keys(seed.nodes).length } : null;
60
60
  }
61
+ /**
62
+ * The document an ordinary page opens with for a chosen LAYOUT, or null.
63
+ *
64
+ * Separate from `seedDocument` because the keys are different things: that one
65
+ * is keyed by the page TYPE the storefront routes by, this one by the PURPOSE an
66
+ * author picked. Every content page is type `page`, so the first table cannot
67
+ * tell an About page from a policy and this one can.
68
+ *
69
+ * Same palette cards, same editor module, read through the same codegen — so a
70
+ * merchant who picks the layout in the create dialog and an agent that lands on
71
+ * it here open the same page.
72
+ */
73
+ export function layoutDocument(layout) {
74
+ const seed = PAGE_LAYOUT_SEEDS[layout];
75
+ return seed ? JSON.parse(JSON.stringify(seed)) : null;
76
+ }
77
+ /** The layouts this build carries. */
78
+ export function layoutNames() {
79
+ return Object.keys(PAGE_LAYOUT_SEEDS);
80
+ }
@@ -7,7 +7,7 @@ import { addSubtree, setMany, baseOnlyKeys, moveNode, removeNode, duplicateNode,
7
7
  import { baseOnlyNote } from '../domains/site/baseonly.js';
8
8
  import { detachNote, presetIdOf, presetLayer } from '../domains/site/theme.js';
9
9
  import { inertHintsFor } from '../domains/site/inert.js';
10
- import { hasSeed, seedDocument, seedSummary, seededTypes } from '../domains/site/storepage.js';
10
+ import { hasSeed, layoutDocument, seedDocument, seedSummary, seededTypes } from '../domains/site/storepage.js';
11
11
  import { animationNote, deadKeyNote, unknownValueNote, unknownWriteNote, } from '../domains/site/vocabulary.js';
12
12
  import { skinLevelNote } from '../domains/site/fieldskin.js';
13
13
  import { siteTheme } from '../domains/site/theme-fetch.js';
@@ -20,7 +20,7 @@ import { readinessGaps, READINESS_NOTICE } from '../domains/site/readiness.js';
20
20
  import { gatherReadiness } from '../domains/site/readiness-fetch.js';
21
21
  import { globalWarning, restampPatches, RESPONSIVE_NOTICE } from '../domains/site/traps.js';
22
22
  import { catalogBrowse, catalogMatches, traitsFor } from '../catalog/element-search.js';
23
- import { missingUsualPages } from '../domains/site/inventory.js';
23
+ import { layoutForPageName, missingUsualPages } from '../domains/site/inventory.js';
24
24
  import { LAYOUT_PATTERNS, PATTERN_BY_ID, THEME_TOKENS, } from '../domains/site/patterns.js';
25
25
  import { tokensFromPage } from '../domains/site/importmap.js';
26
26
  import { middleEnd } from '../domains/site/traps.js';
@@ -1231,6 +1231,19 @@ export function registerPageTools(server, ctx) {
1231
1231
  // been seeding, so a caller planning a build needs to know before it
1232
1232
  // commits whether it is about to hand-assemble a buy box that already
1233
1233
  // exists.
1234
+ // AN ORDINARY PAGE OPENS AS SOMETHING TOO, decided from its NAME.
1235
+ //
1236
+ // `hasSeed` is keyed by page TYPE, and every content page is type "page",
1237
+ // so About, a policy and an FAQ all arrived blank — and were then
1238
+ // hand-assembled out of the same three primitives, which is why they all
1239
+ // came out looking the same.
1240
+ //
1241
+ // The name is the signal because it is the only one there is, and reusing
1242
+ // the keyword table that already decides a page is MISSING is what stops
1243
+ // the two answers drifting apart: a name sb_page_list reads as "the about
1244
+ // page" is the name that gets the about layout. `seed:false` opts out, as
1245
+ // it always has.
1246
+ const layout = seed !== false ? layoutForPageName(name, type) : undefined;
1234
1247
  const willSeed = seed !== false && hasSeed(type);
1235
1248
  const summary = willSeed && type ? seedSummary(type) : null;
1236
1249
  // WHAT MAKES THE NEW PAGE PART OF THE SITE.
@@ -1327,6 +1340,7 @@ export function registerPageTools(server, ctx) {
1327
1340
  would_post: path,
1328
1341
  body: redact(body),
1329
1342
  ...(summary ? { would_seed: { type, ...summary } } : {}),
1343
+ ...(layout ? { would_open_as: layout } : {}),
1330
1344
  ...(wear.header || wear.footer
1331
1345
  ? { would_wear: { ...(wear.header ? { header: wear.header } : {}), ...(wear.footer ? { footer: wear.footer } : {}) } }
1332
1346
  : {}),
@@ -1355,8 +1369,8 @@ export function registerPageTools(server, ctx) {
1355
1369
  // unwinding a page the caller asked for.
1356
1370
  let seeded = null;
1357
1371
  const newId = res.page?.id;
1358
- if (willSeed && type && typeof newId === 'string' && newId) {
1359
- const document = seedDocument(type, { locale, headline });
1372
+ if ((willSeed || layout) && typeof newId === 'string' && newId) {
1373
+ const document = layout ? layoutDocument(layout) : seedDocument(type ?? '', { locale, headline });
1360
1374
  if (document) {
1361
1375
  try {
1362
1376
  await request({
@@ -1369,7 +1383,9 @@ export function registerPageTools(server, ctx) {
1369
1383
  body: { document, schemaVersion: document.schema_version },
1370
1384
  fetchImpl: ctx.fetchImpl,
1371
1385
  });
1372
- seeded = { type, nodes: Object.keys(document.nodes).length };
1386
+ seeded = layout
1387
+ ? { layout, nodes: Object.keys(document.nodes).length }
1388
+ : { type, nodes: Object.keys(document.nodes).length };
1373
1389
  }
1374
1390
  catch (e) {
1375
1391
  seeded = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
5
5
  "mcpName": "io.github.vuluu2k/sbuilder-mcp",
6
6
  "type": "module",