@wootsup/yt-builder-mcp 1.11.0 → 1.12.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.
Files changed (189) hide show
  1. package/README.md +10 -10
  2. package/bin/yt-builder-mcp.js +1 -1
  3. package/dist/auth.d.ts +1 -1
  4. package/dist/auth.js +1 -1
  5. package/dist/catalog/tool-catalog-markdown.d.ts.map +1 -1
  6. package/dist/catalog/tool-catalog-markdown.js +13 -1
  7. package/dist/catalog/tool-catalog-markdown.js.map +1 -1
  8. package/dist/cli/doctor-command.d.ts.map +1 -1
  9. package/dist/cli/doctor-command.js +22 -2
  10. package/dist/cli/doctor-command.js.map +1 -1
  11. package/dist/client.d.ts +3 -3
  12. package/dist/client.js +1 -1
  13. package/dist/client.js.map +1 -1
  14. package/dist/clients/index.d.ts +1 -1
  15. package/dist/clients/index.js +1 -1
  16. package/dist/diagnostics/ca-reexec.js +2 -2
  17. package/dist/diagnostics/diagnose-network-error.d.ts +1 -1
  18. package/dist/diagnostics/doctor.d.ts +34 -2
  19. package/dist/diagnostics/doctor.d.ts.map +1 -1
  20. package/dist/diagnostics/doctor.js +63 -2
  21. package/dist/diagnostics/doctor.js.map +1 -1
  22. package/dist/diagnostics/startup-self-probe.js +1 -1
  23. package/dist/gateway/advanced-read-tool.js +1 -1
  24. package/dist/gateway/advanced-read-tool.js.map +1 -1
  25. package/dist/gateway/advanced-tool/discovery.d.ts.map +1 -1
  26. package/dist/gateway/advanced-tool/discovery.js +12 -1
  27. package/dist/gateway/advanced-tool/discovery.js.map +1 -1
  28. package/dist/gateway/essentials.d.ts +4 -4
  29. package/dist/gateway/essentials.d.ts.map +1 -1
  30. package/dist/gateway/essentials.js +12 -7
  31. package/dist/gateway/essentials.js.map +1 -1
  32. package/dist/install-skill.js +1 -1
  33. package/dist/net/extra-headers.d.ts +1 -1
  34. package/dist/net/extra-headers.js +1 -1
  35. package/dist/net/internal-host.js +2 -2
  36. package/dist/net/proxy-dispatcher.js +1 -1
  37. package/dist/net/proxy-dispatcher.js.map +1 -1
  38. package/dist/net/site-basic-auth.d.ts +2 -2
  39. package/dist/net/site-basic-auth.js +2 -2
  40. package/dist/proxy/bridge.d.ts +2 -2
  41. package/dist/proxy/bridge.js +3 -3
  42. package/dist/proxy/bridge.js.map +1 -1
  43. package/dist/setup-cli.d.ts +2 -2
  44. package/dist/setup-cli.js +2 -2
  45. package/dist/setup-cli.js.map +1 -1
  46. package/dist/setup-npx-spec.d.ts +1 -1
  47. package/dist/setup-npx-spec.js +1 -1
  48. package/dist/setup-wizard-handshake.js +1 -1
  49. package/dist/setup-wizard-types.d.ts +1 -1
  50. package/dist/setup-wizard.js +3 -3
  51. package/dist/setup-wizard.js.map +1 -1
  52. package/dist/sites/client-pool.d.ts +1 -1
  53. package/dist/sites/client-pool.js +1 -1
  54. package/dist/sites/tools/sites-list.d.ts.map +1 -1
  55. package/dist/sites/tools/sites-list.js +4 -2
  56. package/dist/sites/tools/sites-list.js.map +1 -1
  57. package/dist/sites/tools/sites-test.js +1 -1
  58. package/dist/sites/tools/sites-test.js.map +1 -1
  59. package/dist/sites/tools/use-site.js +2 -2
  60. package/dist/sites/tools/use-site.js.map +1 -1
  61. package/dist/tools/budgeted-table.d.ts +21 -0
  62. package/dist/tools/budgeted-table.d.ts.map +1 -0
  63. package/dist/tools/budgeted-table.js +137 -0
  64. package/dist/tools/budgeted-table.js.map +1 -0
  65. package/dist/tools/elements/builders.d.ts.map +1 -1
  66. package/dist/tools/elements/builders.js +48 -24
  67. package/dist/tools/elements/builders.js.map +1 -1
  68. package/dist/tools/elements/handlers-write.d.ts +19 -0
  69. package/dist/tools/elements/handlers-write.d.ts.map +1 -1
  70. package/dist/tools/elements/handlers-write.js +100 -3
  71. package/dist/tools/elements/handlers-write.js.map +1 -1
  72. package/dist/tools/elements/handlers.d.ts +13 -8
  73. package/dist/tools/elements/handlers.d.ts.map +1 -1
  74. package/dist/tools/elements/handlers.js +28 -18
  75. package/dist/tools/elements/handlers.js.map +1 -1
  76. package/dist/tools/elements/schema-validation.d.ts +129 -7
  77. package/dist/tools/elements/schema-validation.d.ts.map +1 -1
  78. package/dist/tools/elements/schema-validation.js +459 -23
  79. package/dist/tools/elements/schema-validation.js.map +1 -1
  80. package/dist/tools/format/elements-format.d.ts.map +1 -1
  81. package/dist/tools/format/elements-format.js +56 -12
  82. package/dist/tools/format/elements-format.js.map +1 -1
  83. package/dist/tools/format/health-format.d.ts +16 -0
  84. package/dist/tools/format/health-format.d.ts.map +1 -1
  85. package/dist/tools/format/health-format.js +9 -0
  86. package/dist/tools/format/health-format.js.map +1 -1
  87. package/dist/tools/format/inspection-format.d.ts +7 -0
  88. package/dist/tools/format/inspection-format.d.ts.map +1 -1
  89. package/dist/tools/format/inspection-format.js +18 -0
  90. package/dist/tools/format/inspection-format.js.map +1 -1
  91. package/dist/tools/format/pages-format.d.ts +1 -1
  92. package/dist/tools/format/pages-format.d.ts.map +1 -1
  93. package/dist/tools/format/pages-format.js +7 -0
  94. package/dist/tools/format/pages-format.js.map +1 -1
  95. package/dist/tools/health.d.ts.map +1 -1
  96. package/dist/tools/health.js +67 -6
  97. package/dist/tools/health.js.map +1 -1
  98. package/dist/tools/index.d.ts.map +1 -1
  99. package/dist/tools/index.js +7 -0
  100. package/dist/tools/index.js.map +1 -1
  101. package/dist/tools/inspection.d.ts +34 -0
  102. package/dist/tools/inspection.d.ts.map +1 -1
  103. package/dist/tools/inspection.js +600 -65
  104. package/dist/tools/inspection.js.map +1 -1
  105. package/dist/tools/library.d.ts +6 -2
  106. package/dist/tools/library.d.ts.map +1 -1
  107. package/dist/tools/library.js +36 -21
  108. package/dist/tools/library.js.map +1 -1
  109. package/dist/tools/local-content/builders.js +4 -4
  110. package/dist/tools/local-content/builders.js.map +1 -1
  111. package/dist/tools/local-content/handlers.d.ts +3 -1
  112. package/dist/tools/local-content/handlers.d.ts.map +1 -1
  113. package/dist/tools/local-content/handlers.js +8 -4
  114. package/dist/tools/local-content/handlers.js.map +1 -1
  115. package/dist/tools/multi-items/builders.d.ts.map +1 -1
  116. package/dist/tools/multi-items/builders.js +2 -4
  117. package/dist/tools/multi-items/builders.js.map +1 -1
  118. package/dist/tools/navigation/builders.d.ts +18 -0
  119. package/dist/tools/navigation/builders.d.ts.map +1 -0
  120. package/dist/tools/navigation/builders.js +47 -0
  121. package/dist/tools/navigation/builders.js.map +1 -0
  122. package/dist/tools/navigation/handlers.d.ts +30 -0
  123. package/dist/tools/navigation/handlers.d.ts.map +1 -0
  124. package/dist/tools/navigation/handlers.js +41 -0
  125. package/dist/tools/navigation/handlers.js.map +1 -0
  126. package/dist/tools/navigation/index.d.ts +15 -0
  127. package/dist/tools/navigation/index.d.ts.map +1 -0
  128. package/dist/tools/navigation/index.js +14 -0
  129. package/dist/tools/navigation/index.js.map +1 -0
  130. package/dist/tools/navigation/schemas.d.ts +46 -0
  131. package/dist/tools/navigation/schemas.d.ts.map +1 -0
  132. package/dist/tools/navigation/schemas.js +41 -0
  133. package/dist/tools/navigation/schemas.js.map +1 -0
  134. package/dist/tools/pages/builders.d.ts.map +1 -1
  135. package/dist/tools/pages/builders.js +148 -49
  136. package/dist/tools/pages/builders.js.map +1 -1
  137. package/dist/tools/pages/handlers-audit.d.ts +14 -3
  138. package/dist/tools/pages/handlers-audit.d.ts.map +1 -1
  139. package/dist/tools/pages/handlers-audit.js +11 -1
  140. package/dist/tools/pages/handlers-audit.js.map +1 -1
  141. package/dist/tools/pages/handlers-read.d.ts +43 -5
  142. package/dist/tools/pages/handlers-read.d.ts.map +1 -1
  143. package/dist/tools/pages/handlers-read.js +176 -44
  144. package/dist/tools/pages/handlers-read.js.map +1 -1
  145. package/dist/tools/pages/handlers-write.d.ts +2 -2
  146. package/dist/tools/pages/handlers-write.d.ts.map +1 -1
  147. package/dist/tools/pages/handlers-write.js +3 -2
  148. package/dist/tools/pages/handlers-write.js.map +1 -1
  149. package/dist/tools/pages/header-transparency.d.ts +64 -0
  150. package/dist/tools/pages/header-transparency.d.ts.map +1 -0
  151. package/dist/tools/pages/header-transparency.js +119 -0
  152. package/dist/tools/pages/header-transparency.js.map +1 -0
  153. package/dist/tools/pages/schemas.d.ts +95 -9
  154. package/dist/tools/pages/schemas.d.ts.map +1 -1
  155. package/dist/tools/pages/schemas.js +201 -19
  156. package/dist/tools/pages/schemas.js.map +1 -1
  157. package/dist/tools/response-budget.d.ts +77 -0
  158. package/dist/tools/response-budget.d.ts.map +1 -0
  159. package/dist/tools/response-budget.js +134 -0
  160. package/dist/tools/response-budget.js.map +1 -0
  161. package/dist/tools/shared-schemas.d.ts +23 -0
  162. package/dist/tools/shared-schemas.d.ts.map +1 -1
  163. package/dist/tools/shared-schemas.js +28 -0
  164. package/dist/tools/shared-schemas.js.map +1 -1
  165. package/dist/tools/sources/builders.d.ts.map +1 -1
  166. package/dist/tools/sources/builders.js +13 -15
  167. package/dist/tools/sources/builders.js.map +1 -1
  168. package/dist/tools/sources/handlers-bind.d.ts +3 -3
  169. package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
  170. package/dist/tools/sources/handlers-bind.js +4 -2
  171. package/dist/tools/sources/handlers-bind.js.map +1 -1
  172. package/dist/tools/sources/handlers.d.ts +4 -2
  173. package/dist/tools/sources/handlers.d.ts.map +1 -1
  174. package/dist/tools/sources/handlers.js +11 -7
  175. package/dist/tools/sources/handlers.js.map +1 -1
  176. package/dist/tools/sparse-fields.d.ts +49 -8
  177. package/dist/tools/sparse-fields.d.ts.map +1 -1
  178. package/dist/tools/sparse-fields.js +90 -10
  179. package/dist/tools/sparse-fields.js.map +1 -1
  180. package/dist/tools/sublayout/builders.d.ts.map +1 -1
  181. package/dist/tools/sublayout/builders.js +1 -2
  182. package/dist/tools/sublayout/builders.js.map +1 -1
  183. package/dist/tools/tool-builder/results.d.ts +19 -6
  184. package/dist/tools/tool-builder/results.d.ts.map +1 -1
  185. package/dist/tools/tool-builder/results.js +19 -6
  186. package/dist/tools/tool-builder/results.js.map +1 -1
  187. package/manifest.json +1 -1
  188. package/package.json +2 -2
  189. package/skills/yt-builder-mcp/SKILL.md +1202 -83
@@ -12,15 +12,15 @@ description: Drive the YOOtheme Pro Page Builder on WordPress or Joomla 5/6. Dis
12
12
  > WordPress and Joomla 5/6.
13
13
 
14
14
  This skill helps AI assistants drive the YOOtheme Pro Page Builder through the
15
- `@wootsup/yt-builder-mcp` server. The server catalogues 49 typed, scoped,
16
- idempotent tools (51 callable end-to-end with the two gateways). 41 of them
15
+ `@wootsup/yt-builder-mcp` server. The server catalogues 53 typed, scoped,
16
+ idempotent tools (55 callable end-to-end with the two gateways). 42 of them
17
17
  advertised as first-class entries in `tools/list`
18
- (37 essential L1 forwards + 2 direct L3 entries + 2 gateways), and 10 advanced
18
+ (38 essential L1 forwards + 2 direct L3 entries + 2 gateways), and 13 advanced
19
19
  tools reachable through the gateways. The write-capable
20
- `yootheme_builder_advanced` gateway routes all 10; the read-only
20
+ `yootheme_builder_advanced` gateway routes all 13; the read-only
21
21
  `yootheme_builder_advanced_read` gateway routes only the read-only subset
22
22
  (page_get_schema, inspect_sublayout, layout_query, local_content_types,
23
- local_content_read, pages_audit) and is itself
23
+ local_content_read, pages_audit, heading_outline, navigation) and is itself
24
24
  read-only-annotated, so MCP hosts can auto-allow pure inspection without a
25
25
  per-call write-approval prompt. This 3-lane split keeps `tools/list` well below
26
26
  the ~40-tool Cursor cap while every catalogued tool stays fully reachable.
@@ -54,8 +54,8 @@ Some clients (notably Claude Desktop with the `.mcpb` bundle) expose **two**
54
54
  entries when the user types "YT Builder MCP" into the picker:
55
55
 
56
56
  - **`YT Builder MCP for YOOtheme Pro (unofficial)`**: the MCP **server**.
57
- Provides the 41 first-class tools (37 essential, 2 direct, 2 gateways).
58
- The `yootheme_builder_advanced` gateway routes all 10 advanced tools; the
57
+ Provides the 42 first-class tools (38 essential, 2 direct, 2 gateways).
58
+ The `yootheme_builder_advanced` gateway routes all 13 advanced tools; the
59
59
  read-only `yootheme_builder_advanced_read` gateway routes the read-only
60
60
  subset without a write-approval prompt.
