sbuilder-mcp 0.2.2 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  export const ELEMENT_SOURCE = {
2
- "count": 106,
2
+ "count": 107,
3
3
  "docSchemaVersion": 2
4
4
  };
5
5
  export const ELEMENTS = {
@@ -670,6 +670,34 @@ export const ELEMENTS = {
670
670
  "locked": false,
671
671
  "hideInLayer": false,
672
672
  "childAllows": [],
673
+ "events": {
674
+ "click": [
675
+ "go_to_url",
676
+ "open_page",
677
+ "popup",
678
+ "open_menu",
679
+ "close_menu",
680
+ "toggle_menu",
681
+ "open_cart",
682
+ "close_cart",
683
+ "go_to_checkout",
684
+ "cart_remove",
685
+ "cart_increment",
686
+ "cart_decrement",
687
+ "change_tab",
688
+ "open_dropdown",
689
+ "close_dropdown",
690
+ "toggle_dropdown",
691
+ "logout_customer",
692
+ "toggle_wishlist"
693
+ ]
694
+ },
695
+ "bindingEvents": {
696
+ "click": [
697
+ "open_cart",
698
+ "go_to_checkout"
699
+ ]
700
+ },
673
701
  "defaults": {
674
702
  "specials": {
675
703
  "text": "Button",
@@ -878,6 +906,17 @@ export const ELEMENTS = {
878
906
  "locked": false,
879
907
  "hideInLayer": false,
880
908
  "childAllows": [],
909
+ "events": {
910
+ "click": [
911
+ "go_to_url",
912
+ "open_page",
913
+ "popup",
914
+ "change_tab",
915
+ "open_dropdown",
916
+ "close_dropdown",
917
+ "toggle_dropdown"
918
+ ]
919
+ },
881
920
  "defaults": {
882
921
  "specials": {
883
922
  "src": "",
@@ -1025,6 +1064,28 @@ export const ELEMENTS = {
1025
1064
  "locked": false,
1026
1065
  "hideInLayer": false,
1027
1066
  "childAllows": [],
1067
+ "events": {
1068
+ "click": [
1069
+ "go_to_url",
1070
+ "open_page",
1071
+ "popup",
1072
+ "open_menu",
1073
+ "close_menu",
1074
+ "toggle_menu",
1075
+ "open_cart",
1076
+ "close_cart",
1077
+ "go_to_checkout",
1078
+ "cart_remove",
1079
+ "cart_increment",
1080
+ "cart_decrement",
1081
+ "change_tab",
1082
+ "open_dropdown",
1083
+ "close_dropdown",
1084
+ "toggle_dropdown",
1085
+ "logout_customer",
1086
+ "toggle_wishlist"
1087
+ ]
1088
+ },
1028
1089
  "defaults": {
1029
1090
  "specials": {
1030
1091
  "name": "StarFill",
@@ -3984,6 +4045,170 @@ export const ELEMENTS = {
3984
4045
  "signed-in"
3985
4046
  ]
3986
4047
  },
4048
+ "order-receipt": {
4049
+ "type": "order-receipt",
4050
+ "label": "Order receipt",
4051
+ "category": "advanced",
4052
+ "isContainer": false,
4053
+ "isRootOnly": false,
4054
+ "locked": false,
4055
+ "hideInLayer": false,
4056
+ "childAllows": [],
4057
+ "defaults": {
4058
+ "specials": {
4059
+ "title": "Chi tiết đơn hàng",
4060
+ "numberLabel": "Mã đơn hàng",
4061
+ "placedLabel": "Ngày đặt",
4062
+ "statusLabel": "Trạng thái",
4063
+ "itemsLabel": "Sản phẩm",
4064
+ "totalLabel": "Tổng cộng",
4065
+ "dueLabel": "Còn phải trả",
4066
+ "unavailableText": "Không tải được chi tiết đơn hàng. Bạn có thể tra cứu bằng mã đơn hàng trong trang tài khoản."
4067
+ },
4068
+ "style": {
4069
+ "width": "100%"
4070
+ }
4071
+ },
4072
+ "inspector": [
4073
+ {
4074
+ "tab": "general",
4075
+ "groups": [
4076
+ {
4077
+ "key": "receipt_content",
4078
+ "label": "Content",
4079
+ "controls": [
4080
+ "receipt_title",
4081
+ "receipt_number_label",
4082
+ "receipt_placed_label",
4083
+ "receipt_status_label",
4084
+ "receipt_items_label"
4085
+ ]
4086
+ },
4087
+ {
4088
+ "key": "receipt_totals",
4089
+ "label": "Totals",
4090
+ "controls": [
4091
+ "receipt_total_label",
4092
+ "receipt_due_label"
4093
+ ]
4094
+ },
4095
+ {
4096
+ "key": "receipt_unavailable",
4097
+ "label": "Unavailable",
4098
+ "controls": [
4099
+ "receipt_unavailable_text"
4100
+ ]
4101
+ },
4102
+ {
4103
+ "key": "size",
4104
+ "label": "Size",
4105
+ "controls": [
4106
+ "width_select",
4107
+ "height_select",
4108
+ "size_bounds"
4109
+ ]
4110
+ },
4111
+ {
4112
+ "key": "background",
4113
+ "label": "Background",
4114
+ "controls": [
4115
+ "bg_color",
4116
+ "bg_image"
4117
+ ]
4118
+ },
4119
+ {
4120
+ "key": "shape",
4121
+ "label": "Shape",
4122
+ "controls": [
4123
+ "border",
4124
+ "corner",
4125
+ "shadow"
4126
+ ]
4127
+ }
4128
+ ]
4129
+ },
4130
+ {
4131
+ "tab": "advanced",
4132
+ "groups": [
4133
+ {
4134
+ "key": "spacing",
4135
+ "label": "Spacing",
4136
+ "controls": [
4137
+ "padding_margin"
4138
+ ]
4139
+ },
4140
+ {
4141
+ "key": "display",
4142
+ "label": "Display",
4143
+ "controls": [
4144
+ "display"
4145
+ ]
4146
+ },
4147
+ {
4148
+ "key": "animation",
4149
+ "label": "Animation",
4150
+ "controls": [
4151
+ "animation"
4152
+ ]
4153
+ },
4154
+ {
4155
+ "key": "class_css",
4156
+ "label": "Class",
4157
+ "controls": [
4158
+ "class_css"
4159
+ ]
4160
+ }
4161
+ ]
4162
+ }
4163
+ ],
4164
+ "controls": [
4165
+ "receipt_title",
4166
+ "receipt_number_label",
4167
+ "receipt_placed_label",
4168
+ "receipt_status_label",
4169
+ "receipt_items_label",
4170
+ "receipt_total_label",
4171
+ "receipt_due_label",
4172
+ "receipt_unavailable_text",
4173
+ "width_select",
4174
+ "height_select",
4175
+ "size_bounds",
4176
+ "bg_color",
4177
+ "bg_image",
4178
+ "border",
4179
+ "corner",
4180
+ "shadow",
4181
+ "padding_margin",
4182
+ "display",
4183
+ "animation",
4184
+ "class_css"
4185
+ ],
4186
+ "description": "Shows the order a shopper has just paid for on a checkout completion page: order number, date, status, the items, the total, and anything still owed. Reads one order through the signed receipt grant the payment return endpoint puts in the completion URL, so it works for a guest who has no account.",
4187
+ "useWhen": [
4188
+ "Building the checkout completion page a shopper lands on after paying at a gateway.",
4189
+ "The store takes card or wallet payments and wants the buyer to see what they bought without signing in.",
4190
+ "Pairing with payment-status, which reports the MONEY; this reports the ORDER."
4191
+ ],
4192
+ "avoidWhen": [
4193
+ "On any page other than the completion page — every other page is reached without the signed grant, so the block hides itself and renders nothing.",
4194
+ "As an order history list: this shows exactly one order. Use order-history for a signed-in shopper’s past orders.",
4195
+ "To show a home address or per-line prices — the receipt read is deliberately reduced and carries neither."
4196
+ ],
4197
+ "contentTips": [
4198
+ "Word the labels the way the store talks about orders; every string here is yours, not the platform’s.",
4199
+ "The \"still owed\" row prints only when something is actually outstanding, so its label can say so plainly.",
4200
+ "The unavailable sentence is read by someone whose link has expired — point them at where the order really lives."
4201
+ ],
4202
+ "semantics": [
4203
+ "order",
4204
+ "receipt",
4205
+ "checkout",
4206
+ "completion",
4207
+ "confirmation",
4208
+ "ecommerce",
4209
+ "guest"
4210
+ ]
4211
+ },
3987
4212
  "course-outline": {
3988
4213
  "type": "course-outline",
3989
4214
  "label": "Course Syllabus",
@@ -5576,6 +5801,29 @@ export const ELEMENTS = {
5576
5801
  "locked": false,
5577
5802
  "hideInLayer": false,
5578
5803
  "childAllows": [],
5804
+ "events": {
5805
+ "form:submit": [
5806
+ "popup",
5807
+ "close_popup",
5808
+ "open_cart",
5809
+ "close_cart"
5810
+ ],
5811
+ "form:success": [
5812
+ "go_to_url",
5813
+ "open_page",
5814
+ "popup",
5815
+ "close_popup",
5816
+ "open_cart",
5817
+ "close_cart",
5818
+ "go_to_checkout"
5819
+ ],
5820
+ "form:error": [
5821
+ "popup",
5822
+ "close_popup",
5823
+ "open_cart",
5824
+ "close_cart"
5825
+ ]
5826
+ },
5579
5827
  "defaults": {
5580
5828
  "specials": {
5581
5829
  "formId": "",
@@ -5716,7 +5964,7 @@ export const ELEMENTS = {
5716
5964
  "avoidWhen": [
5717
5965
  "A single button or link is all that is needed — a form asks the visitor to type, which is a much larger request than a click.",
5718
5966
  "The data belongs to an existing flow that already has its own surface (adding to the cart, checking out).",
5719
- "Sign-in, registration or password reset: those need storefront customer accounts, which do not exist yet, so a form there would submit into nothing."
5967
+ "A `custom` form for sign-in or registration: use the LOGIN / REGISTER / FORGOT / RESET types instead. Storefront customer accounts exist — those types submit through /_wb/account/* rather than the submissions table, and a custom form pointed at the same job stores a password in a response row."
5720
5968
  ],
5721
5969
  "contentTips": [
5722
5970
  "Ask for the fewest fields that make the request answerable. Every additional field measurably reduces the number of people who finish.",
@@ -23374,6 +23622,13 @@ export const ELEMENTS = {
23374
23622
  "locked": false,
23375
23623
  "hideInLayer": false,
23376
23624
  "childAllows": [],
23625
+ "events": {
23626
+ "click": [
23627
+ "go_to_url",
23628
+ "open_page",
23629
+ "popup"
23630
+ ]
23631
+ },
23377
23632
  "defaults": {
23378
23633
  "style": {
23379
23634
  "width": "100%",
@@ -32088,6 +32343,94 @@ export const TRAIT_WRITES = {
32088
32343
  }
32089
32344
  ]
32090
32345
  },
32346
+ "receipt_title": {
32347
+ "key": "receipt_title",
32348
+ "label": "Section title",
32349
+ "writes": [
32350
+ {
32351
+ "target": "specials",
32352
+ "writeKey": "title",
32353
+ "type": "string"
32354
+ }
32355
+ ]
32356
+ },
32357
+ "receipt_number_label": {
32358
+ "key": "receipt_number_label",
32359
+ "label": "Order number label",
32360
+ "writes": [
32361
+ {
32362
+ "target": "specials",
32363
+ "writeKey": "numberLabel",
32364
+ "type": "string"
32365
+ }
32366
+ ]
32367
+ },
32368
+ "receipt_placed_label": {
32369
+ "key": "receipt_placed_label",
32370
+ "label": "Date label",
32371
+ "writes": [
32372
+ {
32373
+ "target": "specials",
32374
+ "writeKey": "placedLabel",
32375
+ "type": "string"
32376
+ }
32377
+ ]
32378
+ },
32379
+ "receipt_status_label": {
32380
+ "key": "receipt_status_label",
32381
+ "label": "Status label",
32382
+ "writes": [
32383
+ {
32384
+ "target": "specials",
32385
+ "writeKey": "statusLabel",
32386
+ "type": "string"
32387
+ }
32388
+ ]
32389
+ },
32390
+ "receipt_items_label": {
32391
+ "key": "receipt_items_label",
32392
+ "label": "Items label",
32393
+ "writes": [
32394
+ {
32395
+ "target": "specials",
32396
+ "writeKey": "itemsLabel",
32397
+ "type": "string"
32398
+ }
32399
+ ]
32400
+ },
32401
+ "receipt_total_label": {
32402
+ "key": "receipt_total_label",
32403
+ "label": "Total label",
32404
+ "writes": [
32405
+ {
32406
+ "target": "specials",
32407
+ "writeKey": "totalLabel",
32408
+ "type": "string"
32409
+ }
32410
+ ]
32411
+ },
32412
+ "receipt_due_label": {
32413
+ "key": "receipt_due_label",
32414
+ "label": "Outstanding label",
32415
+ "writes": [
32416
+ {
32417
+ "target": "specials",
32418
+ "writeKey": "dueLabel",
32419
+ "type": "string"
32420
+ }
32421
+ ]
32422
+ },
32423
+ "receipt_unavailable_text": {
32424
+ "key": "receipt_unavailable_text",
32425
+ "label": "Unavailable message",
32426
+ "writes": [
32427
+ {
32428
+ "target": "specials",
32429
+ "writeKey": "unavailableText",
32430
+ "type": "string"
32431
+ }
32432
+ ]
32433
+ },
32091
32434
  "member_audience": {
32092
32435
  "key": "member_audience",
32093
32436
  "label": "Show to",
package/dist/core/tree.js CHANGED
@@ -4,6 +4,15 @@ export const SPEC_OVERLAY_ID = 'overlayId';
4
4
  /** The specials stamps a composed global section carries. */
5
5
  export const SPEC_GLOBAL_ID = 'globalId';
6
6
  export const SPEC_GLOBAL_KIND = 'globalKind';
7
+ /**
8
+ * The OPTIMISTIC FENCE on a shared master, one per kind.
9
+ *
10
+ * Compose writes it, the save sends it back as `expectRev`, and the platform
11
+ * refuses a stale one with a warning and a 200 — so a client that does not
12
+ * re-stamp from the save's own report silently drops every edit after the first.
13
+ */
14
+ export const SPEC_GLOBAL_REV = 'globalRev';
15
+ export const SPEC_OVERLAY_REV = 'overlayRev';
7
16
  export const SPEC_GLOBAL_REF = 'globalRef';
8
17
  /** The specials stamps a composed APP BLOCK carries (appblocks.go:97-100). */
9
18
  export const SPEC_APP_BLOCK_ID = 'appBlockId';
@@ -1,6 +1,6 @@
1
1
  import { isOverlay, subtreeIds, ancestors, appBlockRoot, SPEC_GLOBAL_ID, SPEC_GLOBAL_KIND, SPEC_GLOBAL_REF, } from '../../core/tree.js';
2
2
  import { ELEMENTS, ELEMENT_SEEDS, SATELLITE_RULES } from '../../catalog/elements.generated.js';
3
- import { createNode, mintSatellites } from './node.js';
3
+ import { bindingsForConfig, createNode, mintSatellites } from './node.js';
4
4
  import { refuseSecondTemplate } from './traps.js';
5
5
  import { genId } from './ids.js';
6
6
  /** Append sentinel: splice clamps a too-large index, and `isSyncablePatch`
@@ -252,17 +252,13 @@ function rebindPatch(doc, id, keys) {
252
252
  if (!touchesAxis)
253
253
  return null;
254
254
  const node = doc.node(id);
255
- const meta = ELEMENTS[node.data.type];
256
- const table = meta?.bindingsFor;
257
- if (!table)
258
- return null;
259
255
  const cfg = node.config ?? {};
260
- const source = String(keys.datasetSource ?? cfg.datasetSource ?? 'product');
261
- const kind = String(keys.kind ?? cfg.kind ?? '');
262
- const next = table[`${source}|${kind}`];
256
+ // The config the node will HAVE once these keys land — the axis is read off
257
+ // the result of the write, not off either half of it.
258
+ const next = bindingsForConfig(node.data.type, { ...cfg, ...keys, datasetSource: keys.datasetSource ?? cfg.datasetSource ?? 'product' });
263
259
  if (!next)
264
260
  return null;
265
- return { op: 'set', path: ['nodes', id, 'bindings'], value: JSON.parse(JSON.stringify(next)) };
261
+ return { op: 'set', path: ['nodes', id, 'bindings'], value: next };
266
262
  }
267
263
  /**
268
264
  * Copy a node and everything under it, under fresh ids, beside the original.
@@ -1,6 +1,27 @@
1
1
  import { applyPatches } from '../../core/patch.js';
2
2
  import { childrenOf, isOverlay, appBlockRoot } from '../../core/tree.js';
3
3
  import { bandOf, isGlobal } from './traps.js';
4
+ import { SATELLITE_RULES } from '../../catalog/elements.generated.js';
5
+ /**
6
+ * The satellite nodes this one owns, as {id, config key} pairs.
7
+ *
8
+ * Read from the GENERATED rules rather than a hand-kept list, so an element the
9
+ * platform gives a satellite is covered the day the catalog is regenerated. A
10
+ * key pointing at an id the document does not hold is skipped rather than
11
+ * reported: a dangling pointer is `validateForSave`'s business, not the map's.
12
+ */
13
+ function satellitesOf(doc, id) {
14
+ const node = doc.nodes[id];
15
+ const rules = SATELLITE_RULES[node?.data?.type ?? ''] ?? [];
16
+ const out = [];
17
+ for (const rule of rules) {
18
+ const satId = node?.config?.[rule.configKey];
19
+ if (typeof satId === 'string' && satId && doc.nodes[satId]) {
20
+ out.push({ id: satId, key: rule.configKey });
21
+ }
22
+ }
23
+ return out;
24
+ }
4
25
  /**
5
26
  * One page's document, in memory.
6
27
  *
@@ -139,8 +160,19 @@ export class PageDoc {
139
160
  out.global = true;
140
161
  if (appBlockRoot(this.doc, id) === id)
141
162
  out.app = true;
142
- if (level + 1 < depth && kidIds.length > 0) {
143
- out.kids = kidIds.map((k) => line(k, level + 1));
163
+ if (level + 1 < depth) {
164
+ // Satellites come FIRST, and are counted apart from `children`: they are
165
+ // this node's own chrome, not content in it, and mixing the two would
166
+ // make the child count disagree with `data.nodes` — the number every
167
+ // index-taking call (sb_add, sb_move) is written against.
168
+ const sats = satellitesOf(this.doc, id).map((sat) => {
169
+ const l = line(sat.id, level + 1);
170
+ l.satellite = sat.key;
171
+ return l;
172
+ });
173
+ const kids = kidIds.map((k) => line(k, level + 1));
174
+ if (sats.length || kids.length)
175
+ out.kids = [...sats, ...kids];
144
176
  }
145
177
  return out;
146
178
  };
@@ -5,6 +5,43 @@ import { genId } from './ids.js';
5
5
  function copy(v) {
6
6
  return JSON.parse(JSON.stringify(v));
7
7
  }
8
+ /**
9
+ * The bindings an element carries for a given config, or null when its data
10
+ * axis says nothing about this pair.
11
+ *
12
+ * Reads the generated `bindingsFor` table — the platform's own
13
+ * `datasetBindings(type, config)` answers, enumerated at codegen.
14
+ *
15
+ * THE KIND FALLBACK IS THE POINT. The table is keyed `${datasetSource}|${kind}`,
16
+ * but TEN elements with a table declare no default `config.kind` at all —
17
+ * `list-dataset`, `dataset-block`, `media-dataset`, `collection-media`,
18
+ * `product-variants`, `quantity-dataset` and friends, which is most of what a
19
+ * store is built from. For those the key came out `category|`, matched nothing,
20
+ * and the lookup returned null: a `list-dataset` switched to collections kept
21
+ * `bind-target` → `product_list` and repeated PRODUCTS under a heading that said
22
+ * collections. Every `<source>|*` row of such an element is identical (they have
23
+ * no kind axis to vary on), so the first row for the source IS the answer, and
24
+ * falling back to it is what makes the axis reachable at all.
25
+ */
26
+ export function bindingsForConfig(type, config) {
27
+ const table = ELEMENTS[type]?.bindingsFor;
28
+ if (!table)
29
+ return null;
30
+ const source = String(config?.datasetSource ?? '');
31
+ if (!source)
32
+ return null;
33
+ const kind = String(config?.kind ?? '');
34
+ const exact = table[`${source}|${kind}`];
35
+ if (exact)
36
+ return copy(exact);
37
+ // No kind axis on this element: any row for the source carries the same answer.
38
+ const prefix = `${source}|`;
39
+ for (const [key, value] of Object.entries(table)) {
40
+ if (key.startsWith(prefix))
41
+ return copy(value);
42
+ }
43
+ return null;
44
+ }
8
45
  /**
9
46
  * Mint a node seeded from its element's catalog defaults.
10
47
  *
@@ -21,6 +58,7 @@ export function createNode(type, opts = {}) {
21
58
  }
22
59
  const d = meta.defaults;
23
60
  const states = d.states;
61
+ const config = { ...copy(d.config ?? {}), ...(opts.config ?? {}) };
24
62
  return {
25
63
  id: genId(type),
26
64
  data: {
@@ -33,7 +71,7 @@ export function createNode(type, opts = {}) {
33
71
  custom: {},
34
72
  },
35
73
  style: { ...copy(d.style ?? {}), ...(opts.style ?? {}) },
36
- config: { ...copy(d.config ?? {}), ...(opts.config ?? {}) },
74
+ config,
37
75
  specials: { ...copy(d.specials ?? {}), ...(opts.specials ?? {}) },
38
76
  responsive: copy(d.responsive ?? {}),
39
77
  ...(states ? { states: copy(states) } : {}),
@@ -49,12 +87,17 @@ export function createNode(type, opts = {}) {
49
87
  // placeholder forever. Found on a live page: four product cards reading
50
88
  // "$0.00" with no titles, on a catalogue that had four products.
51
89
  //
52
- // Codegen bakes the factory's answer for the DEFAULT config. A caller who
53
- // overrides `config.kind` or `config.datasetSource` in the same call is
54
- // changing which field the element binds, and that re-derivation lives in
55
- // the platform — so the bindings here follow the defaults, and a changed
56
- // kind needs an explicit sb_bind.
57
- bindings: copy(d.bindings ?? []),
90
+ // Codegen bakes the factory's answer for EVERY config pair, not just the
91
+ // default one, so a caller who overrides `config.kind` or
92
+ // `config.datasetSource` in the same call is answered here rather than left
93
+ // holding the default entity's bindings. It used to be left: `sb_add` with
94
+ // `datasetSource: "category"` minted a repeater still bound to
95
+ // `product_list`, saved, published, and repeated products under a heading
96
+ // that said collections — with no warning on any of the four steps, because
97
+ // a wrong binding renders exactly like a right one until you read the data.
98
+ // `sb_set` re-derives on the same table for the same reason; the two paths
99
+ // must not disagree about what a config means.
100
+ bindings: bindingsForConfig(type, config) ?? copy(d.bindings ?? []),
58
101
  };
59
102
  }
60
103
  /**
@@ -110,6 +110,47 @@ export function readinessGaps(input) {
110
110
  fix: 'Add at least one shipping method (sb_api_find "shipping methods").',
111
111
  });
112
112
  }
113
+ // THE OTHER FIXED PATHS.
114
+ //
115
+ // `page.FixedPathTypes` is four — search, checkout, complete, account — and
116
+ // each resolves to the site's PUBLISHED page of that type
117
+ // (storefront/typeroute.go). `complete` is the one that backstops itself: with
118
+ // no completion page the storefront serves a built-in receipt, measured 200 on
119
+ // a live store that had none. The other three 404, and `/account` and
120
+ // `/search` 404 QUIETLY — nothing links to them by default, so the merchant
121
+ // finds out when a shopper who wants their order history does.
122
+ //
123
+ // Separate from the five above, and after them, because these do not stand
124
+ // between the store and a PAID ORDER: a shop with no account page still takes
125
+ // money. They stand between it and a finished website, which is the next
126
+ // question a merchant asks.
127
+ for (const [type, id, what, fix] of [
128
+ [
129
+ 'account',
130
+ 'accountPage',
131
+ '/account 404s. A shopper has no way to see their orders, addresses or saved items, and ' +
132
+ 'the account elements (account-info, address-book, wishlist-list, points-card) have ' +
133
+ 'nowhere to live.',
134
+ 'Create a page of type "account" and publish it. Put login and register forms behind a ' +
135
+ 'member-gate with audience "guests", and the profile behind audience "members".',
136
+ ],
137
+ [
138
+ 'search',
139
+ 'searchPage',
140
+ '/search 404s, so a search box in the header sends every shopper to a dead page.',
141
+ 'Create a page of type "search" and publish it.',
142
+ ],
143
+ ]) {
144
+ if (!pages || published(pages, type))
145
+ continue;
146
+ const draft = drafted(pages, type);
147
+ gaps.push({
148
+ id,
149
+ draft,
150
+ problem: what,
151
+ fix: draft ? `Publish the ${type} page that already exists.` : fix,
152
+ });
153
+ }
113
154
  if (input.globalNodes !== null && !opensCart([...input.pageNodes, ...input.globalNodes])) {
114
155
  gaps.push({
115
156
  id: 'cartTrigger',