61
61
  - **`Von YT Builder MCP for YOOtheme Pro`**: the bundled **skill** (this
@@ -82,9 +82,9 @@ The server exposes:
82
82
  - **2 direct top-level tools**, always callable, always in `tools/list`:
83
83
  `yootheme_builder_health` and `yootheme_builder_diagnose`. These are
84
84
  the "the gateway itself might be broken" escape hatch.
85
- - **37 essential forwarded tools**: common reads + the most-used writes
85
+ - **38 essential forwarded tools**: common reads + the most-used writes
86
86
  (pages_list, pages_resolve, pages_create, pages_delete, page_dynamize, scaffold_section,
87
- get_etag, element_list / add / update_settings / get / move / clone / delete,
87
+ get_etag, element_list / add / update_settings / set_status / get / move / clone / delete,
88
88
  page_get_layout, sources_list, source_get_schema, element_types_list, element_type_get_schema,
89
89
  template_summary, inspect_multi_items_binding, library_browse, library_get, library_insert,
90
90
  image_search, image_import,
@@ -99,7 +99,7 @@ The server exposes:
99
99
  binding entry-point — introspect a source's query methods + relations +
100
100
  the current-term archive-context convention before binding — is a direct
101
101
  call too. `render_preview`, `element_set_source`, `rename_source` and
102
- `template_set_type` (Glöckle HANDOFF-4/5) are L1 so the core write/refactor +
102
+ `template_set_type` (customer HANDOFF-4/5) are L1 so the core write/refactor +
103
103
  render-verify tools an agent reaches for after a taxonomy/CPT rename — set a
104
104
  precomposed source at a pointer, render-verify the saved page is not silently
105
105
  blank, rename a source/field template-wide, re-type an orphaned archive/single
@@ -125,11 +125,14 @@ The server exposes:
125
125
  step that turns a placeholder-filled skeleton into a finished-looking
126
126
  page (see "Fill image slots with real on-topic photos" under Layout
127
127
  Library).
128
- - **10 advanced captured tools**: everything else (page_get_schema,
128
+ - **13 advanced captured tools**: everything else (page_get_schema,
129
129
  element_unbind_source, clean_implode_directives,
130
130
  `inspect_sublayout`, `layout_query`, `library_save_as`, the two Local
131
131
  Content Read tools `local_content_types` / `local_content_read`,
132
- `pages_audit`, and `library_delete` — the INVERSE of `library_save_as`).
132
+ `pages_audit`, `library_delete` — the INVERSE of `library_save_as`
133
+ `heading_outline`, a real doc-order H1-H6 outline of one page,
134
+ `navigation`, the site's menus as a parent -> children tree, and
135
+ `header_transparency_set`, the transparent-header inspect/repair).
133
136
  Reachable through one gateway tool:
134
137
  `yootheme_builder_advanced({ tool: "<name>", arguments: { ... } })`. For
135
138
  example, to inspect a panel/grid_item `< SUBLAYOUT`:
@@ -156,8 +159,31 @@ The server exposes:
156
159
  `yootheme_builder_advanced_read({ tool: "yootheme_builder_pages_audit", arguments: { checks: ["seo", "structure"] } })`
157
160
  (the write gateway routes it too, if you prefer one entry point:
158
161
  `yootheme_builder_advanced({ tool: "yootheme_builder_pages_audit" })`).
159
-
160
- The core write/refactor + render-verify tools the Glöckle HANDOFF-4/5 ask for
162
+ And to get a real, doc-order H1-H6 outline of ONE page (each heading with
163
+ its source element and TEXT, plus the alt-status of every image) —
164
+ read-only STRUCTURE inspection, not findings:
165
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_heading_outline", arguments: { template_id } })`
166
+ (or via the write gateway: `yootheme_builder_advanced({ tool: "yootheme_builder_heading_outline" })`).
167
+ And to see the site's Haupt- + Sub-Menüstruktur as a parent -> children
168
+ TREE (not a flat list) — for "which page sits under which menu item"
169
+ (product pages usually live in the main menu), each item carrying a
170
+ `page_role` facet (home/landing/product/legal/thankyou/blog/mockup/
171
+ template/generic, the SAME vocabulary `pages_list` uses) and, when
172
+ derivable at zero extra cost, a `target_page` (`page:<id>` /
173
+ `article:<id>`) ready for `page_get_layout`:
174
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_navigation" })`
175
+ (or via the write gateway: `yootheme_builder_advanced({ tool: "yootheme_builder_navigation" })`).
176
+ Pass `menu: "Footer"` to narrow to one menu by name.
177
+ And to inspect or repair the transparent header — the navigation pulled over
178
+ the first section — a WRITE, so the write gateway only. `dry_run` defaults to
179
+ TRUE, so this call reports the current state, the theme-declared
180
+ `--uk-inverse`, `risk_class`, `confidence` and `would_change` without
181
+ touching anything:
182
+ `yootheme_builder_advanced({ tool: "yootheme_builder_header_transparency_set", arguments: { template_id } })`
183
+ Repeat with `mode: "auto", dry_run: false, etag` to write the theme-declared
184
+ colour. See "Transparent header (the exception — never a default)".
185
+
186
+ The core write/refactor + render-verify tools the customer HANDOFF-4/5 ask for
161
187
  are now **L1 — call them directly by name**, not through a gateway:
162
188
  - To render-check a saved page over the authenticated channel (F5+N5 — does
163
189
  it render, with how many items, or is it silently blank? where an external
@@ -198,7 +224,7 @@ The server exposes:
198
224
  into delete/unbind, so hosts gate it) and `yootheme_builder_advanced_read`
199
225
  (read-only; routes ONLY the read-only advanced subset — page_get_schema,
200
226
  inspect_sublayout, layout_query, local_content_types, local_content_read,
201
- pages_audit —
227
+ pages_audit, heading_outline, navigation
202
228
  and is read-only-annotated so hosts auto-allow it). Prefer the read gateway
203
229
  for pure inspection:
204
230
  `yootheme_builder_advanced_read({ tool: "yootheme_builder_page_get_schema", arguments: { template_id } })`.
@@ -209,9 +235,9 @@ The server exposes:
209
235
  `yootheme_builder_advanced({ tool: "yootheme_builder_local_content_types" })` and
210
236
  `yootheme_builder_advanced({ tool: "yootheme_builder_local_content_read", arguments: { kind: "entities", type: "post" } })`.)
211
237
 
212
- `tools/list` therefore advertises 41 names (37 + 2 + 2). That's 37 L1
213
- essentials + 2 L3 direct + 2 gateways. The total callable surface is 51
214
- (41 advertised + 10 advanced reachable through the gateways). If the AI
238
+ `tools/list` therefore advertises 42 names (38 + 2 + 2). That's 38 L1
239
+ essentials + 2 L3 direct + 2 gateways. The total callable surface is 55
240
+ (42 advertised + 13 advanced reachable through the gateways). If the AI
215
241
  client reports "tool not found", you are almost certainly calling an
216
242
  advanced tool by its raw name. Wrap it in
217
243
  `yootheme_builder_advanced({ tool, arguments })` (or the read gateway for
@@ -233,6 +259,23 @@ guess where the agent is pointing:
233
259
  template"): find the matching row, return `frontend_url` (resolved) or
234
260
  `frontend_url_template` (with placeholders the user fills in).
235
261
 
262
+ ### `available_endpoints` is request-ready, on both platforms
263
+
264
+ `yootheme_builder_health` also returns `available_endpoints`: every REST path the
265
+ host plugin actually serves. On **both** WordPress and Joomla this list is
266
+ derived from the **live route registration** — WordPress enumerates
267
+ `rest_get_server()->get_routes()`, Joomla reads the ApiRouter that matched the
268
+ request — so it can never drift from what the server accepts.
269
+
270
+ Use it as-is when documenting or probing a site's surface: **every path is
271
+ request-ready as printed**, relative to the platform's REST root
272
+ (`/wp-json` on WordPress, `/api/index.php` on Joomla). Captures appear as
273
+ readable placeholders (`<templateId>`, `<path>`) that you substitute.
274
+
275
+ Do not hand-write a Joomla endpoint list from memory: the Joomla paths carry a
276
+ `yt-builder-mcp` segment (`/v1/yt-builder-mcp/pages/audit`, not `/v1/pages/audit`)
277
+ and the router rejects the short form.
278
+
236
279
  Treat `frontend_url: null` as "host plugin could not resolve a public URL for
237
280
  this template". Surface that honestly rather than fabricating one.
238
281
 
@@ -280,6 +323,26 @@ serves_context_ids: [11] }`. Use `pages_resolve` when you just want the answer;
280
323
  use the `serves_context_*` columns when you already hold `pages_list` and want to
281
324
  match locally.
282
325
 
326
+ **`page_role` on `pages_list` rows — a coarse content-role facet for filtering.**
327
+ Every row also carries `page_role`, one of `home`, `landing`, `product`, `legal`,
328
+ `thankyou`, `blog`, `mockup`, `template`, `generic`. It answers "which of these 80
329
+ rows are actually PRODUCT pages?" without you guessing from names — filter the
330
+ rows you already have by `page_role === "product"` instead of pattern-matching
331
+ labels yourself. It is derived HEURISTICALLY from fields already on the row
332
+ (URL slug, the raw YOOtheme type condition, the row's name) — a legal/thank-you/
333
+ product/landing match comes from the URL path (`/impressum`, `/datenschutz`,
334
+ `/danke`, `/produkt/…`); `home` comes from the site's authoritative front-page
335
+ resolution OR the row's own URL being the bare site root; `blog` from a native
336
+ WP post / Joomla article type; `mockup` from a designer's own "demo"/"mockup"/
337
+ "placeholder" wording in the row's name; `template` from a bare internal
338
+ condition. **`generic` is the honest default, not a bug** — exactly like an
339
+ empty `serves_context_ids`, it means no signal fired, not that classification
340
+ failed. Treat `page_role` as a filter/identification AID, not a guarantee: it
341
+ never re-reads anything (zero extra calls), so it can occasionally miss a page
342
+ whose role isn't visible from its URL/type/name — cross-check with
343
+ `page_get_layout` when precision genuinely matters (e.g. before a destructive
344
+ bulk action).
345
+
283
346
  ### Address a page WITHOUT its URL — the `target` object
284
347
 
285
348
  `pages_resolve` and `pages_audit` take the **same** `target` object, so one
@@ -359,7 +422,9 @@ never a transport failure — report it verbatim rather than retrying blindly.
359
422
  | `no_target` | 400 | A `target` WAS supplied but none of its members is a usable address — every member must be a **non-empty scalar**. `target[special][x]=y` (a nested value) and a templated `target[title]=` that rendered empty both land here. | Re-send with exactly one non-empty scalar key. Never re-send without a `target` "to see what happens": that is a whole-site sweep whose findings you would attribute to one page. |
360
423
  | `too_many_targets` | 400 | Two or more address keys, or `url` alongside `target`. | Pick one. Use `title` or `pages_list` when you only know the page by name. |
361
424
  | `types_conflicts_with_target` | 400 | `types` was sent with a `target`. | Drop `types` — a target already names exactly one page, so there is nothing for a type filter to narrow. |
362
- | `unknown_check_category` | 400 | A `checks` value that is not one of the four categories (`seo`, `structure`, `content`, `links`) — a typo, a finding CODE passed where a category belongs, or a case variant: the vocabulary is **case-sensitive**, so `SEO` is not `seo`. | Re-send with a real category (the message names both the unknown members and the valid set). This is refused rather than filtered because an unrecognised value selects ZERO checks, and zero checks over a normally-scanned site returns `findings: []` — a confident CLEAN verdict for a typo. **Never read such an answer as clean.** |
425
+ | `unknown_check_category` | 400 | A `checks` value that is not one of the five categories (`seo`, `structure`, `content`, `links`, `design`) — a typo, a finding CODE passed where a category belongs, or a case variant: the vocabulary is **case-sensitive**, so `SEO` is not `seo`. | Re-send with a real category (the message names both the unknown members and the valid set). This is refused rather than filtered because an unrecognised value selects ZERO checks, and zero checks over a normally-scanned site returns `findings: []` — a confident CLEAN verdict for a typo. **Never read such an answer as clean.** |
426
+ | `unknown_audit_mode` | 400 | A `mode` value that is not `builder` or `rendered` (a typo, e.g. `render`). | Re-send with one of the two valid values. Refused rather than silently falling back to `builder`, for the same reason as `unknown_check_category`: nothing was scanned, so the previous answer is NOT a clean result. |
427
+ | `unknown_heading_outline_mode` | 400 | The SAME `mode` vocabulary (`builder`/`rendered`), on `yootheme_builder_heading_outline` instead of `pages_audit` — a dedicated code so the two are never confused. | Re-send with `builder` or `rendered`. |
363
428
  | `no_template_matched_types` | 400 | A site-sweep `types` filter matched NO template on this site — a typo or, most often, a case variant (`PAGE` is not `page`). Unlike `checks`, the `types` vocabulary is **open and site-dependent** (`page`, `article`, `single-post`, `com_content.category`, …), so it cannot be validated against a fixed list — only a zero match is knowable, and it is knowable only after the sweep. | Re-send with one of the types the message lists (it names what this site actually has), or omit `types` to sweep everything. Refused for the same reason as `unknown_check_category`: a filter that selected nothing answered HTTP 200 with `findings: []` and an all-zero summary, which reads as CLEAN. **Never read such an answer as clean.** |
364
429
  | `template_not_found` | 404 | The `template_id` names no template carrying a YOOtheme layout on this site. | Re-read the id from `pages_list`; ids are site-specific and change when a page is recreated. |
365
430
  | `post_not_found` | 404 | The `post_id` names no WP post / Joomla article with a per-page layout. | Same: confirm via `pages_list`. A post that exists but has no per-page layout is rendered by a THEME template — find that template instead. |
@@ -714,6 +779,164 @@ section as the last child of the template root.
714
779
 
715
780
  ---
716
781
 
782
+ ## Transparent header (the exception — never a default)
783
+
784
+ A transparent header pulls the page's **first** section up behind the site
785
+ header and recolours the navigation over it. It only works over a hero medium
786
+ that was made for it. **The default is: do not pull the section behind the
787
+ header.**
788
+
789
+ **Never turn `header_transparent` on by your own initiative** — not while
790
+ scaffolding a hero, not while inserting a Library layout, not "because it looks
791
+ more modern". Turn it on only when the customer asks for it, or when the
792
+ template you inserted already ships a hero medium built for it. **When in
793
+ doubt, leave it off.** That is always the safe answer, never a compromise.
794
+ Whoever turns it on picks the colour deliberately; with no deliberate choice
795
+ the colour is `""`, never `"light"`.
796
+
797
+ ### The three props (they live on a `section`)
798
+
799
+ | Prop | Values | Read by YOOtheme only when |
800
+ |---|---|---|
801
+ | `header_transparent` | `true` / `false` | — (this is the master switch) |
802
+ | `header_transparent_noplaceholder` | `true` / `false` | `header_transparent` is true |
803
+ | `header_transparent_text_color` | `""` / `"light"` / `"dark"` | `header_transparent` is true |
804
+
805
+ ### Three traps that break pages silently
806
+
807
+ 1. **Only the FIRST section counts.** YOOtheme applies the transparent header
808
+ from the first section of the layout and ignores the props on every later
809
+ section — no error, no builder warning. "I set it and nothing happened" is
810
+ almost always this. Put the props on the first section, or remove them.
811
+ 2. **Setting the colour without the master switch does nothing.**
812
+ `header_transparent_text_color` / `header_transparent_noplaceholder` on a
813
+ section whose `header_transparent` is off are dead props. They read as
814
+ configuration and are never applied.
815
+ 3. **`"light"` is not a safe default — it is white navigation text.** The
816
+ recolour fires even when the section has no image, no video and no `style`,
817
+ so `"light"` over a light theme background is literally white-on-white. The
818
+ THEME already states which colour fits each section style (its compiled
819
+ `theme.css` declares `--uk-inverse` per style, e.g. `dark` for
820
+ `.uk-section-default`). Read that, or — where no image or video is in the way
821
+ — leave the colour at `""` and let the theme decide. Never assume `"light"`.
822
+
823
+ **Over a hero image or video, no colour choice is provable from the layout
824
+ alone.** The image hides the background the theme's declaration describes, and
825
+ which part of a `cover` image ends up under the header depends on the viewport.
826
+
827
+ Do not hand-write a colour there — and do **not** "play it safe" by pushing an
828
+ existing one back to `""`. `""` is the theme's own default colour: a real
829
+ rendered value, not an abstention. Over a dark hero on `.uk-section-default`
830
+ (which declares `dark`) reverting to `""` puts dark navigation on a dark photo,
831
+ which is the very break you were trying to avoid.
832
+
833
+ Let the tool leave it alone — `mode: "auto"` writes NO colour over a medium —
834
+ then LOOK at the rendered page (see "Look at what you built") at the customer's
835
+ viewport — unscrolled AND after scrolling, because a
836
+ sticky header drops its transparency on scroll. Then write what you SAW with
837
+ `mode: "explicit"`. Do not guess a colour, and do
838
+ not reason about "the top N% of the image".
839
+
840
+ ### Use `header_transparency_set` — do not hand-write these props
841
+
842
+ `yootheme_builder_header_transparency_set` (advanced gateway) is the tool for
843
+ all of this. It **always targets the first section**, touches only the three
844
+ props, and **`dry_run` defaults to `true`** — so the first call is an
845
+ inspection that writes nothing.
846
+
847
+ ```jsonc
848
+ // 1. INSPECT (writes nothing — dry_run defaults true, no etag needed).
849
+ // Returns the current state, the theme-declared --uk-inverse, risk_class,
850
+ // confidence, a recommendation and would_change/changes.
851
+ yootheme_builder_header_transparency_set({ template_id: "home" })
852
+
853
+ // 2. REPAIR with the theme's own declared colour.
854
+ yootheme_builder_header_transparency_set({
855
+ template_id: "home", mode: "auto", dry_run: false, etag: "<latest>"
856
+ })
857
+
858
+ // 3. Turn it ON deliberately, without asserting a colour.
859
+ // mode "auto" derives it; over a hero medium it writes NO colour and tells
860
+ // you to look at the page instead.
861
+ yootheme_builder_header_transparency_set({
862
+ template_id: "home", mode: "auto", transparent: true, dry_run: false, etag: "<latest>"
863
+ })
864
+
865
+ // 4. A colour you MEASURED over a hero medium (only mode "explicit" takes one).
866
+ yootheme_builder_header_transparency_set({
867
+ template_id: "home", mode: "explicit", text_color: "light",
868
+ dry_run: false, etag: "<latest>"
869
+ })
870
+ ```
871
+
872
+ `mode: "explicit"` refuses a colour the theme's declaration contradicts when no
873
+ image or video hides that background (HTTP 409,
874
+ `error_code: header_contrast_contradicts_theme`). Override with
875
+ `acknowledge_contrast_risk: true` only after LOOKING at the rendered page.
876
+
877
+ To undo it, call with `transparent: false` — the tool removes all three props,
878
+ so no dead configuration is left behind. It is never refused —
879
+ **turning transparency OFF is always safe and never needs a justification.**
880
+
881
+ ### Writing the props by hand — what `element_update_settings` refuses
882
+
883
+ You *can* write the props by hand with `element_update_settings` (`merge: true`,
884
+ on `/children/0`). That path now refuses the one mistake it can prove on its own,
885
+ and quietly does the safe thing for the other:
886
+
887
+ - **Dead props are rejected.** A `header_transparent_text_color` or
888
+ `header_transparent_noplaceholder` that this write leaves without a true
889
+ `header_transparent` comes back as
890
+ `error_code: header_transparent_dead_prop` and nothing is written. A prop
891
+ YOOtheme never reads is dead whatever the theme declares, so this needs no
892
+ theme knowledge. Note the merge semantics: `element_update_settings` REPLACES
893
+ props unless you pass `merge: true`, so a full-replace write that omits
894
+ `header_transparent` switches the header off.
895
+ - **Turning it on without a colour writes `""`** — the theme's own default —
896
+ and tells you so. It is never defaulted to `"light"`. Under `merge: true` the
897
+ stored colour cannot be read from here, so nothing is seeded and you are
898
+ asked to state the colour yourself rather than have a guess clobber a value
899
+ you measured.
900
+ - **Over a hero image or video the write is ALLOWED**, with a
901
+ `header_contrast_unverifiable_over_media` warning. Nothing is corrected there
902
+ — only your eyes can settle it (see "Look at what you built"). That warning
903
+ **only fires when THIS write states the `image`/`video` itself.** A medium
904
+ already stored on the section is not readable from here, so under
905
+ `merge: true` — the shape recommended just above — such a write goes through
906
+ **silently**. Read no warning as "there is no medium": the usual hand-write
907
+ (`merge: true` with only the header props, on a section that already has its
908
+ hero) is exactly that silent case. `pages_audit` and
909
+ `header_transparency_set` read the stored layout and do see it.
910
+
911
+ **What that hand-write path still cannot check for you:** the **first-section
912
+ rule** — a single element write does not know where its section sits in the
913
+ layout — and the **theme declaration**, because the compiled `theme.css` is not
914
+ readable from the MCP server. A colour that contradicts `--uk-inverse` therefore
915
+ goes through untouched there. `header_transparency_set` and `pages_audit` are
916
+ where both of those are checked.
917
+
918
+ **You do not have to spot this by eye.** `pages_audit` carries two `design`
919
+ codes for it. Both read the first section's props plus the theme's own
920
+ `--uk-inverse` declaration for that section style out of the compiled
921
+ `theme.css`:
922
+
923
+ - **`header-contrast-risk`** (severity `warning`) — the layout forces a colour
924
+ the theme's declaration contradicts, and **no image or video covers** the
925
+ background that declaration describes. Provable from the props alone: the
926
+ recommended colour is the theme's own declared value, so it never guesses a colour.
927
+ - **`header-contrast-unverifiable`** (severity `info`) — a transparent header
928
+ over a hero image or video, **whatever the colour**. The medium covers exactly
929
+ the background the declaration describes, so nothing is provable there and the
930
+ code recommends no colour at all: go and look at the page.
931
+
932
+ The split is "is a medium in the way?", never "is the colour right?". Behind a
933
+ hero medium no value is verifiable and none is safer than another — not even
934
+ `""`, which is simply the theme's default colour. When `theme.css` cannot be
935
+ read, both say nothing at all rather than judging on a hunch. See Workflow 6's
936
+ remediation catalog.
937
+
938
+ ---
939
+
717
940
  ## Multi-column layouts (columns that sit side-by-side)
718
941
 
719
942
  **The single most common layout mistake: a `row` with several `column`
@@ -914,6 +1137,38 @@ version on a layout you read-modify-write is preserved, never overwritten.
914
1137
 
915
1138
  ---
916
1139
 
1140
+ ## Per-element render quirks (the single-element tail)
1141
+
1142
+ The rules that apply to a whole CLASS of props ride in the schema itself:
1143
+ `element_type_get_schema` returns `renders_only_if`, `placeholder_fallback`,
1144
+ `blanked_by_parent`, `media_autoswap`, per-field `value_hint` / `no_effect` /
1145
+ `empty_means_inherit`, and a `rules` id list resolved by a `rule_notes` map
1146
+ (the `enable`-vs-render disagreements, the equal-height model, the `striptags`
1147
+ on link toggles, positional dependence, the collapse cascade, iframe video, the
1148
+ Maps API key, and link attributes under a lightbox). **Read those from the
1149
+ response, not from here.** The response also states which build they were read
1150
+ on — see `claims_verified_on` above.
1151
+
1152
+ What follows is the tail: quirks that belong to ONE element and would cost more
1153
+ in schema bytes than they are worth. Each was read in the YOOtheme **5.0.37**
1154
+ source at the cited location — so on a site running a different build, treat
1155
+ this section the way `claims_verified_on: "differs"` asks you to treat the
1156
+ schema's own claims: a report to check, not a fact about your build.
1157
+
1158
+ | Element / prop | The quirk |
1159
+ |---|---|
1160
+ | `grid.grid_divider` | The divider class is emitted only when the grid has **more than one child** (`grid/templates/template.php:44`, `=> count($children) > 1`). A one-item grid silently has no divider. |
1161
+ | `grid.grid_parallax_justify` | Setting it while `grid_parallax` is empty **forces `grid_parallax` to `'0'`** (`grid/templates/template.php:10-11`). You cannot have justify without parallax being on. |
1162
+ | `grid.panel_style: 'tile-checked'` | A **parent-only** value: the checked pattern comes from `uk-grid-checked` on the grid container, driven by the container's own `panel_style` (`grid/templates/template.php:56`; the render transform branches on it at `grid/element.php:118`). Setting it on an item does nothing. |
1163
+ | `grid_item.tags` | Inert unless the **parent grid's `filter`** is on: the whole tag-collection block runs inside `if (!empty($node->props['filter']))` (`grid/element.php:74`). Tags are also `strip_tags()`'d and their keys have **spaces replaced by dashes** (`:80-83`), so "New York" becomes the key `New-York`. |
1164
+ | `grid_item.title_color` vs `title_decoration` | `title_color: 'background'` and `title_decoration: 'line'` are decided by one `if`/`elseif` (`grid_item/templates/template-content.php:112-114`) — with `background` set, `line` never renders. Not additive. |
1165
+ | `grid.image_vertical_align` | Stops centering once `panel_expand` is active: the class carries `{@!panel_expand}` (`grid_item/templates/template.php:129`). |
1166
+ | `section` without `width` | Still emits a container when the section has a **video or an overlay** (`section/templates/template.php:158`), and that container then also gets `uk-panel` (`:167`) — which zeroes the last child's bottom margin. A width-less section is not always container-less. |
1167
+ | `slideshow` nav (not `thumbnav`) | Borrows the **slide's `title`** as the link text (`slideshow/templates/template-nav.php`: `$thumbnail ?: $child->props['title']`). An untitled slide gets an empty nav link. |
1168
+ | `accordion_item` | Declares **no `title_element`** at all (`grep -c title_element` = 0 in both `accordion_item/element.php` and `accordion/element.php`), so accordion titles can never carry a heading tag and never enter the heading outline. Do not try to write one. |
1169
+
1170
+ ---
1171
+
917
1172
  ## Workflow 2: Bind a dynamic source to a grid
918
1173
 
919
1174
  **Goal:** Wire an existing Grid (or other multi-item element) to a
@@ -1109,6 +1364,12 @@ CMS-level template duplication (in wp-admin or Joomla administrator).
1109
1364
  REPLACES the entire `props` object on the element unless you pass
1110
1365
  `merge: true`. Read the existing shape via `yootheme_builder_element_get`
1111
1366
  first if you only want to tweak a subset and prefer not to use merge.
1367
+ - **Reading a prop's value never needs a second call.** `element_get`'s
1368
+ output, both the JSON and the printed summary, lists every prop KEY
1369
+ together with its VALUE (e.g. `title_element: "h1"`), each value capped
1370
+ at roughly 200 characters with a visible `…(+N chars)` marker when
1371
+ truncated, never a silent drop. There's no need to cross-check via
1372
+ `element_list` just to see what a specific prop is currently set to.
1112
1373
  - **Clone-then-update path drift.** The clone returns a path that's
1113
1374
  correct at the moment of the call. If you fire off many ops in
1114
1375
  parallel, a concurrent edit may shift indices. Refresh via
@@ -1169,7 +1430,8 @@ guessing, and without rotating the user's key unnecessarily.
1169
1430
  auth) and then `/etag` (Bearer auth). Returns
1170
1431
  `{ plugin_reachable, plugin_version, yootheme_loaded, yootheme_version,
1171
1432
  endpoint_count, bearer_valid, bearer_error?, site_url?, home_url?,
1172
- summary? }`. Call this **before** any other tool when you see
1433
+ summary?, capability_ok?, capability_warning?, effective_capabilities? }`.
1434
+ Call this **before** any other tool when you see
1173
1435
  auth errors. (Takes no arguments. The schema is `{}`.)
1174
1436
  2. **Interpret the result:**
1175
1437
  - `plugin_reachable: false` → the WordPress / Joomla install is down
@@ -1191,6 +1453,14 @@ guessing, and without rotating the user's key unnecessarily.
1191
1453
  the tool's required scope (`write` for mutations, `admin` for
1192
1454
  destructive operations). Ask the user to regenerate the key with
1193
1455
  a higher scope and restart the AI client.
1456
+ - `plugin_reachable: true, bearer_valid: true, capability_ok: false` →
1457
+ the Bearer key's scope IS write, but the CMS user it resolves to
1458
+ (`effective_capabilities.can_edit`, `can_create`, `can_delete`, all
1459
+ false) has no write capability at all, so writes will still 403
1460
+ despite the write-scoped token. `capability_warning` carries the
1461
+ exact remediation sentence. Re-mint the key under a user with edit
1462
+ rights, or grant that user the capability. A write-scoped token
1463
+ alone is not enough.
1194
1464
  3. **Walk the user through key rotation if needed:**
1195
1465
  - WordPress: "wp-admin → Tools → YT Builder MCP → Bearer Keys."
1196
1466
  Joomla: "Components → YT Builder MCP → Bearer Keys."
@@ -1393,11 +1663,23 @@ exposes. Never analyze findings by hand and never bulk-apply a fix.
1393
1663
  `title-collision` never carry a `fix_hint` — they need a human
1394
1664
  decision (rename, merge, or intentionally keep as-is), not an
1395
1665
  automatic patch.
1396
- 3. `yootheme_builder_template_summary({ template_id })` drills into one
1397
- page a finding names. It carries the same `heading_levels`,
1398
- `images_missing_alt`, and `disabled_elements` counters the audit
1399
- used, so you can see exactly which nodes are affected before
1400
- writing.
1666
+ 3. `yootheme_builder_template_summary({ template_id, include_nodes: true })`
1667
+ drills into one page a finding names. It carries the same
1668
+ `heading_levels`, `images_missing_alt`, and `disabled_elements` counters
1669
+ the audit used, plus (with `include_nodes:true` opt-in, omit it for
1670
+ the smaller default) a per-node `content_nodes` breakdown (`path`,
1671
+ `type`, `status`, `effective_visible`, `hidden_on_mobile`, `headings`,
1672
+ `alts`), so you can see exactly WHICH node is affected, whether it
1673
+ is disabled or just hidden on mobile, in one call instead of pulling
1674
+ the raw `page_get_layout`. Page a large `content_nodes` list with
1675
+ `limit`/`cursor`; drill into several findings' pages in ONE call with
1676
+ `template_ids:[...]` instead of `template_id` (returns `summaries` keyed
1677
+ by id + `not_found`). `heading_levels` rides alongside a
1678
+ `heading_levels_note` sibling stating exactly which sources feed the
1679
+ census: every element whose title/meta channel resolves to a heading
1680
+ level, PLUS real `<h1>`-`<h6>` tags found inside `html`/`text`/`code`
1681
+ element markup. It does NOT count `module`/widget output — a WordPress
1682
+ widget or Joomla module has no content field this counter can parse.
1401
1683
  4. Fix that ONE page via the write tool named in `fix_hint.tool`, with
1402
1684
  `fix_hint.args_template` as a starting point (`element_update_settings`,
1403
1685
  `element_add`, etc), ETag-locked like every other write in this
@@ -1407,6 +1689,38 @@ exposes. Never analyze findings by hand and never bulk-apply a fix.
1407
1689
  with the same per-page codes, so you can confirm the fix landed
1408
1690
  without a second full sweep.
1409
1691
 
1692
+ **Disabling instead of editing.** Sometimes the right remediation is to
1693
+ exclude an element entirely rather than edit its content (a stray leftover
1694
+ section, a duplicate block). `yootheme_builder_element_set_status({
1695
+ template_id, element_path, status: false, etag })` writes `props.status:
1696
+ 'disabled'` — the SAME field YOOtheme's own native "Disable element"
1697
+ checkbox writes, so the element is excluded from BOTH the live frontend
1698
+ render AND the NEXT `pages_audit`/`template_summary` (both read the same
1699
+ `props.status`/`disabled_elements`/`content_nodes.status` fields, so the fix
1700
+ is visible without a DB round-trip). Pass `status: true` to re-enable it,
1701
+ which restores the YOOtheme default (no status field means active) rather
1702
+ than persisting a fabricated `props.status`.
1703
+
1704
+ **Need a real document outline instead of findings?** `pages_audit` tells you
1705
+ WHAT is wrong (a missing h1, a skipped level); it never shows you the whole
1706
+ heading tree in one look. `yootheme_builder_heading_outline({ template_id })`
1707
+ (read-only, reached via `yootheme_builder_advanced_read`) is a companion
1708
+ STRUCTURE view for ONE page: every H1-H6 heading in true document order, each
1709
+ carrying its TEXT and the element that carries it (`element_type`/`path` —
1710
+ `headline`, or the `html`/`text`/`code` element for a raw-HTML heading), plus
1711
+ the alt-status of every image on the page (`alt: null` for missing, plus
1712
+ `is_generic_alt` for a lazy "Logo"/"Bild"-style alt). Use it BEFORE editing a
1713
+ page's heading hierarchy by hand, or to double-check a `heading-skip` finding
1714
+ in context rather than as an isolated hit:
1715
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_heading_outline", arguments: { template_id: "page:4816" } })`
1716
+ returns `{outline, images, h1_count, has_level_skip, images_missing_alt}` —
1717
+ `has_level_skip` mirrors the `heading-skip` check's EXACT rule (a level more
1718
+ than one above the highest seen so far, including a document that opens below
1719
+ h1). Pass `mode: "rendered"` to read the ACTUAL rendered DOM instead of the
1720
+ saved layout tree (the same trade-off `pages_audit`'s own `mode` makes: sees
1721
+ module/widget output, but `element_type` degrades to `""` since there is no
1722
+ builder node to point at).
1723
+
1410
1724
  **Common pitfalls:**
1411
1725
 
1412
1726
  - **Promising a full SEO audit.** `pages_audit` covers Builder structure
@@ -1418,6 +1732,51 @@ exposes. Never analyze findings by hand and never bulk-apply a fix.
1418
1732
  It does not read or evaluate Yoast/RankMath/any SEO-plugin meta
1419
1733
  title, meta description, or Open Graph tags. Say this scope boundary
1420
1734
  out loud and point the user at their SEO plugin for that layer.
1735
+ - **Assuming builder props are the only content scanned.** Headings and
1736
+ image alt text are also detected inside raw HTML: an `html`/`text`
1737
+ element's markup content, not just builder props like
1738
+ `title`/`image_alt`. A sticky-bar `<h1>` or an alt-less `<img>` buried
1739
+ in raw markup is caught, not silently missed, unless the whole
1740
+ `content` prop is dynamically bound (a bound value is a stale editor
1741
+ preview and is skipped). Since T-A1, `code` joined `html`/`text` as a raw-HTML
1742
+ carrier, and anchor text + heading length/order are ALSO detected the same
1743
+ way: an empty/generic `<a>` link, a heading level skip, or an over-long
1744
+ heading buried in raw markup is caught by `generic-alt-text`/
1745
+ `anchor-text-quality`/`heading-skip`/`long-heading`, not just by
1746
+ `missing-h1`/`multiple-h1`/`no-headings`/`images-missing-alt`/`dead-anchor`.
1747
+ `module`/`module_position` (WordPress "Widget"/"Widget Area", Joomla
1748
+ "Module"/"Module Position") are NOT scanned - verified live that neither
1749
+ carries a markup-shaped `content` field on either platform; their actual
1750
+ content lives in an external WP widget instance / Joomla `#__modules` row,
1751
+ outside the layout JSON entirely.
1752
+ - **`mode:"rendered"` sees what the builder tree structurally cannot** (T-A2,
1753
+ 2026-07-24) — pass `mode:"rendered"` (default is `"builder"`, the tree read
1754
+ above) to instead render each page first (the SAME engine
1755
+ `yootheme_builder_render_preview` uses) and audit the ACTUAL rendered DOM.
1756
+ This is the one way to catch a `module`/widget's own runtime output — a
1757
+ newsletter-signup widget that itself renders an `<h1>`, dynamically-bound
1758
+ content, anything a plugin injects only at render time — none of which the
1759
+ builder-JSON walk above can see AT ALL. Only a curated subset of checks runs
1760
+ in this mode (`no-headings`, `missing-h1`, `multiple-h1`, `heading-skip`,
1761
+ `images-missing-alt`, `generic-alt-text`, `dead-anchor`, `anchor-text-quality`,
1762
+ `long-heading`, plus the three `design-*` markup codes — every check whose
1763
+ evidence is honestly derivable from a flat rendered document);
1764
+ `thin-content`/`duplicate-layout`/`empty-page`/`heading-hidden-on-mobile`/
1765
+ `non-pretty-url`/`stale-content` and both `header-contrast-*` codes still
1766
+ need the builder tree and do not run here (`header_transparent*` are section
1767
+ PROPS — they are gone by render time, so a rendered sweep can never produce
1768
+ those two findings). **Locators carry NO builder element
1769
+ path in this mode** — `breadcrumb`/`element_type` are empty strings BY
1770
+ DESIGN (there is no builder node to point at, not a resolution failure) —
1771
+ locate the hit by its `content_snippet` (the actual rendered text) or the
1772
+ page's `builder_url`/`frontend_url` instead. A site-wide rendered sweep is
1773
+ capped (rendering is expensive — each page runs the full YOOtheme Builder
1774
+ pipeline, not a JSON walk): the response always carries
1775
+ `render_scan: {attempted, rendered, capped}`, even when the cap never
1776
+ engaged, so you can always see whether every page was actually rendered.
1777
+ A page whose render failed is excluded from findings (never reported as a
1778
+ false "no headings") and listed in `render_errors` instead — check both
1779
+ fields before trusting a clean rendered-mode result.
1421
1780
  - **Confusing the CONTENT count with `pages_list.elements_count`.**
1422
1781
  `pages_list.elements_count` counts every node, including
1423
1782
  `section`/`row`/`column`/`fragment` layout wrappers. `pages_audit`
@@ -1587,6 +1946,88 @@ exactly one page, so there is nothing for a type filter to narrow). A `title`
1587
1946
  that matches several pages returns `title_ambiguous` with a candidate list —
1588
1947
  show it, let the user choose.
1589
1948
 
1949
+ ### Design-Consistency-Check (`checks: ["design"]`)
1950
+
1951
+ A fifth audit category, alongside `seo`/`structure`/`content`/`links`, in the
1952
+ DEFAULT-ALL set (no opt-in flag needed — just call `pages_audit` normally, or
1953
+ narrow to it with `checks: ["design"]`). It flags markup that breaks OUT of
1954
+ the site's UIkit design system — most commonly a **plugin's own output**
1955
+ (Formidable, ACF, a shortcode-rendered form) carrying none of YOOtheme's
1956
+ `uk-*` classes, e.g. a form submit button with no `uk-button`. Native
1957
+ YOOtheme builder elements (grid_item, panel, button, …) are never checked —
1958
+ the theme always renders them with the right `uk-*` class; there is nothing
1959
+ to flag there.
1960
+
1961
+ Three finding codes, all `warning` severity, none carrying a `fix_hint` (see
1962
+ below for why):
1963
+
1964
+ - **`design-missing-uikit-class`** — a contract-bearing element
1965
+ (button/select/textarea/input/`a[role=button]`/form/label-in-a-form) with
1966
+ no class at all, or one that already carries a DIFFERENT real `uk-*` class
1967
+ (on-system, just incomplete) — its expected class is simply absent.
1968
+ - **`design-foreign-class`** — the SAME element universe, but it carries
1969
+ class(es) and NONE of them are real UIkit classes — positive evidence a
1970
+ DIFFERENT framework (Bootstrap's `btn btn-primary`, a plugin's own
1971
+ `form-control`) is actively styling it instead of UIkit.
1972
+ - **`design-inline-style`** — ANY element (no tag restriction) with a
1973
+ non-empty `style=` attribute inside embedded content — bypasses the theme's
1974
+ design system entirely.
1975
+
1976
+ A FOURTH and FIFTH code share the `design` category but are not about markup at
1977
+ all — they read section props, not embedded HTML, so unlike the three above they
1978
+ are **builder-mode only** and never run in `mode: "rendered"` (the props do not
1979
+ exist in a rendered document):
1980
+
1981
+ - **`header-contrast-risk`** (`warning`) — the first section pulls the
1982
+ navigation over itself in a colour the theme's own `--uk-inverse` declaration
1983
+ contradicts, with no image or video hiding the background that declaration
1984
+ describes. Unlike the three above it DOES carry a `fix_hint`.
1985
+ - **`header-contrast-unverifiable`** (`info`) — the same transparent header over
1986
+ a hero medium, whatever the colour: nothing there is provable from the props,
1987
+ so no colour is recommended and readability is only verifiable by looking.
1988
+
1989
+ See their remediation-catalog entries below.
1990
+
1991
+ **Run the three markup codes in `mode: "rendered"` for the highest-value
1992
+ result.** A Formidable/ACF form's markup exists ONLY at render time — it is
1993
+ invisible to a builder-tree walk unless it happens to be typed static markup
1994
+ inside an `html`/`text`/`code` element. `mode: "rendered"` renders each page
1995
+ first (the SAME engine `render_preview` uses) and audits the ACTUAL output, so
1996
+ a plugin shortcode's real markup is exactly what gets checked. (The two
1997
+ `header-contrast-*` codes do NOT run in that mode — use the default
1998
+ `mode: "builder"` when you want them.)
1999
+
2000
+ ```jsonc
2001
+ yootheme_builder_advanced_read({
2002
+ tool: "yootheme_builder_pages_audit",
2003
+ arguments: { mode: "rendered", checks: ["design"], target: { special: "contact" } }
2004
+ })
2005
+ ```
2006
+
2007
+ **No `fix_hint` on any of the three codes — this is deliberate, not a gap.**
2008
+ The offending markup lives inside an opaque `props.content` string (an
2009
+ html/text/code element) or a plugin's own rendered output — there is no
2010
+ structured prop `element_update_settings` could merge a class into without
2011
+ string-surgery on customer HTML, which this server's read-only-analysis
2012
+ policy forbids on the write path. Report the locator's `content_snippet`
2013
+ (the offending element's OWN markup, e.g. `<button class="btn btn-primary">`
2014
+ — unlike every other finding's snippet, this one deliberately keeps the
2015
+ class/style attribute instead of stripping it, since that attribute IS the
2016
+ evidence) and tell the user to add the class by hand, or fix it in the
2017
+ plugin's own settings/template.
2018
+
2019
+ **Design-consistency fixes: ground the exact class via Context7 when it is
2020
+ available.** A `design-missing-uikit-class` or `design-foreign-class` finding
2021
+ asks you to name a real UIkit class by hand; recalling one purely from
2022
+ training data risks a subtly wrong suggestion, since class names can drift
2023
+ across UIkit versions. When the user's client has Context7 (or an equivalent
2024
+ documentation-grounding MCP) configured, query it (library id
2025
+ `/websites/getuikit`) to confirm the exact class before naming it in the fix.
2026
+ Without Context7, degrade cleanly: keep working from the tool's own output
2027
+ (the locator's `content_snippet`) plus the class names already catalogued
2028
+ above, and never invent a class name you have not confirmed one way or the
2029
+ other.
2030
+
1590
2031
  ### Remediation catalog — what each finding means and how to fix it
1591
2032
 
1592
2033
  The audit reports; YOU explain. Every finding below carries its fixed
@@ -1644,6 +2085,25 @@ second, independent heading channel), which is exactly what the raw path cannot.
1644
2085
  N times through an iterating container or a Multi-Items binding, so the fix is
1645
2086
  one edit on the item, not N edits; the locator's `×repeat` shows the multiplier.
1646
2087
 
2088
+ #### `heading-hidden-on-mobile` - structure · warning
2089
+
2090
+ **Why it matters.** Google indexes mobile-first: a heading that renders on
2091
+ desktop but is hidden at the base (phone) UIkit breakpoint is effectively
2092
+ invisible for ranking, even though it exists in the markup and a desktop
2093
+ review would never catch it.
2094
+ **How to fix.** No `fix_hint`. Hiding a heading on mobile is sometimes
2095
+ intentional (a duplicate mobile-only title elsewhere). Check the node's
2096
+ `props.visibility` (own or an ancestor's) and either remove the
2097
+ `hidden@`/`visible@` class or confirm an equivalent heading is visible on
2098
+ mobile some other way.
2099
+ **Severity.** Warning. Strict subset of ONLY the headline-TYPE portion of the
2100
+ pool `heading-skip` reads (since T-A1, `heading-skip` ALSO covers raw-HTML
2101
+ h1-h6 tags inside `html`/`text`/`code` elements - this check does not; a
2102
+ raw-HTML heading's mobile-visibility is not tracked): `hidden@<bp>` hides FROM
2103
+ that breakpoint upward (mobile stays visible), `visible@<bp>` is the mirror
2104
+ (hidden below it, mobile included), a bare `hidden` hides everywhere. Only the
2105
+ last two ever fire this check.
2106
+
1647
2107
  #### `heading-skip` — structure · info
1648
2108
 
1649
2109
  **Why it matters.** Jumping h1 → h3 breaks the document outline: assistive tech
@@ -1652,9 +2112,12 @@ reports a missing level, and the hierarchy stops describing the real structure.
1652
2112
  the jumped-to heading (`element_update_settings`, `props.title_element`) or add
1653
2113
  the missing intermediate level. The evidence lists each skip as
1654
2114
  `{ path, from, to }`.
1655
- **Severity.** Info: it is a real hierarchy defect but the page remains usable,
1656
- and the check is deliberately `headline`-only, so container-started hierarchies
1657
- are invisible to it. Do not present it as complete heading-hierarchy coverage.
2115
+ **Severity.** Info: it is a real hierarchy defect but the page remains usable.
2116
+ Reads BOTH `headline` elements AND raw-HTML h1-h6 tags inside `html`/`text`/
2117
+ `code` elements' `content` (interleaved in one true document-order sequence,
2118
+ since T-A1) - but container-STARTED hierarchies (e.g. a page whose only
2119
+ headings are 3× h3 grid-item titles with no h1/h2 anywhere) remain invisible
2120
+ to it. Do not present it as complete heading-hierarchy coverage.
1658
2121
 
1659
2122
  #### `images-missing-alt` — content · warning
1660
2123
 
@@ -1750,7 +2213,12 @@ prop is `title`. For an icon-only or image-only link, the right fix is
1750
2213
  `props.link_aria_label`, not invented visible text. Only link-bearing elements
1751
2214
  whose visible-text prop is known are checked — `button`, `button_item`,
1752
2215
  `grid_item`, `panel`, `headline`, `list_item`, `nav_item`, `subnav_item` — so
1753
- this is an allowlist, not a sweep over every element carrying a `link`.
2216
+ this is an allowlist, not a sweep over every element carrying a `link`. Since
2217
+ T-A1, a raw `<a href>` tag inside an `html`/`text`/`code` element's `content` is
2218
+ ALSO checked - that path judges every anchor with a set `href` (any
2219
+ destination, not just in-page), exempting an empty one only when it carries an
2220
+ `aria-label` or a nested `<img alt>` (the raw-HTML mirror of the allowlist's
2221
+ icon/image/aria-label exemption).
1754
2222
  **Severity.** Warning. The locator's `reason` says which rule fired: `empty` or
1755
2223
  `generic` (an **exact match** against the DE+EN generic list — `mehr`, `hier`,
1756
2224
  `weiterlesen`, `click here`, `read more`, `learn more`, …). Exact match only, so
@@ -1779,7 +2247,9 @@ the image actually shows ("SunWest Büro in Manresa").
1779
2247
  the EMPTY case). Only an **exact match** against the generic list (`image`,
1780
2248
  `photo`, `picture`, `logo`, `icon`, `bild`, `foto`, `grafik`) fires, so
1781
2249
  `"ACME Logo"` is already a good alt text and is never flagged. Bound alt/image
1782
- props are exempt.
2250
+ props are exempt. Since T-A1, a raw `<img alt="...">` inside an `html`/`text`/
2251
+ `code` element's `content` is checked the SAME way, judged against the
2252
+ identical generic list.
1783
2253
 
1784
2254
  #### `long-heading` — structure · info
1785
2255
 
@@ -1790,8 +2260,9 @@ keyword focus, get truncated in search results and wrap badly on mobile.
1790
2260
  detail into the body text below it. The locator carries the measured `length`.
1791
2261
  **Severity.** Info — a long heading is a quality signal, never a defect. The
1792
2262
  threshold is **70 characters**, measured multibyte-safely over the
1793
- markup-stripped text, on unbound static text only. Scope is deliberately narrow:
1794
- `headline` plus the standalone title carriers (`alert`, `overlay`, `panel`).
2263
+ markup-stripped text, on unbound static text only. Scope: `headline` plus the
2264
+ standalone title carriers (`alert`, `overlay`, `panel`), PLUS (since T-A1) every
2265
+ h1-h6 tag's own text inside an `html`/`text`/`code` element's raw `content`.
1795
2266
  Multi-Items containers keep their title text on the paired `*_item` children and
1796
2267
  are not measured — do not claim full heading-length coverage.
1797
2268
 
@@ -1799,6 +2270,157 @@ are not measured — do not claim full heading-length coverage.
1799
2270
  checks, so they also ride in the `quick_check` block on `page_publish` /
1800
2271
  `page_save` alongside the other per-page codes.
1801
2272
 
2273
+ #### `design-missing-uikit-class` — design · warning
2274
+
2275
+ **Why it matters.** A contract-bearing element (a form's submit button, its
2276
+ text inputs, its labels) with no `uk-*` class at all renders unstyled — it
2277
+ visually breaks out of the site's design system, most visibly on a
2278
+ plugin-rendered form (Formidable, ACF) dropped onto an otherwise polished
2279
+ page.
2280
+ **How to fix.** No `fix_hint` — see "Design-Consistency-Check" above for why.
2281
+ Add the missing class by hand (`uk-button` on a submit button, `uk-input` on
2282
+ a text input, `uk-form-label` on a label inside a form, `uk-form-stacked` or
2283
+ `uk-form-horizontal` on the form itself) — via the plugin's own class/CSS
2284
+ settings when it is a plugin form, or by editing the raw markup when it is a
2285
+ custom html/text/code element.
2286
+ **Severity.** Warning — a visual/consistency defect, not a functional one.
2287
+ Fires when the element carries NO class at all, or already carries a
2288
+ DIFFERENT real UIkit class (on-system, just incomplete) — never confuse this
2289
+ with `design-foreign-class` below.
2290
+
2291
+ #### `design-foreign-class` — design · warning
2292
+
2293
+ **Why it matters.** The SAME contract-bearing universe as
2294
+ `design-missing-uikit-class`, but here the element DOES carry class(es) and
2295
+ NONE of them are real UIkit classes — positive evidence that a DIFFERENT
2296
+ framework (Bootstrap's `btn btn-primary`, a plugin's own `form-control`) is
2297
+ actively styling it instead of UIkit. This is a REPLACEMENT problem, not a
2298
+ silence: the fix is a class swap, not an addition.
2299
+ **How to fix.** No `fix_hint`. Replace the foreign class with UIkit's
2300
+ equivalent (`btn btn-primary` → `uk-button uk-button-primary`), typically in
2301
+ the plugin's own template/class settings rather than in YOOtheme.
2302
+ **Severity.** Warning. The locator's `content_snippet` shows the element's
2303
+ own markup WITH its class attribute — the exact evidence, never tag-stripped
2304
+ like every other snippet in this audit.
2305
+
2306
+ #### `design-inline-style` — design · warning
2307
+
2308
+ **Why it matters.** A `style=` attribute inside embedded content (a
2309
+ `<div style="color:red">` in an html/text/code element, or in a plugin's
2310
+ rendered output) hard-codes styling that bypasses the theme's design system
2311
+ entirely — it cannot be themed, cannot respond to a style/color-mode change,
2312
+ and is invisible to any CSS audit that only looks at stylesheets.
2313
+ **How to fix.** No `fix_hint`. Move the styling into a UIkit class or the
2314
+ theme's own CSS, or remove it if it was accidental (a rich-text editor
2315
+ sometimes injects one). ANY tag can carry this finding — there is no
2316
+ tag restriction, unlike the two class-based checks above.
2317
+ **Severity.** Warning.
2318
+
2319
+ All three are `appliesTo: per-page` but — unlike `anchor-text-quality` /
2320
+ `generic-alt-text` / `long-heading` above — they deliberately do NOT ride the
2321
+ `quick_check` block on `page_publish` / `page_save`: `quick_check` is a
2322
+ publish-time advisory scoped to what a single-template pass can see without a
2323
+ live vocabulary, and design detection needs the site's actual compiled UIkit
2324
+ CSS. Run `pages_audit` (ideally `mode: "rendered"`) to see these three markup
2325
+ findings. The two `header-contrast-*` codes below skip `quick_check` for the
2326
+ same reason but are **builder-mode only** — `mode: "rendered"` never emits them.
2327
+
2328
+ #### `header-contrast-risk` — design · warning
2329
+
2330
+ **Why it matters.** The page's FIRST section pulls the navigation over itself
2331
+ (`header_transparent`) and forces a `header_transparent_text_color` the site's
2332
+ own theme does not declare for that section style. The forced colour wins —
2333
+ `.uk-inverse-light` / `.uk-inverse-dark` carry `!important` and beat the
2334
+ style's own declaration — so this is not a taste question: it is white
2335
+ navigation on a white hero, or dark navigation on a dark one. Nothing in the
2336
+ builder warns about it. See "Transparent header (the exception — never a
2337
+ default)" for the whole trap family.
2338
+
2339
+ **One class only, and it is decidable.** The page entry carries `risk_class`,
2340
+ `confidence`, `text_color`, `recommended_text_color`, `style` and
2341
+ `media_backdrop`:
2342
+
2343
+ - **`risk_class: "B"`, `confidence: "high"`** — no image or video covers the
2344
+ section background, so the contradiction is decidable from the layout alone.
2345
+ `recommended_text_color` is the theme's OWN declared inverse for that style
2346
+ (READ from the compiled `theme.css` — not a guess, not a computed contrast
2347
+ ratio).
2348
+
2349
+ **`pages_audit` never emits this code over a hero image or video.** A medium
2350
+ covers exactly the background `--uk-inverse` describes, so there is nothing to
2351
+ contradict and nothing to recommend; that state is the
2352
+ `header-contrast-unverifiable` info finding below, at one severity whatever the
2353
+ colour. A transparent header you do not see reported here is therefore *not*
2354
+ endorsed — look for the info finding.
2355
+
2356
+ **How to fix.** `fix_hint` → `header_transparency_set` with `mode: "auto"` and
2357
+ `dry_run: true`. The dry-run reports the current state, the theme-declared
2358
+ inverse, `risk_class`, `confidence` and exactly what would change; repeat with
2359
+ `dry_run: false` + an `etag` to write it. The tool resolves the FIRST section
2360
+ itself — you do not pass an `element_path`, and the repair therefore cannot
2361
+ land on a section whose props YOOtheme never reads. `mode: "auto"` writes the
2362
+ theme's own declared value, so no colour is ever guessed. Turning the
2363
+ transparent header off entirely is always the safe direction and is never
2364
+ refused (`transparent: false`).
2365
+
2366
+ **Severity.** Warning. It never blocks a write, and it is per-page but does NOT
2367
+ ride `quick_check` (same reason as the three design codes above: the verdict
2368
+ needs the site's compiled `theme.css`, which the publish-time pass does not
2369
+ read). **When that CSS cannot be read, or the theme declares nothing for that
2370
+ style, there is no finding at all** — an unreadable theme yields silence, never
2371
+ a guess. It is also **builder-mode-only**: `header_transparent*` are section
2372
+ props, so `mode: "rendered"` never emits this code — a clean rendered audit is
2373
+ NOT evidence that the header is fine.
2374
+
2375
+ **What it does NOT report.** A dead prop (a colour set while
2376
+ `header_transparent` is off) and props on a non-first section are not contrast
2377
+ risks and are not reported here — see the transparent-header section for those
2378
+ two traps.
2379
+
2380
+ #### `header-contrast-unverifiable` — design · info
2381
+
2382
+ **Why it matters.** The page's FIRST section pulls the navigation over itself
2383
+ AND carries a hero image or video —
2384
+ **whatever `header_transparent_text_color` holds**.
2385
+ Nothing here is provable: the theme's `--uk-inverse` declaration
2386
+ describes the section's BACKGROUND COLOUR, and the medium hides exactly that
2387
+ background, so "is the navigation readable?" cannot be answered from the layout
2388
+ at all. Which part of a `cover` image ends up under the header depends on the
2389
+ viewport.
2390
+
2391
+ `risk_class: "C"`, `confidence: "low"`, `recommended_text_color: null`.
2392
+
2393
+ **`null`, not `""` — this code recommends no colour at all.**
2394
+ `""` is not the humble answer: it is the theme's default colour, a concrete
2395
+ rendered value, so writing it back over a colour someone set deliberately (or
2396
+ measured in a browser) is a change, not an abstention. Over a dark hero photo
2397
+ on a `--uk-inverse: dark` style, `""` renders dark navigation on a dark image.
2398
+
2399
+ **One severity for the whole media case, deliberately.** An earlier revision
2400
+ graded it — `""` info, any other colour a warning — and that punished the
2401
+ correct action: the colour a browser measurement finds readable over a dark
2402
+ hero is usually the one the theme does NOT declare, so an eyes-verified page
2403
+ scored a warning whose remedy was to revert it. Behind a medium there is no
2404
+ baseline to deviate from, so there is nothing to grade.
2405
+
2406
+ **How to fix.** No `fix_hint`, because there is nothing to write. **Look at
2407
+ it** — render the page (see "Look at what you built") at the customer's
2408
+ viewport, unscrolled AND after scrolling, and read the navigation. If it is
2409
+ unreadable, set the colour that IS readable over that medium with
2410
+ `header_transparency_set({ mode: "explicit", text_color: … })` —
2411
+ a value you have seen, never one
2412
+ you inferred. `mode: "auto"` deliberately writes no colour here, so it cannot
2413
+ help you: it has not seen the page either. If it reads fine, say so and move on.
2414
+
2415
+ **The finding stays at `info` afterwards, and that is correct.** A transparent
2416
+ header over a medium is never provable from props, so the honest end state is
2417
+ "no warning, still only verifiable by eye" — not "clean". Report it as verified
2418
+ and move on; do not go looking for a colour that makes it disappear.
2419
+
2420
+ **Severity.** Info — an open question, not a defect. Same fail-open rule as the
2421
+ warning above: no readable `theme.css`, no finding. Same builder-mode-only
2422
+ restriction, same absence from `quick_check`.
2423
+
1802
2424
  ### The report contract — how to present an audit
1803
2425
 
1804
2426
  The raw response is a machine payload. What you hand the user follows this
@@ -1812,8 +2434,8 @@ shape every time, so two audits of two sites read the same way:
1812
2434
  2. **Order by severity: critical → warning → info.** The server already sorts
1813
2435
  that way; never re-rank findings by how easy they look to fix.
1814
2436
  3. **Group page → category → finding.** One block per affected page, its
1815
- findings grouped by `category` (`structure`, `content`, `seo`, `links`), so
1816
- the user can fix one page in one pass instead of hopping between pages per
2437
+ findings grouped by `category` (`structure`, `content`, `seo`, `links`,
2438
+ `design`), so the user can fix one page in one pass instead of hopping between pages per
1817
2439
  code.
1818
2440
  4. **Render every hit from its locator**, in this shape:
1819
2441
 
@@ -1832,7 +2454,13 @@ shape every time, so two audits of two sites read the same way:
1832
2454
  `element_type`) plus the `breadcrumb` that locates it in the builder. A **bare
1833
2455
  path list is NOT acceptable output** — `/children/0/children/2` tells the user
1834
2456
  nothing about which box on their page to click. Add the page's `builder_url`
1835
- as the "open in builder" link when the response carries one.
2457
+ as the "open in builder" link and its `frontend_url` as the "view live" link,
2458
+ whenever the response carries them — every finding page carries both today
2459
+ (previously only the nine element-located checks carried `builder_url`, and
2460
+ only `non-pretty-url` carried `frontend_url`). A theme/archive/internal
2461
+ template with no public route or no resolvable admin base legitimately omits
2462
+ one or both — that is not a bug, and it is not the agent's job to look the
2463
+ URL up separately via pages_list/pages_resolve when it is missing here.
1836
2464
  6. **State what did and did not run.** On a site sweep, say the site-wide checks
1837
2465
  ran (`duplicate-layout`, `title-collision` compare pages against each other).
1838
2466
  On a single-page audit, say they did not — reuse the response's own `note`
@@ -2087,6 +2715,493 @@ to re-read the entire layout after a wrong-path error.
2087
2715
  > `{given, nearest_path, nearest_path_label, available_paths}` envelope is
2088
2716
  > byte-identical across WordPress and Joomla.
2089
2717
 
2718
+ ## Response budgets — how a read tool shortens an oversized answer
2719
+
2720
+ Every high-volume read tool takes `max_chars`. When a response does not fit,
2721
+ it is **shortened by dropping whole entries, never by cutting the JSON**, and
2722
+ it tells you so. Read the verdict before you report a result as complete:
2723
+
2724
+ - `truncated: true` — this response is not the whole answer.
2725
+ - `omitted_count: N` — exactly how many entries were left out.
2726
+ - `next_cursor` — present whenever the resume point is derivable; pass it back
2727
+ as `cursor` to read the entries that were dropped. It is **omitted rather
2728
+ than guessed**: no cursor means the tool could not derive one, not that
2729
+ nothing is missing. Use `omitted_count` in that case. A cursor that could not
2730
+ be rewound past the dropped entries is **removed, never left stale**, so a
2731
+ `next_cursor` you receive is always safe to follow.
2732
+ - `omitted_keys: [...]` — on a single-object response, which fields were shed
2733
+ (the small identifying ones like `template_id` / `etag` are kept last).
2734
+ - `retry_hint` — the concrete lever: a higher `max_chars`, a narrower
2735
+ `fields[]` projection, or a smaller `limit`.
2736
+
2737
+ The payload always parses — on the plugin's own `/mcp` endpoint the text leg
2738
+ itself is reduced entry by entry, and through this Node server a shortened text
2739
+ preview always travels beside a complete `structuredContent` leg carrying the
2740
+ full result. So read `structuredContent` when the text leg looks cut. If you
2741
+ ever receive a response where **neither** leg parses, that is a bug — report it
2742
+ rather than working around it.
2743
+
2744
+ Both transports carry that structured leg, but **they do not put the same thing
2745
+ in it**. Over stdio it is the complete result. The plugin's own `/mcp` endpoint
2746
+ puts the **same shortened payload in both legs** — reading its `structuredContent`
2747
+ gives you exactly what its text leg already said, no more. That is a constraint,
2748
+ not a preference: on WordPress the host plugin's MCP adapter builds both legs
2749
+ from one returned value, so a bounded text leg beside a complete structured one
2750
+ cannot exist there. Decide whether an answer is whole from `truncated` /
2751
+ `omitted_count`, never from which leg you happened to read.
2752
+
2753
+ The reverse is also true: **absence of `truncated` means the answer is
2754
+ complete.** Do not hedge a full response with "there may be more".
2755
+
2756
+ Two levers beat raising `max_chars`, because they reduce what is produced
2757
+ rather than what survives: ask for fewer fields (`fields[]`) and fewer rows
2758
+ (`limit` + `cursor`). Reach for `max_chars` last.
2759
+
2760
+ Every default is derived from measured response sizes, not chosen, and the same
2761
+ default applies on both transports — the proxy and the plugin's own `/mcp` are
2762
+ two paths to one tool (see `docs/mcp-response-budgets.md`). A derived class is
2763
+ applied **raise-only**: measurement may lift a default, never lower it, so no
2764
+ upgrade silently returns less than the version before it.
2765
+
2766
+ ### Ask for the fields you need — `element_type_get_schema`
2767
+
2768
+ A big element type is the sharpest case: `grid` declares **154** field
2769
+ descriptors. Do not read all of them and do not raise the cap — **name what you
2770
+ want**:
2771
+
2772
+ - `field_names: ["image_expand","grid_masonry"]` — exact names, case-insensitive.
2773
+ - `name_contains: "height"` — substring of the field's **name OR label**, so it
2774
+ also finds `image_expand`, whose label is "Height".
2775
+ - Both together **UNION** — each argument names what you want; adding one never
2776
+ removes what the other selected.
2777
+
2778
+ A selected descriptor comes back **COMPLETE** — its `enable` condition, `enum`
2779
+ and `default` are all there, which is exactly what a truncated read destroyed.
2780
+ Every response carries `field_count` (what the type declares) and
2781
+ `returned_count` (what you received), so a narrowed answer is never silent and
2782
+ you always know the size of what you did not ask for. A pattern that matches
2783
+ nothing returns an **empty list plus the counts, not an error**.
2784
+
2785
+ `field_names`/`name_contains` pick WHICH descriptors; `fields[]` picks the
2786
+ sub-keys of each one. They compose — selection runs first, so
2787
+ `name_contains:"height", fields:["name","enable"]` is a legitimate call.
2788
+
2789
+ ### Read a field's MEANING, not just its `label` — `text` and `description`
2790
+
2791
+ A descriptor carries YOOtheme's own two meaning-bearing keys, and on a
2792
+ **checkbox** they are not optional reading:
2793
+
2794
+ - **A checkbox's value is a boolean (`true`/`false`); its MEANING is in `text`,
2795
+ because `label` on a checkbox is the GROUP heading, not the option.** `grid`'s
2796
+ equal-height option is
2797
+ `{"name":"image_expand","type":"checkbox","label":"Height","text":"Expand image","enable":"!grid_masonry"}`.
2798
+ Read `label` alone and it says "Height: true/false" — which is not what the
2799
+ option does. The caption rendered next to the box is `text`: "Expand image".
2800
+ Same shape for `image_loading` ("Loading" / "Load image eagerly"), `panel_link`
2801
+ ("Link" / "Link panel") and `panel_match` ("Height" / "Match panel heights").
2802
+ - **`description` is the Builder's own tooltip** for the field, e.g.
2803
+ `grid.panel_content_width` → "Set an optional content width which doesn't
2804
+ affect the image if there is just one column."
2805
+
2806
+ Both keys are **absent when YOOtheme declares none**, so a descriptor without
2807
+ `text` means "this field has no caption", never "the caption was dropped".
2808
+
2809
+ ### When the field says nothing, the GROUP does — `group` and `groups[]`
2810
+
2811
+ **YOOtheme parks the semantics of a whole class of fields on the enclosing
2812
+ fieldset GROUP, not on the field.** So a descriptor with no `label` and no
2813
+ `description` is not undocumented — you are reading at the wrong level.
2814
+ `group` names the nearest enclosing group (the heading the Builder renders beside
2815
+ the field), and the schema's **root `groups[]`** states that group's own sentence
2816
+ once. Join the two on the group name:
2817
+
2818
+ `{"name":"image_width","type":"number","placeholder":"auto","enable":"show_image || show_video","group":"Width/Height"}`
2819
+
2820
+ `"groups":[{"name":"Width/Height","description":"Setting just one value preserves the original proportions. The image will be resized and cropped automatically, and where possible, high resolution images will be auto-generated."}]`
2821
+
2822
+ Read the field alone and `image_width` looks like a plain number. The rule you
2823
+ actually needed — **set only ONE of width/height and the proportions are kept;
2824
+ set BOTH and the image is CROPPED** — lives on the group shared by `image_width`
2825
+ and `image_height`. Same shape for the `lightbox_image_*` pair.
2826
+
2827
+ - The sentence is the **group's**, never re-labelled as the field's, so a field
2828
+ may carry its own `description` while its group carries another at once —
2829
+ they are two different statements. It sits at the root because it is shared:
2830
+ measured over nine element types, 32 fields carried only 5 distinct sentences,
2831
+ one of them 20 times.
2832
+ - `groups[]` **narrows with your selection** — a `field_names` /`name_contains`
2833
+ read carries only the groups its descriptors name, never the whole type's.
2834
+ - `placeholder` is `attrs.placeholder`, which for several fields IS the
2835
+ documented fallback: `grid.image_width` → `"auto"`, `countdown.label_days` →
2836
+ `"Days"`. An empty value renders the placeholder, it does not render empty.
2837
+
2838
+ ### What an enum VALUE does not say — `enum_labels`
2839
+
2840
+ `enum` carries the values you write. `enum_labels` carries the Builder's label for
2841
+ a value **where the value does not already carry it**:
2842
+
2843
+ `{"name":"grid_default","type":"select","label":"Phone Portrait","enum":["1","2","3","4","5","6","auto"],"default":"1","enum_labels":{"1":"1 Column","2":"2 Columns","3":"3 Columns","4":"4 Columns","5":"5 Columns","6":"6 Columns"}}`
2844
+
2845
+ Without it, `["1"…"6"]` is six digits with nothing saying they are **column
2846
+ counts**; `grid_column_gap`'s `"collapse"` is a word with nothing saying the
2847
+ Builder calls it **None**.
2848
+
2849
+ - **A value with no entry is not unlabelled.** Its label is the value itself, up
2850
+ to case and punctuation — `"small"` is "Small", `"arrow-right"` is
2851
+ "Arrow Right". Only genuine renamings are listed, which is why the key is
2852
+ affordable: measured over nine element types, 611 of 2043 label/value pairs
2853
+ (29.9 %) are genuine renamings and the other 1432 are silent.
2854
+ - **`""` is a real choice, not a gap.** YOOtheme uses the empty string as a
2855
+ meaningful selection — `panel.video_autoplay` declares `'On' => ''`,
2856
+ `switcher_item.item_element` declares `'div' => ''` — and since an empty value
2857
+ carries no characters at all, its label is always in `enum_labels`
2858
+ (`{"":"On"}`). So `""` in an `enum` is a selection to make.
2859
+ - **A missing entry can also mean "not decidable".** When two options collapse
2860
+ onto the same value — `'On' => ''` beside `'No' => false`, because `false`
2861
+ stringifies to `""` — neither label is emitted for that value rather than one
2862
+ guessed. The same value in a DIFFERENT field may legitimately mean something
2863
+ else: in `grid`, `""` carries twelve different labels across its fields, and
2864
+ `"1-2"` is "50%" on one field and "Halves" on another. Read `enum_labels` per
2865
+ field; never carry one field's label to another.
2866
+
2867
+ ### A prop that is declared and does nothing — `no_effect`
2868
+
2869
+ A few props are declared in YOOtheme's config, ship a label and a tooltip, and
2870
+ are **read by no consumer**. For those the descriptor drops `text`/`description`
2871
+ — forwarding a promise nothing keeps is worse than silence — and carries
2872
+ `no_effect` with what was actually read instead:
2873
+
2874
+ `{"name":"lightbox_image_orientation","type":"checkbox","label":"Image Orientation","no_effect":"No effect in YOOtheme Pro 5.0.37: declared in grid/element.php and gallery/element.php only, read by no template. Writing it changes nothing.","enable":"lightbox","group":"Lightbox"}`
2875
+
2876
+ **A descriptor with `no_effect` is not a prop to set** — the write succeeds and
2877
+ nothing changes. The other one today is `panel-slider.show_hover_video`, dead
2878
+ because `panel-slider_item` lists `hover_image` twice and `hover_video` never.
2879
+ The prop stays writable on purpose: a stored layout may already carry the key,
2880
+ and rejecting it would break a round-trip. `enable` and `label` survive too —
2881
+ a Builder-UI condition and a UI name are not behaviour claims.
2882
+
2883
+ ### Which build a claim was read on — `claims_verified_on` and `no_effect_unverified`
2884
+
2885
+ That evidence was read in **one** YOOtheme tree. A site running a different
2886
+ build is a site nobody has read, so the response says which is which:
2887
+
2888
+ `"claims_verified_on":{"build":"5.0.37","here":"4.5.33","status":"differs","keys":["rule_notes","no_effect_unverified"]}`
2889
+
2890
+ - **`status:"match"`** — the claims were read on the build you are running.
2891
+ Take them as fact.
2892
+ - **`status:"differs"`** — read on `build`, not re-checked on `here`.
2893
+ - **`status:"unknown"`** — the site's YOOtheme version could not be resolved
2894
+ (`here` is `null`). It never means "probably the same".
2895
+
2896
+ `here` is resolved per host — from YOOtheme's runtime on WordPress, from the
2897
+ installed `yootheme` template manifest on Joomla — and on both it is the SAME
2898
+ value `yootheme_builder_health` reports as `yootheme_version`. So the two must
2899
+ agree: **if health names a build while this record says `unknown`, that is a
2900
+ resolver bug, not a fact about your site.** Check health first.
2901
+
2902
+ Off a `match`, `no_effect` **is not emitted at all**. The finding ships under
2903
+ **`no_effect_unverified`** instead, naming both builds and ending in `UNKNOWN`,
2904
+ and `text`/`description` **stay** — those are YOOtheme's own words read from
2905
+ *your* build's live registry, so they are true for you, and only a verified
2906
+ deadness earns their removal. Every `rule_notes` entry is likewise prefixed
2907
+ `[Read in YOOtheme Pro <build>. … — unverified here.]`.
2908
+
2909
+ **`differs` does not mean the claim is false on your build** — it means nobody
2910
+ looked. Treat `no_effect_unverified` as a strong reason to check before relying
2911
+ on the prop, not as permission to assume it works.
2912
+
2913
+ **The record scopes exactly the keys it lists in `keys` — nothing else in the
2914
+ response.** It is emitted when, and only when, that list is non-empty, so scope
2915
+ and trigger are one thing and cannot drift apart. What can appear there:
2916
+
2917
+ | Key | Why it is build-scoped |
2918
+ |---|---|
2919
+ | `renders_only_if`, `blanked_by_parent`, `media_autoswap` | render gates read in one tree |
2920
+ | `rule_notes` | the shared render-rule texts, all read in that same tree |
2921
+ | `no_effect` / `no_effect_unverified` | whichever of the two actually shipped |
2922
+ | `value_hint` | **only** when the hint is one of the render-template reads: a value-FORMAT trap (`countdown.date`, `map_item.location`, `map.width`/`height`, `popover_item.position_x`/`_y`, `table.table_order`) or `image_expand` |
2923
+
2924
+ Everything else is **outside** it, whatever its own provenance — including
2925
+ `binding_contract`, whose `child_type` comes from a container/item table read on
2926
+ a *different* YOOtheme build, and `groups`, `enum_labels`, `semantic_role` and
2927
+ the descriptors themselves, which come from your own build's live registry.
2928
+ `placeholder_fallback` is outside for that second reason: it is derived from your
2929
+ own config, so it is true here by construction.
2930
+
2931
+ A `value_hint` that is **not** named in `keys` carries no single-build scope — the
2932
+ image/icon shape hints, the grid-layout string and the type scale state a
2933
+ convention rather than a render reading, and `title_element`'s per-type defaults
2934
+ were verified against **both** the 4.5.33 and the YT5 element sources. Do not read
2935
+ `keys` as "these are the only claims in the response"; read it as "these are the
2936
+ claims this record vouches for."
2937
+
2938
+ ### Read what no field text can say — `value_hint`
2939
+
2940
+ `text` and `description` are YOOtheme's own words. `value_hint` is ours, and it
2941
+ carries only what lives in the element's **render template** — a rule no field
2942
+ definition can state. The equal-height option is the sharpest case, and it is
2943
+ **per element**:
2944
+
2945
+ - **A `grid` is ALREADY column-matched with no option set** — its container always
2946
+ renders `uk-grid-match` (sole exception: `grid_masonry`). Switching
2947
+ `image_expand` on to equalise the **columns** solves a solved problem: what it
2948
+ expands is the **IMAGE**. The media element takes `flex:1`, and because every
2949
+ `img` renders with `object-fit:cover`, the added height **crops** the picture.
2950
+ - **The order matters and the wrong order fails SILENTLY.** `image_expand` needs
2951
+ its gate set FIRST: `panel_match` on `panel-slider`, `height_expand` on
2952
+ `panel`, `grid_masonry` OFF on `grid` and `gallery`. Otherwise the element's
2953
+ template **blanks the value at RENDER** — the write succeeds and the value stays
2954
+ stored, so there is no error anywhere. **A field's `enable` condition does not
2955
+ protect you**: it is a Builder-UI condition only — no render path reads it, so it
2956
+ never guards an API write. Read the field's own `value_hint` for that element's exact condition
2957
+ (most gates have an `image_align: left`/`right` escape; `gallery` has none).
2958
+ - On the two sliders the render can also **FORCE it on**. With `slider_width`
2959
+ unset, `panel-slider` forces `panel_match` and `image_expand` to true, and
2960
+ `overlay-slider` forces `image_expand` to true — as it also does whenever
2961
+ `height_expand`, `slider_min_height` or `height_viewport` is set. There an
2962
+ `image_expand: false` write is accepted and stays stored, then replaced by true
2963
+ at **RENDER**: on those settings the option cannot be switched off at all.
2964
+ - **The fixed-size alternative:** set **BOTH** `image_width` and `image_height` —
2965
+ both numeric crops server-side to exactly that box, one alone only resizes
2966
+ proportionally.
2967
+
2968
+ `name_contains: "expand"` returns that one descriptor with the whole rule set.
2969
+
2970
+ ### Will this element render at all? — `renders_only_if`, `placeholder_fallback` and `blanked_by_parent`
2971
+
2972
+ The schema root carries three arrays that decide whether a write is visible **at
2973
+ all**. They are element-level, so they are present on **every** read of that type
2974
+ — a `field_names` selection does not narrow them away.
2975
+
2976
+ **`renders_only_if` — an element with PRESENT-but-EMPTY required props is DELETED at render.**
2977
+ Not hidden, not empty: removed from the tree, with no error anywhere. It is a
2978
+ list of groups, and **ALL groups need at least one non-empty member**:
2979
+
2980
+ `"renders_only_if":[["title","meta","content","image","video","icon"]]` ← `grid_item`
2981
+
2982
+ `"renders_only_if":[["title"],["content","image","link"]]` ← `accordion_item`, which needs a title **and** one of the three
2983
+
2984
+ > **Read `placeholder_fallback` before concluding "nothing renders".** For props
2985
+ > you simply never wrote, the answer on **27 of the 28 gated element types** is
2986
+ > not deletion — see the next subsection. `renders_only_if` alone over-states it.
2987
+
2988
+ - **`link` is almost never a keeper.** It is absent from the gate of `grid_item`,
2989
+ `gallery_item`, `list_item`, `table_item`, `description_list_item` and `panel`,
2990
+ so a card carrying **only a link plus link text renders nothing at all**.
2991
+ `social_item` is the one element where a bare `link` is the whole gate — which
2992
+ is why this is a per-element read, never a blanket rule.
2993
+ - **The emptied parent goes too.** A container whose children all dropped is
2994
+ itself dropped, cascading column → row → section. So one unsatisfied gate on a
2995
+ repeated item can blank a whole section.
2996
+ - A member may be a `&&`-joined conjunction, and a `parent.` prefix addresses the
2997
+ **container's** prop: `switcher_item`'s second group lists
2998
+ `"title&&parent.show_title"`, i.e. with the container's `show_title` off a title
2999
+ alone does not satisfy it — meta, content or image is then required.
3000
+ - Media-only elements are the mirror image: `overlay`, `overlay-slider_item`,
3001
+ `slideshow_item` and `gallery_item` need an image or video and drop a text-only
3002
+ item.
3003
+
3004
+ **`placeholder_fallback` — an UNSET prop does not delete the element; it publishes
3005
+ YOOtheme's placeholder to the live page.** This is the difference between
3006
+ *absent* and *present-but-empty*, and absent is the normal shape of an MCP write:
3007
+
3008
+ `"placeholder_fallback":{"suppressed_by":["title","meta","content","image","video","icon","hover_image","hover_video"],"fills":["title","content"]}` ← `grid_item`
3009
+
3010
+ - **`fills` is what your visitors see.** YOOtheme merges the element type's own
3011
+ placeholder props *before* the gate is evaluated, on the **front end** as well
3012
+ as in the customizer, so the gate is satisfied by content you never wrote:
3013
+ lorem ipsum, the literal string `"Title"`, `"Headline"`, the icon `"star"`, a
3014
+ demo date, or `assets/images/element-image-placeholder.png`. On `overlay`, for
3015
+ instance, YOOtheme injects **both** its placeholder image **and** the literal
3016
+ text `"Title"` into the published page.
3017
+ - **`suppressed_by` is your off switch.** Write **any** key in that list —
3018
+ including to `""` — and the merge is skipped entirely for the whole element.
3019
+ Only then does `renders_only_if` delete. It is all-or-nothing: one written key
3020
+ suppresses every fill, not just its own.
3021
+ - **So there are three outcomes, not two.** Leave the props unset → placeholder
3022
+ content ships. Write one gate prop with real content → your content ships.
3023
+ Write a gate prop as `""` → the element is deleted. If you want an element
3024
+ gone, delete it; do not rely on leaving props unset.
3025
+ - **`social_item` is the one gated type with no placeholder at all**, so no key
3026
+ is present and an absent `link` really does drop the node. Absence of
3027
+ `placeholder_fallback` therefore means the gate is literal.
3028
+
3029
+ **`blanked_by_parent` — a container's `show_*` flag is NOT CSS visibility, and it
3030
+ is NOT a write either.** Both halves matter, and a reader who takes only one acts
3031
+ wrongly. It **empties the child's prop in the render transform**, so the value is
3032
+ gone from the **rendered HTML** while **the stored layout still holds it,
3033
+ unchanged** — flip the flag back and the render returns. The emptied value then
3034
+ feeds the child's own `renders_only_if`, in the same function, so clearing one
3035
+ flag can delete the **entire item**, not just the field you meant to hide:
3036
+
3037
+ `"blanked_by_parent":{"show_title":["title"],"show_image":["image","icon"],"show_link":["link"], …}`
3038
+
3039
+ - **MEASURED, YOOtheme Pro 5.0.37** — a two-item `grid`, written with
3040
+ `element_update_settings` + `page_publish`, read back with `page_get_layout`
3041
+ and from the published page. With `show_title: false` on the grid: rendered
3042
+ `.el-title` **2 → 0**, neither title string anywhere in the section HTML, and
3043
+ the grid's stored children subtree **byte-identical** before and after (equal
3044
+ sha256, 699 B, `diff` exit 0) — the whole-layout diff was **one added line**,
3045
+ `"show_title": false`, on the GRID node and nothing on any child. Setting it
3046
+ back to `true` restored the byte-identical baseline render. So **re-writing the
3047
+ props is never the fix; flipping the flag back is.**
3048
+ - **The whole item really does go — and takes its container.** In the state where
3049
+ the blanking left only `link` (which is not in `grid_item`'s gate), `.el-item`
3050
+ went **2 → 0** and the `.uk-grid` container with it (**2 → 1**; section HTML
3051
+ 2221 → 609 chars) — while the read-back still carried `link` and `link_text` in
3052
+ full. Nothing rendered; nothing lost.
3053
+ - **A source-bound item behaves the same — measured, not assumed.** With
3054
+ `grid_item[0]` bound to a native source and `grid_item[1]` left literal,
3055
+ `show_title: false` blanked **both** (`.el-title` 4 → 0), and the read-back
3056
+ still showed the binding intact (`source.props.title`) on the bound item and
3057
+ the literal string on the static one. For a bound prop the surviving stored
3058
+ value is the **binding**, and it survives exactly as a literal does. Measured
3059
+ on the REST write path and the published page only — the customizer's own
3060
+ tick-and-save path was not measured, and nothing here is claimed about it.
3061
+ - **Intersect the two arrays to see the blast radius.** Where a blanked prop also
3062
+ appears in the gate, the item can vanish — true for nine of the thirteen item
3063
+ types that blank. In `nav_item`, `subnav_item`, `slideshow_item` and `map_item`
3064
+ it cannot: their gate is `content` / `content` / `image`+`video` / `location`,
3065
+ which no `show_*` blanks.
3066
+ - **`show_image` also wipes `icon`** on `grid_item`, `panel-slider_item`,
3067
+ `list_item`, `nav_item` and `subnav_item` — the array shows it, so read the
3068
+ values rather than assuming one flag clears one prop.
3069
+ - **Some flags blank from the TEMPLATE, not the transform** — `map_item`'s five
3070
+ and `switcher_item`'s `show_title`, which is why `switcher_item` carries seven
3071
+ flags and not the six its transform loop enumerates. These empty the value for
3072
+ **rendering** but do not feed the element's own gate, so do **not** intersect
3073
+ them with `renders_only_if`: `switcher_item`'s gate reads `title` raw in its
3074
+ first group and consults `show_title` itself in the second
3075
+ (`"title&&parent.show_title"`), and `map_item`'s gate is `location`, which no
3076
+ flag touches.
3077
+ - `table_item`, `social_item` and `button_item` have no blanking at all. They
3078
+ still have gates.
3079
+ - On `description_list_item`, `link` renders **only** as a wrapper around
3080
+ `content`, so losing `content` loses the link too — by a different route than
3081
+ the flag.
3082
+
3083
+ ### `""` means INHERIT, not off — `empty_means_inherit`
3084
+
3085
+ A descriptor with `"empty_means_inherit":true` merges over its **container's**
3086
+ value. Because the empty string is falsy, writing it — which is what the
3087
+ Builder's **"None"** option stores — **re-inherits the parent** instead of
3088
+ switching the feature off. Item overrides are therefore **add-only**: a single
3089
+ item cannot be forced back to plain inside a styled container. **To make one item
3090
+ plain you must restyle the container, not the item.**
3091
+
3092
+ It is on **43 fields across 12 element types**, so read the flag rather than
3093
+ guessing the family: the styling props (`panel_style`, `*_text_color`, `media_*`)
3094
+ on `grid_item`, `gallery_item`, `panel-slider_item`, `overlay-slider_item` and
3095
+ `slideshow_item`, but also `link_text` on **ten** item types and
3096
+ `link_aria_label` on **eight** — **eleven** types carry one or the other,
3097
+ including `accordion_item`, `switcher_item` and `table_item` (`link_text` only)
3098
+ and `social_item` (`link_aria_label` only) — plus `list_item`'s `icon` and
3099
+ `image_svg_color` and `popover_item`'s `drop_position`. An empty `link_text` on a
3100
+ `table_item` shows the TABLE's link text, not no text.
3101
+
3102
+ ### Which media actually renders — `media_autoswap`
3103
+
3104
+ Image/video collisions are resolved **per element, and YOOtheme is not
3105
+ consistent about it**. Read the key; there is no default to assume.
3106
+
3107
+ `"media_autoswap":{"precedence":["video","image","icon"],"sniffed":["image","video","hover_image","hover_video"]}`
3108
+
3109
+ - **`precedence`** — the first non-empty prop renders; the losers are simply not
3110
+ drawn. All seven builder media elements (`grid_item`, `panel`,
3111
+ `panel-slider_item`, `gallery_item`, `overlay`, `overlay-slider_item`,
3112
+ `slideshow_item`) are **video-first**, so setting both image and video shows the
3113
+ video.
3114
+ - **`sniffed`** — these props are re-classified by URL sniffing and the value is
3115
+ **MOVED** between `image` and `video`, nulling the other. **The field you wrote
3116
+ may not be the field that renders**: an `.mp4` in `image` ends up in `video`.
3117
+ - **`clears_on_conflict`** — `section` and `column` **invert** the precedence
3118
+ (image-first) **and** are destructive: with an image set, `video` is set to
3119
+ `false`, so anything derived from it downstream sees no video. These two are the
3120
+ only elements that do this.
3121
+
3122
+ ### `rules` + `rule_notes` — the rules that need a sentence
3123
+
3124
+ The four keys above are arrays you can evaluate. Nine more rules have no array
3125
+ form, so they ship as prose — **stated once per response and referenced by id**,
3126
+ never repeated per prop:
3127
+
3128
+ ```json
3129
+ { "name": "slideshow",
3130
+ "rules": ["expand_model", "positional", "enable_is_ui_only"],
3131
+ "rule_notes": { "expand_model": "…", "positional": "…", "enable_is_ui_only": "…" },
3132
+ "fields": [ { "name": "slideshow_max_height", "enable": "!height_viewport",
3133
+ "rules": ["slideshow_max_height_vs_render"] } ] }
3134
+ ```
3135
+
3136
+ - `rules` on the **element** = rules about the container's own template. They
3137
+ survive a `field_names` / `name_contains` selection, because they are true of
3138
+ the element whichever descriptors you asked for.
3139
+ - `rules` on a **field** = rules about that prop. Their notes are narrowed away
3140
+ when the field is not returned, so a two-descriptor read pays for two.
3141
+ - `rule_notes` resolves every id the response still references, and only those.
3142
+
3143
+ **`enable` and `show` are Builder-UI conditions only — they are not enforced.**
3144
+ In YOOtheme 5.0.37 the word `enable` appears **0×** in the theme's
3145
+ `packages/builder/src`, so no PHP render path reads either key and neither one
3146
+ guards an API write. They are still projected verbatim (they are the best
3147
+ statement of intent available), but they are wrong in **both** directions: a
3148
+ value the UI would refuse to let you edit is still stored and can still take
3149
+ effect, and a value whose `enable` was satisfied can still be dropped or
3150
+ overridden at render. Every prop where the render is known to disagree carries
3151
+ its own rule id — `slideshow_max_height_vs_render`, `thumbnav_ui_locked`,
3152
+ `nav_grid_needs_two`, `overlay_slider_two_positions`,
3153
+ `panel_slider_show_hover_video_dead` — plus the five-way `image_expand` split,
3154
+ which is in that field's `value_hint`.
3155
+
3156
+ That last one is the shape to expect from the rest: on `panel-slider`,
3157
+ `show_hover_video` is a **dead switch**. Its three sibling containers (`grid`,
3158
+ `gallery`, `overlay-slider`) blank the item's `hover_video` when it is off;
3159
+ `panel-slider`'s item lists `hover_image` twice and `hover_video` not at all, so
3160
+ the hover video keeps rendering. Prop, label and default are identical to the
3161
+ three that work — nothing in the descriptor can reveal it. Clear the item's
3162
+ `hover_video` prop instead.
3163
+
3164
+ Four rules to know before you write anything:
3165
+
3166
+ - **Equal height is not one switch** (`expand_model`). A `grid` already matches
3167
+ its column heights with nothing set; a `row` **never** does; a `column` matches
3168
+ only under a specific prop combination, and setting a row height **removes**
3169
+ matching from every column. `image_expand` expands the image, not the layout —
3170
+ the content-expand lever is a `*_margin_auto` checkbox.
3171
+ - **A link toggle silently strips HTML** (`link_striptags`). With `panel_link` /
3172
+ `overlay_link` / `card_link` on and a `link` present, YOOtheme's own renderer
3173
+ runs `striptags()` over `title`, `meta` **and** `content`. The stored value
3174
+ keeps the markup, so the loss is invisible in a read-back. This is YOOtheme's
3175
+ renderer, not this server's, and it cannot be switched off.
3176
+ **`list_item` is worse and gets its own id** (`link_striptags_ungated`): there
3177
+ is **no checkbox** to turn off — as soon as the item has a `link`, `content` is
3178
+ stripped, on both the with-image and the without-image render branch. To keep
3179
+ markup there, leave `link` empty or move the markup to an element that is not
3180
+ link-wrapped.
3181
+ - **A lightbox or modal drops the four link attributes**
3182
+ (`modal_drops_link_attrs`). `link_target`, `link_download`,
3183
+ `link_rel_nofollow` and `link_rel_noreferrer` are emitted only in the
3184
+ plain-link render branch. With `lightbox` on and a link present, an earlier
3185
+ branch wins and all four vanish — on `image`, and on `grid` and `gallery`
3186
+ (whose item templates have the identical shape; the four props are declared on
3187
+ the container). `button_item` does the same for a `dialog` with
3188
+ `dialog_layout` `modal`/`offcanvas` and no `link`. Open-in-new-tab, download
3189
+ and rel hints cannot be combined with a lightbox.
3190
+ - **An emptied container disappears with it** (`collapse_cascade`). A container
3191
+ that HAD children and ends up with none is removed — the mark is keyed on the
3192
+ element type's own `container` flag, and **21 element types** declare it in
3193
+ 5.0.37, so `grid`, `gallery`, `slideshow` and the rest go exactly the way a
3194
+ `column` does. Items dropped by their own render gates therefore cascade
3195
+ upwards and a page can lose a whole section with no error anywhere. Only
3196
+ `column` offers `prevent_collapse`.
3197
+
3198
+ A field's **value format** is stated on the field itself, in `value_hint` — a
3199
+ value that does not parse is usually removed or defaulted silently, never
3200
+ rejected. `countdown.date` must be `strtotime()`-parseable or the element
3201
+ disappears; `map_item.location` must be numeric `"lat,lng"` or the marker is
3202
+ skipped (no geocoding); an empty `map.height` becomes 300; a non-numeric
3203
+ `popover_item.position_x`/`_y` becomes 50 %.
3204
+
2090
3205
  ## Dynamize an existing section (designed layout → live data, one call)
2091
3206
 
2092
3207
  **Goal:** Take a section you (or the customer) already designed by hand
@@ -2705,58 +3820,62 @@ perform.
2705
3820
 
2706
3821
  <!-- TOOL-CATALOG:BEGIN -->
2707
3822
 
2708
- **49 catalogued tools** plus the `yootheme_builder_advanced` (write) + `yootheme_builder_advanced_read` (read-only) gateways = **51 reachable via `tools/list`** (37 L1 + 2 L3 + 2 gateways = 41 advertised; the gateways route to 10 additional advanced tools, bringing the total to 51 callable). Generated by `scripts/extract-tools.mjs` from the compiled `buildAllTools()` registry. Do not hand-edit this section; re-run `npm run build && node scripts/extract-tools.mjs` after changing tool definitions.
3823
+ **53 catalogued tools** plus the `yootheme_builder_advanced` (write) + `yootheme_builder_advanced_read` (read-only) gateways = **55 callable** end to end, of which **42 are advertised in `tools/list`** (38 L1 + 2 L3 + 2 gateways); the remaining 13 advanced tools are reachable only through the gateways. Generated by `scripts/extract-tools.mjs` from the compiled `buildAllTools()` registry. Do not hand-edit this section; re-run `npm run build && node scripts/extract-tools.mjs` after changing tool definitions.
2709
3824
 
2710
3825
  | Tool | Kind | Input keys | Description |
2711
3826
  | --- | --- | --- | --- |
2712
- | `yootheme_builder_clean_implode_directives` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Use when a Multi-Items element renders rows as ONE comma-joined string instead of N repeated items — typically stale `implode` directives left by a plugin upgrade (detect via yootheme_builder_inspect_multi_items_binding). Strips `props.source.props.*.implode` directives from an element binding. Returns audit log + new ETag. Idempotent (cleaned_count: 0 when nothing to remove) but destructive: the binding can't be restored without re-mapping. Ask the user to confirm, then call with `confirm: true`. Requires ETag. Operates on the default site unless site_id is provided. |
2713
- | `yootheme_builder_diagnose` | read+idempotent | `site_id` | Full diagnostic: /health + authenticated /etag probe. Returns site_url, home_url, plugin reachability, Bearer validity in one call. First call when you need to know where the site lives. For per-template URLs see pages_list. Operates on the default site unless site_id is provided. |
2714
- | `yootheme_builder_element_add` | mutating | `children`, `element_type`, `etag`, `index`, `name`, `parent_path`, `position`, `props`, `site_id`, `template_id`, `type_name` | Add a new element to a template. Provide `parent_path` (or "" for root), `element_type` (e.g. "headline", "text", "grid"), and optional `props` / `children`. Content/leaf elements may only be added inside a `column`; only `section` is valid at the root — call yootheme_builder_scaffold_section first for a valid column path. Appends by default; pass `index` (0 = first) to insert. Inserting renumbers later siblings — re-read paths via element_list after any structural write. Returns the new element's JSON-Pointer path. Requires ETag. Operates on the default site unless site_id is provided. |
2715
- | `yootheme_builder_element_bind_source` | idempotent | `arguments`, `bindingLevel`, `directives`, `element_path`, `etag`, `field`, `field_mappings`, `query_arguments`, `relation`, `site_id`, `source_id`, `source_name`, `template_id` | Binds a Builder source to an element (canonical shape: `source` top-level). Pass `field_mappings` in the SAME call; bindingLevel "item" binds a container's first *_item child. Leaf under an iterating *_item → CONSOLIDATE onto item slots (409 slot_collision if taken/not a slot); leaf under a #parent fragment → INHERIT (`__node_item__`); standalone renders empty. List on a grid_item with children → iterator_is_leaf_renderer. Map: bind `map_item`, map `location` to a "lat,lng" field or no markers. Requires ETag. Call directly. Operates on the default site unless site_id is provided. |
2716
- | `yootheme_builder_element_clone` | mutating | `element_path`, `etag`, `site_id`, `template_id` | Clone an element as a sibling (same parent, immediately after the source). Returns the new element's path. Cloning renumbers later siblings — re-read paths via element_list after any structural write. Requires ETag. Operates on the default site unless site_id is provided. |
2717
- | `yootheme_builder_element_delete` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | PERMANENTLY delete an element + children. Cannot be undone. Pass `confirm: true` (boolean) AND a fresh ETag. Deleting renumbers later siblings — re-read paths via element_list after any structural write. If `preview:true` returns despite confirm, check `details.received_confirm` — strings/numbers accepted, but boolean is canonical. Operates on the default site unless site_id is provided. |
2718
- | `yootheme_builder_element_get` | read+idempotent | `element_path`, `fields`, `max_chars`, `site_id`, `template_id` | Get the full element object at a specific JSON-Pointer path, including props and a `children_count` (NOT the children themselves — use yootheme_builder_element_list with root_path to enumerate children). `fields[]` narrows the returned object; `max_chars` bounds the text output. Address by template_id only — resolve a URL/post first via pages_resolve. Operates on the default site unless site_id is provided. |
2719
- | `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Read the source binding attached to an element. Always returns {template_id, element_path, binding, has_binding, field_mapping_count, warning?}: `binding` is the raw source binding (bound source name, field-mappings, query args/directives); `has_binding` is false and `field_mapping_count` 0 when the element is not bound. Operates on the default site unless site_id is provided. |
2720
- | `yootheme_builder_element_list` | read+idempotent | `cursor`, `depth`, `fields`, `limit`, `max_chars`, `root_path`, `site_id`, `template_id` | List elements in a template as a flat array with JSON-Pointer paths + types + `label` (the builder-assigned name; "" when unset — not that the type lacks one). Scope with `root_path`/`depth` for a subtree, paginate with `limit`/`cursor` for large templates. `fields[]` narrows each row; `max_chars` bounds the text output. template_id only — resolve a URL/post first via pages_resolve. Operates on the default site unless site_id is provided. |
2721
- | `yootheme_builder_element_move` | idempotent | `element_path`, `etag`, `site_id`, `template_id`, `to_index`, `to_parent_path` | Move an element to a new parent + index in the tree. Useful for reordering or reparenting (e.g. moving a card from one grid column to another). Moving renumbers later siblings — re-read paths via element_list after any structural write. Requires ETag. Operates on the default site unless site_id is provided. |
2722
- | `yootheme_builder_element_set_source` | idempotent | `element_path`, `etag`, `site_id`, `source`, `source_extended`, `template_id` | Surgically SET a precomposed `source` (+ optional `source_extended.props`) on ONE element at a pointer, ETag-locked (blast-radius 0). Use this — not element_bind_source — when you have the exact source shape OR need `source_extended.props`, the native Multi-Items item-mapping carrier bind_source cannot write. E.g. FAQ accordion scoped to term: query.name="categoryFaqs.taxonomyCategoryFaq", field={name:"faq"}, source_extended.props→#node. Element-prop keys the type does not render echo back as `unknown_prop_keys` (non-fatal). ETag req. Operates on the default site unless site_id is provided. |
2723
- | `yootheme_builder_element_type_get_schema` | read+idempotent | `element_type`, `fields`, `max_chars`, `site_id`, `type_name` | Fetch a type's prop schema before element_add/element_update_settings: unknown prop keys are hard-rejected (error lists valid_prop_keys + did_you_mean). Returns each field as {name,type,label?,enum?,value_hint?,group?}, field_count, and a binding_contract for container/item types. Read value_hint+enum first some defaults are SEO-fatal (headline.title_element→h1); checkbox = boolean true/false; group:"runtime-accepted" = honoured at render but hidden in the builder UI. Bound size with fields[]/max_chars. Operates on the default site unless site_id is provided. |
2724
- | `yootheme_builder_element_types_list` | read+idempotent | `fields`, `site_id` | List element types registered on this site (built-ins + YOOessentials/uEssentials extras). Names feed `element_type` of element_add. Pass `fields[]` to narrow each row. Keywords: "slider"/"carousel" `slideshow`/`panel-slider`/`overlay-slider`. Prop keys differ per element (grid_item `title` vs headline `content`) use element_type_get_schema before element_add/bind. Container rows carry `requires_child_type` (grid→grid_item); `has_children`/`has_children_support` are aliases (same value). Operates on the default site unless site_id is provided. |
2725
- | `yootheme_builder_element_unbind_source` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Remove the source binding from an element. Clears the element's canonical top-level `source` key AND the legacy `props.source`. Destructive in the sense that it may break dynamic-content rendering always ask the user to confirm. Requires ETag. Operates on the default site unless site_id is provided. |
2726
- | `yootheme_builder_element_update_settings` | idempotent | `element_path`, `etag`, `merge`, `name`, `props`, `site_id`, `template_id`, `unset` | Update `props` on an element. Default replaces all props; pass `merge:true` for server-side deep-merge (only request keys overwritten, others survive avoids read-modify-write races). Requires ETag. Pass `unset:["key",…]` to reset props to their YOOtheme default by removing them. Operates on the default site unless site_id is provided. |
2727
- | `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current ETag (state revision) for the YOOtheme builder. Returns a sha256+revision string for optimistic locking. This ETag is SITE-WIDE: it covers the ENTIRE builder state, so ANY write to ANY template (by any client) bumps it a 412 means the site state advanced, not necessarily that YOUR template changed. It is the SAME token the read tools (page_get_layout, element_list) return in their `etag`. Pass it back as `etag` on any write tool; on 412, re-read and retry. Operates on the default site unless site_id is provided. |
2728
- | `yootheme_builder_health` | read+idempotent | `site_id` | Check plugin installed/reachable. Returns plugin version, YT Pro version, REST endpoints. Authenticated payload adds site_url + home_url for deep-linking. See yootheme_builder_diagnose for Bearer-validity + connectivity summary. Operates on the default site unless site_id is provided. |
2729
- | `yootheme_builder_image_import` | idempotent | `site_id`, `url` | Sideload a photo into the site's own Media Library. Accepts ANY public image URLit does NOT require a Library API key, so it still works when image_search is unavailable (typically pass a `full_url` from yootheme_builder_image_search). Returns `{imported:true, local_url}` where local_url is a SITE-RELATIVE path (e.g. /wp-content/uploads/...). Bind it via element_update_settings({props:{image: local_url}, merge:true}). A caught import failure returns `{imported:false, reason}`, never a 500. Operates on the default site unless site_id is provided. |
2730
- | `yootheme_builder_image_search` | read+idempotent | `color`, `orientation`, `per_page`, `query`, `site_id`, `source` | Search real on-topic photos (Unsplash/Pexels) or YOOtheme Pro Images to fill layout image slots with REAL, on-brand photography instead of bare placeholders. Returns candidate rows with `full_url` (pass straight to yootheme_builder_image_import) plus dimensions/author/orientation. `total:0` usually means no Library API key is configured (the same theme apikey library_browse/insert use). Operates on the default site unless site_id is provided. |
2731
- | `yootheme_builder_inspect_multi_items_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Reports Multi-Items binding state: container/item pair (grid↔grid_item, slideshow↔slideshow_item, map↔map_item, …), the binding level (none\|container\|item), and a recommended_fix when it sits on the container. For map/map_item, a bound item without a `location` ("lat,lng") field is flagged as a blocking map_location_warning (renders 0 markers). A List on a grid_item with nested children → blocking iterator_is_leaf_renderer (children never render). If rows render as ONE comma-joined string, see clean_implode_directives. Operates on the default site unless site_id is provided. |
2732
- | `yootheme_builder_inspect_sublayout` | read+idempotent | `element_path`, `site_id`, `template_id` | Inspects a panel/grid_item `< SUBLAYOUT`: lists the `fragment` subtree bindable leaves (with JSON-Pointer paths to bind on NOT the container) and the render-time inner grid. Flags `ungated_grid_prop` (blocking): grid props set but the gate (title_align=left / image_align=left\|right) unsatisfied silent no-op. For container↔item binding see inspect_multi_items_binding. Operates on the default site unless site_id is provided. |
2733
- | `yootheme_builder_layout_query` | read+idempotent | `cursor`, `fields`, `has_binding`, `limit`, `max_chars`, `max_depth`, `name_matches`, `site_id`, `template_id`, `type`, `under_section` | Find elements in a template by a structured filter (no expression language). AND-combine any of: `type` (one type or a list), `name_matches` (case-insensitive substring of the element name), `has_binding` (bound vs unbound), `max_depth` (cap descent 0 = top-level), `under_section` (only nodes inside a named section). template_id only — resolve a URL/post first via pages_resolve. Returns matching rows {path, element_type, label, has_binding}; paginate with `limit`/`cursor`, narrow rows with `fields[]`, bound text with `max_chars`. Operates on the default site unless site_id is provided. |
2734
- | `yootheme_builder_library_browse` | read+idempotent | `cursor`, `element_type`, `has_effects`, `kind`, `limit`, `purpose`, `query`, `role`, `site_id`, `source`, `topic`, `type`, `website` | Browse the YOOtheme Pro Library catalog: pre-designed page LAYOUTS, element PRESETS, or purpose-classified SECTIONS (hero/features/cta/...). Filter with `kind` ("layout" default \| "element" \| "section"), `topic`, `type`/`role`, `element_type`, `purpose`/`has_effects` (sections only), `website`, `query`. Returns `{items, total, next_cursor}`; a "section" id is the composite "<layoutId>#<sectionIndex>" feeds straight into library_get/library_insert. `total:0` usually means no Library API key configured. Operates on the default site unless site_id is provided. |
2735
- | `yootheme_builder_library_delete` | destructive | `confirm`, `id`, `site_id`, `source` | Delete ONE saved entry from your "WootsUp AI Layouts" store (the isolated library yootheme_builder_library_save_as writes into) — clean up experimental sections/layouts the agent saved. Pass the `id` from a library_browse(source:"ai") row. `source` defaults to "ai", the ONLY deletable store; the curated library is read-only, so source:"catalog" returns a 403. IRREVERSIBLE requires confirm:true. Returns `{deleted, id, deleted_count, source}`; an unknown id returns a 404. Advanced/gateway-only. Operates on the default site unless site_id is provided. |
2736
- | `yootheme_builder_library_get` | read+idempotent | `id`, `include_thumbnail`, `kind`, `site_id`, `source` | Fetch a SINGLE YOOtheme Pro Library layout or element preset by `id` call after yootheme_builder_library_browse (items[].id feeds this). Returns `{id, kind, name, version, tree, sections, images_manifest_count}`: `tree` is the raw JSON; `sections` lists `tree.children` (layouts only, presets return `[]`), each `{index, name, path, element_count}`. `include_thumbnail:true` also returns an image block (costs tokens see its own description). Read-only; does not insert/bind. Operates on the default site unless site_id is provided. |
2737
- | `yootheme_builder_library_insert` | mutating | `etag`, `id`, `images`, `kind`, `section_index`, `site_id`, `source`, `target` | Insert a YOOtheme Pro Library item (layout, section, or preset) onto a page call after yootheme_builder_library_get/browse. `target`: new_page (creates a page), {template_id,element_path?} (splices onto the layout root, or a preset onto a Column), or sublayout (splices rows into a fragment). A preset (kind:"element") MUST target a Column — 422 otherwise; scaffold one first with yootheme_builder_scaffold_section. `images`: "placeholder" (default) or "curated" (downloads to Media Library). Optional `etag` locks (412 if stale). Operates on the default site unless site_id is provided. |
2738
- | `yootheme_builder_library_save_as` | mutating | `destination`, `name`, `site_id`, `source` | Save a page or subtree into a reusable library the REVERSE of yootheme_builder_library_insert. `source`: {template_id, element_path?} (omit element_path to save the WHOLE page). `destination`: "ai" (default your isolated "WootsUp AI Layouts", reusable via library_browse/insert source:"ai") or "native" (YOOtheme's built-in My Layouts). Returns `{library_id, name, storage, invalid_source_possible}` (the last flags bound sources that may need re-binding after reload). Advanced/gateway-only. Operates on the default site unless site_id is provided. |
2739
- | `yootheme_builder_local_content_read` | read+idempotent | `fields`, `include_children`, `kind`, `limit`, `max_chars`, `offset`, `parent_id`, `search`, `site_id`, `status`, `taxonomy`, `type` | Read native records for one `kind`: entities (posts/pages/CPT/articles), terms (TERMS ids that archive-scope a Grid), users, fields (ACF/com_fields defs), menus, or media. Published-only + paginated: {items,total,next_offset}; pass next_offset back as `offset`. Archive-scoping flow: read {kind:"terms",taxonomy:"category"} pick a term id bind a source, pass query_arguments:{id:<term_id>}. CALL local_content_types FIRST for valid type/taxonomy. E.g. {kind:"entities",type:"product",search:"shoe"}. Slim with `fields[]`. Operates on the default site unless site_id is provided. |
2740
- | `yootheme_builder_local_content_types` | read+idempotent | `fields`, `kind`, `max_chars`, `site_id` | Discover the site's NATIVE content landscape post types/articles, taxonomies and custom-field groups (ACF/com_fields), native to WP/Joomla (no API Mapper). Returns {content_types, taxonomies, field_groups, capabilities}; rows carry `bindable` + `source_name` (null for native types bind those with element_bind_source by the content-type `name`, e.g. "post"). CALL FIRST, then local_content_read for records. Narrow with `kind`, slim with `fields[]`. E.g. {kind:"content_types"}. Operates on the default site unless site_id is provided. |
2741
- | `yootheme_builder_page_dynamize` | mutating | `conditions`, `dry_run`, `element_path`, `etag`, `leaf_map`, `list_source_name`, `page`, `remove_leaves`, `section_name`, `site_id`, `source_map`, `template_id` | One-call way to dynamize a designed section: copies the layout byte-identically and swaps only what you map. Use INSTEAD of hand-editing JSON. Give a REFERENCE (section_name from template_summary, or element_path) + list_source_name + a leaf_map (original leaf field-name flat flow field; #parent + tuned props kept). A section with several source blocks dynamizes in ONE call via source_map ({match, source_name, leaf_map?}[]). Sub-query panels: key by "<subquery>.<leaf>". Returns page + `summary`. Preview with dry_run:true. Operates on the default site unless site_id is provided. |
2742
- | `yootheme_builder_page_get_layout` | read+idempotent | `cursor`, `fields`, `flat`, `force`, `max_chars`, `mode`, `post_id`, `site_id`, `template_id`, `url` | Get the layout tree for one template address it by `template_id` (from pages_list), `post_id`, or `url`. `mode:"bindings_only"` for dynamize/bind; `mode:"full"` (default) is nested `{layout, etag}`, `flat:true` a depth-first array. `max_chars` bounds the text; a clipped read returns `next_cursor` to page the rest. For a site-wide SEO/structure/content/links audit (h1, image alt, thin pages, dead links) do NOT loop this tool page-by-page call yootheme_builder_pages_audit (one sweep via yootheme_builder_advanced_read). Operates on the default site unless site_id is provided. |
2743
- | `yootheme_builder_page_get_schema` | read+idempotent | `fields`, `max_chars`, `post_id`, `site_id`, `template_id`, `url` | Get the flat schema for a template nodes with their JSON-Pointer paths and element types. Lighter than page_get_layout, enough to locate elements before editing. Address by `template_id` (pages_list), `post_id`, or `url`. `fields[]` slims each node; `max_chars` bounds output. Operates on the default site unless site_id is provided. |
2744
- | `yootheme_builder_page_publish` | idempotent | `etag`, `quick_check`, `site_id`, `template_id` | Publish a template persist state, flush YT + WP caches, snapshot the published-state ETag. YOOtheme templates already publish on save, so this is a cache-flush + state-snapshot op. Neither publish nor save changes the CMS post status a draft page stays invisible to visitors; set status at creation via pages_create/library_insert. ETag optional when provided, 412 on conflict; when omitted, last-write-wins. Operates on the default site unless site_id is provided. |
2745
- | `yootheme_builder_page_save` | idempotent | `dry_run`, `etag`, `layout`, `quick_check`, `site_id`, `template_id` | Re-run save-transforms + flush caches. Optionally REPLACE the template tree by passing the INNER {type:"layout",children:[...]} tree as `layout` page_get_layout's `.layout` child, NOT the whole response it returns (rejected 422, would blank the page). Validated + ETag-locked; nothing persists on reject. Does NOT change CMS post status a draft stays invisible to visitors; set status via pages_create. `dry_run:true` preflights; success carries `previous_layout` for rollback. ETag optional (412 on conflict; else last-write-wins). Operates on the default site unless site_id is provided. |
2746
- | `yootheme_builder_pages_audit` | read+idempotent | `checks`, `max_chars`, `site_id`, `stale_after_months`, `target`, `thin_content_threshold`, `types` | One-call site-wide SEO/structure/content/links audit severity-sorted findings with fix_hints (each a props FRAGMENT to merge, not a ready-made call supply template_id + path from the finding locator). Covers h1/headings, thin pages, image alt, duplicate layouts, title collisions, non-pretty URLs, dead anchors. A SITE-wide sweep caps hits/page and drops per-hit content_snippet; scope to ONE page via `target` for the full set (cap 100). Filter with `types`/`checks`. Disclaimer: SEO-plugin meta (title/description/OG) NOT covered. Operates on the default site unless site_id is provided. |
2747
- | `yootheme_builder_pages_create` | idempotent | `dry_run`, `layout`, `menu_item`, `published`, `site_id`, `slug`, `status`, `title` | Create a page (WordPress) or article (Joomla 5/6) with a YOOtheme per-page layout. Required: title. Optional: slug, status (default draft), published (default false live on true: WP publish / Joomla state 1), layout (JSON), menu_item (Joomla SEF route), dry_run (preflight, no persist). Returns page_id (WP) / article_id (Joomla), slug, template_id (page:{id} or article:{id}), etag, warnings. Joomla: a published article has NO SEF route until a menu item points at it — pass menu_item, else frontend_url may 404. Operates on the default site unless site_id is provided. |
2748
- | `yootheme_builder_pages_delete` | destructive | `confirm`, `force`, `post_id`, `site_id` | Delete a page (WordPress) or article (Joomla 5/6) with its YOOtheme per-page layout. Requires confirm:true (gate against accidental deletion). Default: trash (recoverable). force:true: permanent delete. Refuses non-page/article types with 403 (Layer 9 defense). Returns deleted:true, page_id (WP) / article_id (Joomla), force_deleted, bumped etag. Operates on the default site unless site_id is provided. |
2749
- | `yootheme_builder_pages_list` | read+idempotent | `cursor`, `fields`, `label_contains`, `limit`, `max_chars`, `site_id` | List pages, templates, and layouts in the YOOtheme Pro builder (id, label, type, elements_count, frontend_url, builder_url, serves_context_kind, serves_context_ids per row). CALL THIS FIRST to discover template IDs before page_get_layout / element_list / page_get_schema. Paginates: up to `limit` rows (default 50) plus `total` and a `next_cursor` (pass it back as `cursor`) when more remain. Filter with `label_contains`. Slim rows with `fields:["id","label"]`; bound output with `max_chars`. Operates on the default site unless site_id is provided. |
2750
- | `yootheme_builder_pages_resolve` | read+idempotent | `site_id`, `target`, `url` | Resolve a public page/article/archive URL to the template_id that renders it paste a URL, get the layout id. Pretty/SEF permalinks resolve on WordPress AND Joomla (incl. deep sub-category paths), not only raw ?option= URLs. A singular page its per-page layout; a category/tag/CPT/author archive the governing THEME TEMPLATE, and for a category/term the SPECIFICALLY-ASSIGNED template (honoring include-child-categories), not a generic one. Or a `target`. Returns {template_id, post_id, type, frontend_url, context, resolved_via}. Operates on the default site unless site_id is provided. |
2751
- | `yootheme_builder_rename_source` | idempotent | `dry_run`, `etag`, `field_map`, `from`, `site_id`, `source_map`, `template_id`, `to` | Template-wide RENAME of a source / type / field name across EVERY binding in a template, in ONE ETag-locked write preserving each binding's arguments, props and field_mappings. Use after a taxonomy/CPT rename (faq→help) breaks every bound layout. Pass `from`/`to` for one source, and/or `source_map`/`field_map` to rename a relation + its field tokens together; the dotted "<source>.<relation>" form is rewritten segment-by-segment. Returns {replaced_count, changed_paths, new_etag}. Preview with dry_run:true. ETag req. Operates on the default site unless site_id is provided. |
2752
- | `yootheme_builder_render_preview` | read+idempotent | `expect`, `post_id`, `site_id`, `template_id`, `url` | Render-check a saved template does it actually render, with how many items, or is it blank? Address by `template_id` (from pages_list), `post_id`, or `url` (resolved for you) exactly one. `item_count` = items yielded by the template's dynamic/Multi-Items bindings (rendered repeat-source rows); 0 with empty:true means a binding returned nothing. Returns {rendered, item_count, empty, error?, cache_purge_hint}; with `expect`, also {matched, snippet}. Use AFTER a save/bind to catch a silently-empty page. Read-only. Operates on the default site unless site_id is provided. |
2753
- | `yootheme_builder_scaffold_section` | mutating | `column_widths`, `columns`, `etag`, `site_id`, `template_id` | Create a VALID section row → column hull in ONE call; returns the column JSON-Pointer path(s) for content. The correct way to start a layout block: only `section` may sit at the layout root; a bare section accepts `row`, a bare row accepts `column`; leaf/content elements are rejected anywhere except inside a `column`. Pass `columns` (1-12, default 1); columns get width tokens so they render side-by-side (hand-built columns default to full width), or `column_widths` (one token per column) for an asymmetric row. Requires ETag. Operates on the default site unless site_id is provided. |
2754
- | `yootheme_builder_sites_list` | read+idempotent | `site_id` | List all sites configured in this multi-site MCP installation. Returns site_id + URL + platform (wordpress\|joomla) + default flag per row. CALL THIS FIRST when working with a fresh MCP connection to discover available site_ids before targeting one with any other tool. Read-only, no REST calls. Keywords: list sites, list connections, list installations, discover site_id, available sites, configured sites, what sites exist, multi-site index. The `default_site_id` is the write target when a call omits site_id. (site_id is accepted for schema-uniformity but ignored by this tool.) |
3827
+ | `yootheme_builder_clean_implode_directives` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Use when a Multi-Items element renders rows as ONE comma-joined string instead of N repeated items — typically stale `implode` directives left by a plugin upgrade (detect via yootheme_builder_inspect_multi_items_binding). Strips `props.source.props.*.implode` directives from an element binding. Returns audit log + new ETag. Idempotent (cleaned_count: 0 when nothing to remove) but destructive: the binding can't be restored without re-mapping. Ask the user to confirm, then call with `confirm: true`. Requires ETag. |
3828
+ | `yootheme_builder_diagnose` | read+idempotent | `site_id` | Full diagnostic: /health + authenticated /etag probe. Returns site_url, home_url, plugin reachability, Bearer validity in one call. First call when you need to know where the site lives. For per-template URLs see pages_list. When a write-scoped key resolves to a CMS user with no write capability, the response adds `capability_warning` and `effective_capabilities.can_edit:false` instead of writes failing with a blind 403. |
3829
+ | `yootheme_builder_element_add` | mutating | `children`, `element_type`, `etag`, `index`, `name`, `parent_path`, `position`, `props`, `site_id`, `template_id`, `type_name` | Add a new element to a template. Provide `parent_path` (or "" for root), `element_type` (e.g. "headline", "text", "grid"), and optional `props` / `children`. Content/leaf elements may only be added inside a `column`; only `section` is valid at the root — call yootheme_builder_scaffold_section first for a valid column path. Appends by default; pass `index` (0 = first) to insert. Inserting renumbers later siblings — re-read paths via element_list after any structural write. Returns the new element's JSON-Pointer path. Requires ETag. |
3830
+ | `yootheme_builder_element_bind_source` | idempotent | `arguments`, `bindingLevel`, `directives`, `element_path`, `etag`, `field`, `field_mappings`, `query_arguments`, `relation`, `site_id`, `source_id`, `source_name`, `template_id` | Binds a Builder source to an element (canonical shape: `source` top-level). Pass `field_mappings` in the SAME call; bindingLevel "item" binds a container's first *_item child. Leaf under an iterating *_item → CONSOLIDATE onto item slots (409 slot_collision if taken/not a slot); leaf under a #parent fragment → INHERIT (`__node_item__`); standalone renders empty. List on a grid_item with children → iterator_is_leaf_renderer. Map: bind `map_item`, map `location` to a "lat,lng" field or no markers. Requires ETag. Call directly. |
3831
+ | `yootheme_builder_element_clone` | mutating | `element_path`, `etag`, `site_id`, `template_id` | Clone an element as a sibling (same parent, immediately after the source). Returns the new element's path. Cloning renumbers later siblings — re-read paths via element_list after any structural write. Requires ETag. |
3832
+ | `yootheme_builder_element_delete` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | PERMANENTLY delete an element + children. Cannot be undone. Pass `confirm: true` (boolean) AND a fresh ETag. Deleting renumbers later siblings — re-read paths via element_list after any structural write. If `preview:true` returns despite confirm, check `details.received_confirm` — strings/numbers accepted, but boolean is canonical. |
3833
+ | `yootheme_builder_element_get` | read+idempotent | `element_path`, `fields`, `max_chars`, `site_id`, `template_id` | Get the full element object at a specific JSON-Pointer path, including props and a `children_count` (NOT the children themselves — use yootheme_builder_element_list with root_path to enumerate children). `fields[]` narrows the returned object; `max_chars` bounds the text output. Address by template_id only — resolve a URL/post first via pages_resolve. |
3834
+ | `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Read the source binding attached to an element. Always returns {template_id, element_path, binding, has_binding, field_mapping_count, warning?}: `binding` is the raw source binding (bound source name, field-mappings, query args/directives); `has_binding` is false and `field_mapping_count` 0 when the element is not bound. |
3835
+ | `yootheme_builder_element_list` | read+idempotent | `cursor`, `depth`, `fields`, `include_meta`, `limit`, `max_chars`, `root_path`, `site_id`, `template_id` | List elements in a template as a flat array with JSON-Pointer paths + types + `label` (the builder-assigned name; "" when unset — not that the type lacks one). Scope with `root_path`/`depth` for a subtree, paginate with `limit`/`cursor` for large templates. `fields[]` narrows each row; `max_chars` bounds the text output. template_id only — resolve a URL/post first via pages_resolve. |
3836
+ | `yootheme_builder_element_move` | idempotent | `element_path`, `etag`, `site_id`, `template_id`, `to_index`, `to_parent_path` | Move an element to a new parent + index in the tree. Useful for reordering or reparenting (e.g. moving a card from one grid column to another). Moving renumbers later siblings — re-read paths via element_list after any structural write. Requires ETag. |
3837
+ | `yootheme_builder_element_set_source` | idempotent | `element_path`, `etag`, `site_id`, `source`, `source_extended`, `template_id` | Surgically SET a precomposed `source` (+ optional `source_extended.props`) on ONE element at a pointer, ETag-locked (blast-radius 0). Use this — not element_bind_source — when you have the exact source shape OR need `source_extended.props`, the native Multi-Items item-mapping carrier bind_source cannot write. E.g. FAQ accordion scoped to term: query.name="categoryFaqs.taxonomyCategoryFaq", field={name:"faq"}, source_extended.props→#node. Element-prop keys the type does not render echo back as `unknown_prop_keys` (non-fatal). ETag req. |
3838
+ | `yootheme_builder_element_set_status` | idempotent | `element_path`, `etag`, `site_id`, `status`, `template_id` | Enable or disable an element via its `props.status` field the SAME field YOOtheme's own native "Disable element" checkbox writes. status:false excludes the element and its descendants from BOTH the live render and live findings; pages_audit and template_summary content_nodes both honour it. status:true re-enables it, restoring the YOOtheme default (no status field means active). Requires ETag. |
3839
+ | `yootheme_builder_element_type_get_schema` | read+idempotent | `element_type`, `field_names`, `fields`, `max_chars`, `name_contains`, `site_id`, `type_name` | Prop schema; read before element_add/element_update_settings: unknown keys rejected (error lists valid_prop_keys+did_you_mean). Field: {name,type,label?,text?,description?,enum?,value_hint?,group?,no_effect?} + groups[] + field_count/returned_count + binding_contract (containers/items). value_hint+enum first: some defaults are SEO-fatal (headline.title_element→h1); checkbox = boolean true/false; no_effect = read by nothing, never write it; group:"runtime-accepted" = honoured at render, hidden in UI. Select descriptors: field_names/name_contains. Bound size with fields[]/max_chars. |
3840
+ | `yootheme_builder_element_types_list` | read+idempotent | `fields`, `include_meta`, `site_id` | List element types registered on this site (built-ins + YOOessentials/uEssentials extras). Names feed `element_type` of element_add. Pass `fields[]` to narrow each row. Keywords: "slider"/"carousel" → `slideshow`/`panel-slider`/`overlay-slider`. Prop keys differ per element (grid_item `title` vs headline `content`)use element_type_get_schema before element_add/bind. Container rows carry `requires_child_type` (grid→grid_item); `has_children`/`has_children_support` are aliases (same value). |
3841
+ | `yootheme_builder_element_unbind_source` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Remove the source binding from an element. Clears the element's canonical top-level `source` key AND the legacy `props.source`. Destructive in the sense that it may break dynamic-content rendering always ask the user to confirm. Requires ETag. |
3842
+ | `yootheme_builder_element_update_settings` | idempotent | `element_path`, `etag`, `merge`, `name`, `props`, `site_id`, `template_id`, `unset` | Update `props` on an element. Default replaces all props; pass `merge:true` for server-side deep-merge (only request keys overwritten, others surviveavoids read-modify-write races). Requires ETag. Pass `unset:["key",…]` to reset props to their YOOtheme default by removing them. |
3843
+ | `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current ETag (state revision) for the YOOtheme builder. Returns a sha256+revision string for optimistic locking. This ETag is SITE-WIDE: it covers the ENTIRE builder state, so ANY write to ANY template (by any client) bumps it — a 412 means the site state advanced, not necessarily that YOUR template changed. It is the SAME token the read tools (page_get_layout, element_list) return in their `etag`. Pass it back as `etag` on any write tool; on 412, re-read and retry. |
3844
+ | `yootheme_builder_header_transparency_set` | idempotent | `acknowledge_contrast_risk`, `dry_run`, `etag`, `mode`, `pull_content_behind`, `site_id`, `template_id`, `text_color`, `transparent` | Inspect + repair the transparent header (nav over the hero). Always targets the FIRST sectionYOOtheme reads these props there only; later ones are silently dead. dry_run defaults TRUE: returns current state, the theme-declared --uk-inverse, risk_class, confidence, recommendation, would_change. mode:"auto" writes the theme-declared colour; over a hero image/video it writes NO colour and says to LOOK. mode:"explicit" refuses a colour contradicting the theme (409) unless acknowledge_contrast_risk. transparent:false is always allowed. |
3845
+ | `yootheme_builder_heading_outline` | read+idempotent | `mode`, `site_id`, `template_id` | A real, doc-order H1-H6 outline of ONE template: each heading with its TEXT and the source element that carries it (`element_type`/`path`), plus the alt-status of every image (including images inside html/text/code raw markup). Read-only STRUCTURE inspection, not findings; use yootheme_builder_pages_audit for severity-sorted SEO findings across the whole site. Returns {outline, images, h1_count, has_level_skip, images_missing_alt}. Address by `template_id` (from pages_list or pages_resolve). |
3846
+ | `yootheme_builder_health` | read+idempotent | `site_id` | Check plugin installed/reachable. Returns plugin version, YT Pro version, REST endpoints. available_endpoints is derived from the live route registration on both platforms, so every path listed is request-ready as printed. Authenticated payload adds site_url + home_url for deep-linking. See yootheme_builder_diagnose for Bearer-validity + connectivity summary. |
3847
+ | `yootheme_builder_image_import` | idempotent | `site_id`, `url` | Sideload a photo into the site's own Media Library. Accepts ANY public image URL it does NOT require a Library API key, so it still works when image_search is unavailable (typically pass a `full_url` from yootheme_builder_image_search). Returns `{imported:true, local_url}` where local_url is a SITE-RELATIVE path (e.g. /wp-content/uploads/...). Bind it via element_update_settings({props:{image: local_url}, merge:true}). A caught import failure returns `{imported:false, reason}`, never a 500. |
3848
+ | `yootheme_builder_image_search` | read+idempotent | `color`, `include_meta`, `orientation`, `per_page`, `query`, `site_id`, `source` | Search real on-topic photos (Unsplash/Pexels) or YOOtheme Pro Images to fill layout image slots with REAL, on-brand photography instead of bare placeholders. Returns candidate rows with `full_url` (pass straight to yootheme_builder_image_import) plus dimensions/author/orientation. `total:0` usually means no Library API key is configured (the same theme apikey library_browse/insert use). |
3849
+ | `yootheme_builder_inspect_multi_items_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Reports Multi-Items binding state: container/item pair (grid↔grid_item, slideshow↔slideshow_item, map↔map_item, …), the binding level (none\|container\|item), and a recommended_fix when it sits on the container. For map/map_item, a bound item without a `location` ("lat,lng") field is flagged as a blocking map_location_warning (renders 0 markers). A List on a grid_item with nested children blocking iterator_is_leaf_renderer (children never render). If rows render as ONE comma-joined string, see clean_implode_directives. |
3850
+ | `yootheme_builder_inspect_sublayout` | read+idempotent | `element_path`, `site_id`, `template_id` | Inspects a panel/grid_item `< SUBLAYOUT`: lists the `fragment` subtree bindable leaves (with JSON-Pointer paths to bind on NOT the container) and the render-time inner grid. Flags `ungated_grid_prop` (blocking): grid props set but the gate (title_align=left / image_align=left\|right) unsatisfied silent no-op. For container↔item binding see inspect_multi_items_binding. |
3851
+ | `yootheme_builder_layout_query` | read+idempotent | `content_contains`, `cursor`, `fields`, `has_binding`, `include_meta`, `limit`, `max_chars`, `max_depth`, `name_matches`, `site_id`, `template_id`, `type`, `under_section` | Find elements in a template via AND-combined filters: `type` (one type or a list), `name_matches` (case-insensitive substring of the name), `content_contains` (case-insensitive substring of `content`/`title`/`meta` props), `has_binding` (bound vs unbound), `max_depth` (cap descent 0 = top-level), `under_section` (inside a named section). template_id onlyresolve a URL/post first via pages_resolve. Returns rows {path, element_type, label, has_binding}; paginate `limit`/`cursor`, narrow `fields[]`, bound `max_chars`. |
3852
+ | `yootheme_builder_library_browse` | read+idempotent | `cursor`, `element_type`, `has_effects`, `include_meta`, `kind`, `limit`, `purpose`, `query`, `role`, `site_id`, `source`, `topic`, `type`, `website` | Browse the YOOtheme Pro Library catalog: pre-designed page LAYOUTS, element PRESETS, or purpose-classified SECTIONS (hero/features/cta/...). Filter with `kind` ("layout" default \| "element" \| "section"), `topic`, `type`/`role`, `element_type`, `purpose`/`has_effects` (sections only), `website`, `query`. Returns `{items, total, next_cursor}`; a "section" id is the composite "<layoutId>#<sectionIndex>" feeds straight into library_get/library_insert. `total:0` usually means no Library API key configured. |
3853
+ | `yootheme_builder_library_delete` | destructive | `confirm`, `id`, `site_id`, `source` | Delete ONE saved entry from your "WootsUp AI Layouts" store (the isolated library yootheme_builder_library_save_as writes into) clean up experimental sections/layouts the agent saved. Pass the `id` from a library_browse(source:"ai") row. `source` defaults to "ai", the ONLY deletable store; the curated library is read-only, so source:"catalog" returns a 403. IRREVERSIBLE requires confirm:true. Returns `{deleted, id, deleted_count, source}`; an unknown id returns a 404. Advanced/gateway-only. |
3854
+ | `yootheme_builder_library_get` | read+idempotent | `id`, `include_thumbnail`, `kind`, `site_id`, `source` | Fetch a SINGLE YOOtheme Pro Library layout or element preset by `id` call after yootheme_builder_library_browse (items[].id feeds this). Returns `{id, kind, name, version, tree, sections, images_manifest_count}`: `tree` is the raw JSON; `sections` lists `tree.children` (layouts only, presets return `[]`), each `{index, name, path, element_count}`. `include_thumbnail:true` also returns an image block (costs tokens see its own description). Read-only; does not insert/bind. |
3855
+ | `yootheme_builder_library_insert` | mutating | `etag`, `id`, `images`, `kind`, `section_index`, `site_id`, `source`, `target` | Insert a YOOtheme Pro Library item (layout, section, or preset) onto a page call after yootheme_builder_library_get/browse. `target`: new_page (creates a page), {template_id,element_path?} (splices onto the layout root, or a preset onto a Column), or sublayout (splices rows into a fragment). A preset (kind:"element") MUST target a Column 422 otherwise; scaffold one first with yootheme_builder_scaffold_section. `images`: "placeholder" (default) or "curated" (downloads to Media Library). Optional `etag` locks (412 if stale). |
3856
+ | `yootheme_builder_library_save_as` | mutating | `destination`, `name`, `site_id`, `source` | Save a page or subtree into a reusable library the REVERSE of yootheme_builder_library_insert. `source`: {template_id, element_path?} (omit element_path to save the WHOLE page). `destination`: "ai" (default your isolated "WootsUp AI Layouts", reusable via library_browse/insert source:"ai") or "native" (YOOtheme's built-in My Layouts). Returns `{library_id, name, storage, invalid_source_possible}` (the last flags bound sources that may need re-binding after reload). Advanced/gateway-only. |
3857
+ | `yootheme_builder_local_content_read` | read+idempotent | `fields`, `include_children`, `include_meta`, `kind`, `limit`, `max_chars`, `offset`, `parent_id`, `search`, `site_id`, `status`, `taxonomy`, `type` | Read native records for one `kind`: entities (posts/pages/CPT/articles), terms (TERMS ids that archive-scope a Grid), users, fields (ACF/com_fields defs), menus, or media. Published-only + paginated: {items,total,next_offset}; pass next_offset back as `offset`. Archive-scoping flow: read {kind:"terms",taxonomy:"category"} pick a term id bind a source, pass query_arguments:{id:<term_id>}. CALL local_content_types FIRST for valid type/taxonomy. E.g. {kind:"entities",type:"product",search:"shoe"}. Slim with `fields[]`. |
3858
+ | `yootheme_builder_local_content_types` | read+idempotent | `fields`, `kind`, `max_chars`, `site_id` | Discover the site's NATIVE content landscapepost types/articles, taxonomies and custom-field groups (ACF/com_fields), native to WP/Joomla (no API Mapper). Returns {content_types, taxonomies, field_groups, capabilities}; rows carry `bindable` + `source_name` (null for native types — bind those with element_bind_source by the content-type `name`, e.g. "post"). CALL FIRST, then local_content_read for records. Narrow with `kind`, slim with `fields[]`. E.g. {kind:"content_types"}. |
3859
+ | `yootheme_builder_navigation` | read+idempotent | `menu`, `site_id` | The site's navigation menus (WP nav menus / Joomla menus) as a parent -> children TREE, not a flat list, for "which page is under which menu item" (product pages usually live in the main menu). Each item carries a `page_role` facet (home\|landing\|product\|legal\|thankyou\|blog\|mockup\|template\|generic, same vocabulary as pages_list) and, when derivable at zero extra cost, a `target_page` (page:<id> / article:<id>) ready for yootheme_builder_page_get_layout. Read-only. Narrow to one menu with `menu`. |
3860
+ | `yootheme_builder_page_dynamize` | mutating | `conditions`, `dry_run`, `element_path`, `etag`, `leaf_map`, `list_source_name`, `page`, `remove_leaves`, `section_name`, `site_id`, `source_map`, `template_id` | One-call way to dynamize a designed section: copies the layout byte-identically and swaps only what you map. Use INSTEAD of hand-editing JSON. Give a REFERENCE (section_name from template_summary, or element_path) + list_source_name + a leaf_map (original leaf field-name flat flow field; #parent + tuned props kept). A section with several source blocks dynamizes in ONE call via source_map ({match, source_name, leaf_map?}[]). Sub-query panels: key by "<subquery>.<leaf>". Returns page + `summary`. Preview with dry_run:true. |
3861
+ | `yootheme_builder_page_get_layout` | read+idempotent | `cursor`, `fields`, `flat`, `force`, `max_chars`, `mode`, `post_id`, `site_id`, `template_id`, `url` | Get the layout tree for one template address it by `template_id` (from pages_list), `post_id`, or `url`. `mode:"bindings_only"` for dynamize/bind; `mode:"full"` (default) is nested `{layout, etag}`, `flat:true` a depth-first array. `max_chars` bounds the text; a clipped read returns `next_cursor` to page the rest. For a site-wide SEO/structure/content/links audit (h1, image alt, thin pages, dead links) do NOT loop this tool page-by-page call yootheme_builder_pages_audit (one sweep via yootheme_builder_advanced_read). |
3862
+ | `yootheme_builder_page_get_schema` | read+idempotent | `fields`, `include_meta`, `max_chars`, `post_id`, `site_id`, `template_id`, `url` | Get the flat schema for a template nodes with their JSON-Pointer paths and element types. Lighter than page_get_layout, enough to locate elements before editing. Address by `template_id` (pages_list), `post_id`, or `url`. `fields[]` slims each node; `max_chars` bounds output. |
3863
+ | `yootheme_builder_page_publish` | idempotent | `etag`, `quick_check`, `site_id`, `template_id` | Publish a template persist state, flush YT + WP caches, snapshot the published-state ETag. YOOtheme templates already publish on save, so this is a cache-flush + state-snapshot op. Neither publish nor save changes the CMS post status a draft page stays invisible to visitors; set status at creation via pages_create/library_insert. ETag optional when provided, 412 on conflict; when omitted, last-write-wins. |
3864
+ | `yootheme_builder_page_save` | idempotent | `dry_run`, `etag`, `layout`, `quick_check`, `site_id`, `template_id` | Re-run save-transforms + flush caches. Optionally REPLACE the template tree by passing the INNER {type:"layout",children:[...]} tree as `layout` page_get_layout's `.layout` child, NOT the whole response it returns (rejected 422, would blank the page). Validated + ETag-locked; nothing persists on reject. Does NOT change CMS post status a draft stays invisible to visitors; set status via pages_create. `dry_run:true` preflights; success carries `previous_layout` for rollback. ETag optional (412 on conflict; else last-write-wins). |
3865
+ | `yootheme_builder_pages_audit` | read+idempotent | `checks`, `include_meta`, `max_chars`, `mode`, `site_id`, `stale_after_months`, `target`, `thin_content_threshold`, `types` | One-call site-wide SEO/structure/content/links/design audit severity-sorted findings with fix_hints (a props fragment to merge: supply template_id+path from the locator). Covers h1/headings, thin pages, image alt, duplicate layouts, title collisions, non-pretty URLs, dead anchors. Only active (non-disabled) nodes produce findings. A site sweep caps hits/page, drops content_snippet; `target` scopes to ONE page for the full set (cap 100). Filter with `types`/`checks`. Disclaimer: SEO-plugin meta (title/description/OG) NOT covered. |
3866
+ | `yootheme_builder_pages_create` | idempotent | `dry_run`, `layout`, `menu_item`, `published`, `site_id`, `slug`, `status`, `title` | Create a page (WordPress) or article (Joomla 5/6) with a YOOtheme per-page layout. Required: title. Optional: slug, status (default draft), published (default false live on true: WP publish / Joomla state 1), layout (JSON), menu_item (Joomla SEF route), dry_run (preflight, no persist). Returns page_id (WP) / article_id (Joomla), slug, template_id (page:{id} or article:{id}), etag, warnings. Joomla: a published article has NO SEF route until a menu item points at it pass menu_item, else frontend_url may 404. |
3867
+ | `yootheme_builder_pages_delete` | destructive | `confirm`, `force`, `post_id`, `site_id` | Delete a page (WordPress) or article (Joomla 5/6) with its YOOtheme per-page layout. Requires confirm:true (gate against accidental deletion). Default: trash (recoverable). force:true: permanent delete. Refuses non-page/article types with 403 (Layer 9 defense). Returns deleted:true, page_id (WP) / article_id (Joomla), force_deleted, bumped etag. |
3868
+ | `yootheme_builder_pages_list` | read+idempotent | `cursor`, `fields`, `include_meta`, `label_contains`, `limit`, `max_chars`, `site_id` | List pages, templates, and layouts in the YOOtheme Pro builder (id, label, type, elements_count, frontend_url, builder_url, serves_context_kind, serves_context_ids, page_role per row). CALL THIS FIRST to discover template IDs before page_get_layout / element_list / page_get_schema. Paginates: up to `limit` rows (default 50) plus `total` and a `next_cursor` (pass it back as `cursor`) when more remain. Filter with `label_contains`. Slim rows with `fields:["id","label"]`; bound output with `max_chars`. |
3869
+ | `yootheme_builder_pages_resolve` | read+idempotent | `site_id`, `target`, `url` | Resolve a public page/article/archive URL to the template_id that renders it paste a URL, get the layout id. Pretty/SEF permalinks resolve on WordPress AND Joomla (incl. deep sub-category paths), not only raw ?option= URLs. A singular page its per-page layout; a category/tag/CPT/author archive the governing THEME TEMPLATE, and for a category/term the SPECIFICALLY-ASSIGNED template (honoring include-child-categories), not a generic one. Or a `target`. Returns {template_id, post_id, type, frontend_url, context, resolved_via}. |
3870
+ | `yootheme_builder_rename_source` | idempotent | `dry_run`, `etag`, `field_map`, `from`, `site_id`, `source_map`, `template_id`, `to` | Template-wide RENAME of a source / type / field name across EVERY binding in a template, in ONE ETag-locked write — preserving each binding's arguments, props and field_mappings. Use after a taxonomy/CPT rename (faq→help) breaks every bound layout. Pass `from`/`to` for one source, and/or `source_map`/`field_map` to rename a relation + its field tokens together; the dotted "<source>.<relation>" form is rewritten segment-by-segment. Returns {replaced_count, changed_paths, new_etag}. Preview with dry_run:true. ETag req. |
3871
+ | `yootheme_builder_render_preview` | read+idempotent | `expect`, `post_id`, `site_id`, `template_id`, `url` | Render-check a saved template — does it actually render, with how many items, or is it blank? Address by `template_id` (from pages_list), `post_id`, or `url` (resolved for you) — exactly one. `item_count` = items yielded by the template's dynamic/Multi-Items bindings (rendered repeat-source rows); 0 with empty:true means a binding returned nothing. Returns {rendered, item_count, empty, error?, cache_purge_hint}; with `expect`, also {matched, snippet}. Use AFTER a save/bind to catch a silently-empty page. Read-only. |
3872
+ | `yootheme_builder_scaffold_section` | mutating | `column_widths`, `columns`, `etag`, `site_id`, `template_id` | Create a VALID section → row → column hull in ONE call; returns the column JSON-Pointer path(s) for content. The correct way to start a layout block: only `section` may sit at the layout root; a bare section accepts `row`, a bare row accepts `column`; leaf/content elements are rejected anywhere except inside a `column`. Pass `columns` (1-12, default 1); columns get width tokens so they render side-by-side (hand-built columns default to full width), or `column_widths` (one token per column) for an asymmetric row. Requires ETag. |
3873
+ | `yootheme_builder_sites_list` | read+idempotent | `include_meta`, `site_id` | List all sites configured in this multi-site MCP installation. Returns site_id + URL + platform (wordpress\|joomla) + default flag per row. CALL THIS FIRST when working with a fresh MCP connection to discover available site_ids before targeting one with any other tool. Read-only, no REST calls. Keywords: list sites, list connections, list installations, discover site_id, available sites, configured sites, what sites exist, multi-site index. The `default_site_id` is the write target when a call omits site_id. (site_id is accepted for schema-uniformity but ignored by this tool.) |
2755
3874
  | `yootheme_builder_sites_test` | read+idempotent | `site_id` | Verify connectivity to ONE site: probes /health (no auth) + /etag (auth) in parallel; returns plugin_reachable + bearer_valid. `site_id` is REQUIRED. Use sites_list to find IDs. |
2756
- | `yootheme_builder_source_get_schema` | read+idempotent | `max_chars`, `name`, `site_id` | Introspect ONE data source: its query methods + arguments + RELATIONS (e.g. Term→Posts: categoryFaqs.taxonomyCategoryFaq → field "faq") + a provides_archive_context flag (id:0 = current archive term, auto-scoped). SOME arguments carry `enum_values` (the allowed values); when an arg has none it is a free string — pick from enum_values when present, never invent values. CALL BEFORE binding on an archive/category template. Keywords: source schema, relations, archive context, enum values. Get `name` from yootheme_builder_sources_list. Operates on the default site unless site_id is provided. |
2757
- | `yootheme_builder_sources_list` | read+idempotent | `cursor`, `fields`, `group`, `kind`, `limit`, `max_chars`, `name_contains`, `site_id` | List all data sources, feeds, and dynamic content sources available in the YOOtheme Pro builder. Returns name + label + origin (apimapper / wordpress / joomla / essentials) per source. CALL THIS BEFORE binding any element to a data source. Then bind directly with yootheme_builder_element_bind_source. Keywords: list sources, list feeds, list bindings, dynamic content, what sources exist. Paginates (`limit`/`cursor`/`total`); scope with `group`/`kind`/`name_contains`; slim with `fields[]`; bound output with `max_chars`. Operates on the default site unless site_id is provided. |
2758
- | `yootheme_builder_template_set_type` | idempotent | `dry_run`, `etag`, `site_id`, `template_id`, `type` | Reassign which archive/single a YOOtheme THEME template applies to — set its `type` condition (the value pages_list shows) in ONE ETag-locked write. Use after a taxonomy/CPT rename orphans a template (e.g. re-type "taxonomy-category_faq" → "taxonomy-category_help"). Returns {type, previous_type, new_etag}. Site-wide + immediate — preview with dry_run:true, reversible via previous_type. THEME templates only: page:/article: is refused (422). ETag req. Operates on the default site unless site_id is provided. |
2759
- | `yootheme_builder_template_summary` | read+idempotent | `site_id`, `template_id` | Token-efficient template overview: element counts by type, binding count, max nesting depth, and named landmark sections computed server-side in one call. Use this to grasp a large template before pulling element_list or page_get_layout. template_id only resolve a URL/post first via pages_resolve. Example: yootheme_builder_template_summary({ template_id: "default" }). Operates on the default site unless site_id is provided. |
3875
+ | `yootheme_builder_source_get_schema` | read+idempotent | `max_chars`, `name`, `site_id` | Introspect ONE data source: its query methods + arguments + RELATIONS (e.g. Term→Posts: categoryFaqs.taxonomyCategoryFaq → field "faq") + a provides_archive_context flag (id:0 = current archive term, auto-scoped). SOME arguments carry `enum_values` (the allowed values); when an arg has none it is a free string — pick from enum_values when present, never invent values. CALL BEFORE binding on an archive/category template. Keywords: source schema, relations, archive context, enum values. Get `name` from yootheme_builder_sources_list. |
3876
+ | `yootheme_builder_sources_list` | read+idempotent | `cursor`, `fields`, `group`, `include_meta`, `kind`, `limit`, `max_chars`, `name_contains`, `site_id` | List all data sources, feeds, and dynamic content sources available in the YOOtheme Pro builder. Returns name + label + origin (apimapper / wordpress / joomla / essentials) per source. CALL THIS BEFORE binding any element to a data source. Then bind directly with yootheme_builder_element_bind_source. Keywords: list sources, list feeds, list bindings, dynamic content, what sources exist. Paginates (`limit`/`cursor`/`total`); scope with `group`/`kind`/`name_contains`; slim with `fields[]`; bound output with `max_chars`. |
3877
+ | `yootheme_builder_template_set_type` | idempotent | `dry_run`, `etag`, `site_id`, `template_id`, `type` | Reassign which archive/single a YOOtheme THEME template applies to — set its `type` condition (the value pages_list shows) in ONE ETag-locked write. Use after a taxonomy/CPT rename orphans a template (e.g. re-type "taxonomy-category_faq" → "taxonomy-category_help"). Returns {type, previous_type, new_etag}. Site-wide + immediate — preview with dry_run:true, reversible via previous_type. THEME templates only: page:/article: is refused (422). ETag req. |
3878
+ | `yootheme_builder_template_summary` | read+idempotent | `cursor`, `include_nodes`, `limit`, `site_id`, `template_id`, `template_ids` | Token-efficient template overview: element counts by type, binding count, max nesting depth, and named landmark sections, computed server-side in one call. Use this to grasp a large template before pulling element_list or page_get_layout. Pass `include_nodes:true` for a `content_nodes` per-node breakdown (status/effective_visible/headings/alts), pageable via `limit`/`cursor`. Pass `template_ids:[...]` for a batch: response carries `summaries` + `not_found`. Example: yootheme_builder_template_summary({ template_id: "default" }). |
2760
3879
  | `yootheme_builder_use_site` | idempotent | `probe`, `site_id` | Switch the ACTIVE site for this multi-site session. After this call, every tool that omits `site_id` targets the site you picked here. NOTE: this default is PROCESS-WIDE — concurrent clients sharing this MCP server inherit it; pass `site_id` per call for isolation between parallel agents. Runs a /health + /etag probe by default; pass `probe: false` to switch offline. A degraded site is a soft warning (the switch still happens). `site_id` is REQUIRED. Keywords: switch/change/set active/target site, select installation, use profile. Use yootheme_builder_sites_list first to find IDs. |
2761
3880
 
2762
3881
  <!-- TOOL-CATALOG:END -->