@wootsup/yt-builder-mcp 1.8.0 → 1.10.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 (119) hide show
  1. package/README.md +24 -16
  2. package/dist/client.d.ts +6 -0
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +38 -2
  5. package/dist/client.js.map +1 -1
  6. package/dist/gateway/advanced-tool/domains.d.ts +14 -2
  7. package/dist/gateway/advanced-tool/domains.d.ts.map +1 -1
  8. package/dist/gateway/advanced-tool/domains.js +20 -5
  9. package/dist/gateway/advanced-tool/domains.js.map +1 -1
  10. package/dist/gateway/essentials.d.ts +1 -1
  11. package/dist/gateway/essentials.d.ts.map +1 -1
  12. package/dist/gateway/essentials.js +27 -0
  13. package/dist/gateway/essentials.js.map +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/platform/detect.d.ts +1 -1
  17. package/dist/platform/detect.js +1 -1
  18. package/dist/platform/index.d.ts +1 -1
  19. package/dist/platform/index.js +1 -1
  20. package/dist/proxy/mode.d.ts.map +1 -1
  21. package/dist/proxy/mode.js +25 -3
  22. package/dist/proxy/mode.js.map +1 -1
  23. package/dist/server.d.ts +14 -11
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +14 -11
  26. package/dist/server.js.map +1 -1
  27. package/dist/setup-cli.d.ts +8 -0
  28. package/dist/setup-cli.d.ts.map +1 -1
  29. package/dist/setup-cli.js +57 -5
  30. package/dist/setup-cli.js.map +1 -1
  31. package/dist/setup-npx-spec.d.ts +1 -1
  32. package/dist/setup-npx-spec.js +1 -1
  33. package/dist/setup-prompts.d.ts +11 -0
  34. package/dist/setup-prompts.d.ts.map +1 -1
  35. package/dist/setup-prompts.js +28 -0
  36. package/dist/setup-prompts.js.map +1 -1
  37. package/dist/setup-wizard-defaults.d.ts.map +1 -1
  38. package/dist/setup-wizard-defaults.js +40 -9
  39. package/dist/setup-wizard-defaults.js.map +1 -1
  40. package/dist/setup-wizard-handshake.d.ts +5 -4
  41. package/dist/setup-wizard-handshake.d.ts.map +1 -1
  42. package/dist/setup-wizard-handshake.js +28 -24
  43. package/dist/setup-wizard-handshake.js.map +1 -1
  44. package/dist/setup-wizard-types.d.ts +48 -9
  45. package/dist/setup-wizard-types.d.ts.map +1 -1
  46. package/dist/setup-wizard.d.ts.map +1 -1
  47. package/dist/setup-wizard.js +62 -10
  48. package/dist/setup-wizard.js.map +1 -1
  49. package/dist/sites/env-bridge.d.ts +19 -0
  50. package/dist/sites/env-bridge.d.ts.map +1 -1
  51. package/dist/sites/env-bridge.js +29 -15
  52. package/dist/sites/env-bridge.js.map +1 -1
  53. package/dist/sites/store.d.ts +1 -1
  54. package/dist/sites/store.js +1 -1
  55. package/dist/skill-loader.d.ts +2 -2
  56. package/dist/skill-loader.js +2 -2
  57. package/dist/tools/elements/builders.d.ts.map +1 -1
  58. package/dist/tools/elements/builders.js +21 -2
  59. package/dist/tools/elements/builders.js.map +1 -1
  60. package/dist/tools/elements/handlers-write.d.ts +2 -0
  61. package/dist/tools/elements/handlers-write.d.ts.map +1 -1
  62. package/dist/tools/elements/handlers-write.js +16 -4
  63. package/dist/tools/elements/handlers-write.js.map +1 -1
  64. package/dist/tools/elements/handlers.d.ts +1 -0
  65. package/dist/tools/elements/handlers.d.ts.map +1 -1
  66. package/dist/tools/elements/handlers.js +14 -0
  67. package/dist/tools/elements/handlers.js.map +1 -1
  68. package/dist/tools/format/pages-format.d.ts.map +1 -1
  69. package/dist/tools/format/pages-format.js +21 -0
  70. package/dist/tools/format/pages-format.js.map +1 -1
  71. package/dist/tools/index.d.ts.map +1 -1
  72. package/dist/tools/index.js +8 -0
  73. package/dist/tools/index.js.map +1 -1
  74. package/dist/tools/library.d.ts +279 -0
  75. package/dist/tools/library.d.ts.map +1 -0
  76. package/dist/tools/library.js +1060 -0
  77. package/dist/tools/library.js.map +1 -0
  78. package/dist/tools/local-content/builders.d.ts +19 -0
  79. package/dist/tools/local-content/builders.d.ts.map +1 -0
  80. package/dist/tools/local-content/builders.js +127 -0
  81. package/dist/tools/local-content/builders.js.map +1 -0
  82. package/dist/tools/local-content/handlers.d.ts +54 -0
  83. package/dist/tools/local-content/handlers.d.ts.map +1 -0
  84. package/dist/tools/local-content/handlers.js +287 -0
  85. package/dist/tools/local-content/handlers.js.map +1 -0
  86. package/dist/tools/local-content/index.d.ts +17 -0
  87. package/dist/tools/local-content/index.d.ts.map +1 -0
  88. package/dist/tools/local-content/index.js +16 -0
  89. package/dist/tools/local-content/index.js.map +1 -0
  90. package/dist/tools/local-content/schemas.d.ts +82 -0
  91. package/dist/tools/local-content/schemas.d.ts.map +1 -0
  92. package/dist/tools/local-content/schemas.js +89 -0
  93. package/dist/tools/local-content/schemas.js.map +1 -0
  94. package/dist/tools/pages/builders.d.ts.map +1 -1
  95. package/dist/tools/pages/builders.js +60 -11
  96. package/dist/tools/pages/builders.js.map +1 -1
  97. package/dist/tools/pages/handlers-audit.d.ts +31 -0
  98. package/dist/tools/pages/handlers-audit.d.ts.map +1 -0
  99. package/dist/tools/pages/handlers-audit.js +98 -0
  100. package/dist/tools/pages/handlers-audit.js.map +1 -0
  101. package/dist/tools/pages/handlers-write.d.ts +12 -2
  102. package/dist/tools/pages/handlers-write.d.ts.map +1 -1
  103. package/dist/tools/pages/handlers-write.js +13 -3
  104. package/dist/tools/pages/handlers-write.js.map +1 -1
  105. package/dist/tools/pages/index.d.ts +5 -1
  106. package/dist/tools/pages/index.d.ts.map +1 -1
  107. package/dist/tools/pages/index.js +5 -1
  108. package/dist/tools/pages/index.js.map +1 -1
  109. package/dist/tools/pages/schemas.d.ts +133 -0
  110. package/dist/tools/pages/schemas.d.ts.map +1 -1
  111. package/dist/tools/pages/schemas.js +110 -0
  112. package/dist/tools/pages/schemas.js.map +1 -1
  113. package/dist/tools/sources/handlers-bind.d.ts +2 -0
  114. package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
  115. package/dist/tools/sources/handlers-bind.js +18 -0
  116. package/dist/tools/sources/handlers-bind.js.map +1 -1
  117. package/manifest.json +2 -2
  118. package/package.json +3 -2
  119. package/skills/yt-builder-mcp/SKILL.md +1164 -75
@@ -12,14 +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 39 typed, scoped,
16
- idempotent tools (41 callable end-to-end with the two gateways). 36 of them
15
+ `@wootsup/yt-builder-mcp` server. The server catalogues 48 typed, scoped,
16
+ idempotent tools (50 callable end-to-end with the two gateways). 41 of them
17
17
  advertised as first-class entries in `tools/list`
18
- (32 essential L1 forwards + 2 direct L3 entries + 2 gateways), and 5 advanced
18
+ (37 essential L1 forwards + 2 direct L3 entries + 2 gateways), and 9 advanced
19
19
  tools reachable through the gateways. The write-capable
20
- `yootheme_builder_advanced` gateway routes all 5; the read-only
20
+ `yootheme_builder_advanced` gateway routes all 9; the read-only
21
21
  `yootheme_builder_advanced_read` gateway routes only the read-only subset
22
- (page_get_schema, inspect_sublayout, layout_query) and is itself
22
+ (page_get_schema, inspect_sublayout, layout_query, local_content_types,
23
+ local_content_read, pages_audit) and is itself
23
24
  read-only-annotated, so MCP hosts can auto-allow pure inspection without a
24
25
  per-call write-approval prompt. This 3-lane split keeps `tools/list` well below
25
26
  the ~40-tool Cursor cap while every catalogued tool stays fully reachable.
@@ -49,12 +50,12 @@ Joomla 5/6 host plugin:
49
50
 
50
51
  ### Two picker entries (activate both)
51
52
 
52
- Some clients (notably Claude Desktop with the `.dxt` bundle) expose **two**
53
+ Some clients (notably Claude Desktop with the `.mcpb` bundle) expose **two**
53
54
  entries when the user types "YT Builder MCP" into the picker:
54
55
 
55
56
  - **`YT Builder MCP for YOOtheme Pro (unofficial)`**: the MCP **server**.
56
- Provides the 36 first-class tools (32 essential, 2 direct, 2 gateways).
57
- The `yootheme_builder_advanced` gateway routes all 5 advanced tools; the
57
+ Provides the 41 first-class tools (37 essential, 2 direct, 2 gateways).
58
+ The `yootheme_builder_advanced` gateway routes all 9 advanced tools; the
58
59
  read-only `yootheme_builder_advanced_read` gateway routes the read-only
59
60
  subset without a write-approval prompt.
60
61
  - **`Von YT Builder MCP for YOOtheme Pro`**: the bundled **skill** (this
@@ -81,11 +82,13 @@ The server exposes:
81
82
  - **2 direct top-level tools**, always callable, always in `tools/list`:
82
83
  `yootheme_builder_health` and `yootheme_builder_diagnose`. These are
83
84
  the "the gateway itself might be broken" escape hatch.
84
- - **32 essential forwarded tools**: common reads + the most-used writes
85
+ - **37 essential forwarded tools**: common reads + the most-used writes
85
86
  (pages_list, pages_resolve, pages_create, pages_delete, page_dynamize, scaffold_section,
86
87
  get_etag, element_list / add / update_settings / get / move / clone / delete,
87
88
  page_get_layout, sources_list, source_get_schema, element_types_list, element_type_get_schema,
88
- template_summary, inspect_multi_items_binding, sites_list, sites_test,
89
+ template_summary, inspect_multi_items_binding, library_browse, library_get, library_insert,
90
+ image_search, image_import,
91
+ sites_list, sites_test,
89
92
  use_site, the bind→verify→save→publish chain element_bind_source /
90
93
  element_get_binding / page_save / page_publish, and the archive-rebind /
91
94
  refactor surface render_preview / element_set_source / rename_source /
@@ -104,10 +107,29 @@ The server exposes:
104
107
  creates a valid
105
108
  section → row → column hull in one call — the easiest correct way to
106
109
  start a layout block (content placed anywhere else is rejected by the
107
- structural-hierarchy guard).
108
- - **5 advanced captured tools**: everything else (page_get_schema,
110
+ structural-hierarchy guard). `library_browse` (Task 2, Layout Library) is L1
111
+ so browsing the YOOtheme Pro Library catalog — pre-designed page layouts and
112
+ element presets, filterable by topic/type/role/website/element_type/query —
113
+ is a direct call before hand-building a section from scratch. `library_get`
114
+ (Task 3, Layout Library) is L1 so fetching a SINGLE browsed item's full tree
115
+ + derived section list (`{id, name, path, element_count}` per top-level
116
+ section) — the natural next call once `library_browse` returns an `id` —
117
+ is a direct call too. `library_insert` (Task 6, Layout Library) is L1 so
118
+ composing + persisting a browsed/fetched item (layout, section, or preset)
119
+ onto a page or existing template — target `new_page` / `{template_id,
120
+ element_path}` / `sublayout` — through the SAME hardened write chain as
121
+ every element/page write, is a direct call as well. `image_search` and
122
+ `image_import` (Task D1, Image Sourcing) are L1 so searching Unsplash /
123
+ Pexels / YOOtheme Pro Images for on-topic photography, then sideloading
124
+ the chosen photo into the site's Media Library, are direct calls — the
125
+ step that turns a placeholder-filled skeleton into a finished-looking
126
+ page (see "Fill image slots with real on-topic photos" under Layout
127
+ Library).
128
+ - **9 advanced captured tools**: everything else (page_get_schema,
109
129
  element_unbind_source, clean_implode_directives,
110
- `inspect_sublayout`, and `layout_query`).
130
+ `inspect_sublayout`, `layout_query`, `library_save_as`, the two Local
131
+ Content Read tools `local_content_types` / `local_content_read`, and
132
+ `pages_audit`).
111
133
  Reachable through one gateway tool:
112
134
  `yootheme_builder_advanced({ tool: "<name>", arguments: { ... } })`. For
113
135
  example, to inspect a panel/grid_item `< SUBLAYOUT`:
@@ -116,18 +138,35 @@ The server exposes:
116
138
  AND-combined `type`/`name_matches`/`has_binding`/`max_depth`/`under_section`):
117
139
  `yootheme_builder_advanced({ tool: "yootheme_builder_layout_query", arguments: { template_id, type: "headline", has_binding: false, under_section: "Hero" } })`
118
140
  (it's read-only, so the read gateway `yootheme_builder_advanced_read({ tool: "yootheme_builder_layout_query", … })` works too).
141
+ And to save a page/subtree INTO "My Layouts" (Task 7 — the reverse of
142
+ `library_insert`; omit `element_path` to save the whole page):
143
+ `yootheme_builder_advanced({ tool: "yootheme_builder_library_save_as", arguments: { source: { template_id, element_path }, name: "Hero Section" } })`.
144
+ It flags `invalid_source_possible: true` when the saved subtree carries
145
+ bound dynamic sources that may not exist wherever it is later reloaded —
146
+ re-bind after inserting it elsewhere.
147
+ And to run a one-call site-wide SEO/structure/content/links audit
148
+ (read-only, severity-sorted findings + machine-executable fix_hints),
149
+ prefer the read gateway:
150
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_pages_audit", arguments: { checks: ["seo", "structure"] } })`
151
+ (the write gateway routes it too, if you prefer one entry point:
152
+ `yootheme_builder_advanced({ tool: "yootheme_builder_pages_audit" })`).
119
153
 
120
154
  The core write/refactor + render-verify tools the Glöckle HANDOFF-4/5 ask for
121
155
  are now **L1 — call them directly by name**, not through a gateway:
122
156
  - To render-check a saved page over the authenticated channel (F5+N5 — does
123
157
  it render, with how many items, or is it silently blank? where an external
124
158
  cURL 401s behind staging Basic-Auth), call the now-L1
125
- `yootheme_builder_render_preview({ template_id })` directly. It takes no
126
- post/term target: it renders YOOtheme's implicit fallback context, so a
127
- source-bound template comes back non-empty. Treat a non-empty render as the
128
- success signal, not "blank despite a binding". Pass `expect: "<substring>"`
129
- to confirm a specific edit reached the HTML in one call (the response adds
130
- `matched` + `snippet`).
159
+ `yootheme_builder_render_preview({ template_id })` directly or address it by
160
+ a public URL with `yootheme_builder_render_preview({ url })` (the URL is
161
+ resolved to its template for you, exactly like `page_get_layout`), so if you
162
+ only hold a resolved category/archive URL you can render-check it in ONE call
163
+ without first converting it to a template_id. It takes no post/term target: it
164
+ renders YOOtheme's implicit fallback context, so a source-bound template comes
165
+ back non-empty. Treat a non-empty render as the success signal, not "blank
166
+ despite a binding". `item_count` counts the TOP-LEVEL repeat-source rows only
167
+ (a static grid/list nested in a repeated card no longer inflates it). Pass
168
+ `expect: "<substring>"` to confirm a specific edit reached the HTML in one call
169
+ (the response adds `matched` + `snippet`).
131
170
  - To surgically SET a precomposed `source` + the native item-level
132
171
  `source_extended.props` carrier on one element at a pointer, ETag-locked,
133
172
  leaving the rest of the layout byte-unchanged (F2 — the one-call source edit
@@ -152,13 +191,21 @@ The server exposes:
152
191
  - **2 gateway tools**: `yootheme_builder_advanced` (write-capable; can route
153
192
  into delete/unbind, so hosts gate it) and `yootheme_builder_advanced_read`
154
193
  (read-only; routes ONLY the read-only advanced subset — page_get_schema,
155
- inspect_sublayout, layout_query and is read-only-annotated
156
- so hosts auto-allow it). Prefer the read gateway for pure inspection:
194
+ inspect_sublayout, layout_query, local_content_types, local_content_read,
195
+ pages_audit
196
+ and is read-only-annotated so hosts auto-allow it). Prefer the read gateway
197
+ for pure inspection:
157
198
  `yootheme_builder_advanced_read({ tool: "yootheme_builder_page_get_schema", arguments: { template_id } })`.
158
-
159
- `tools/list` therefore advertises 36 names (32 + 2 + 2). That's 32 L1
160
- essentials + 2 L3 direct + 2 gateways. The total callable surface is 41
161
- (36 advertised + 5 advanced reachable through the gateways). If the AI
199
+ Discover the site's NATIVE content the same way — the landscape then the records:
200
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_local_content_types" })`
201
+ then `yootheme_builder_advanced_read({ tool: "yootheme_builder_local_content_read", arguments: { kind: "terms", taxonomy: "category" } })`.
202
+ (The write gateway routes them too, if you prefer one entry:
203
+ `yootheme_builder_advanced({ tool: "yootheme_builder_local_content_types" })` and
204
+ `yootheme_builder_advanced({ tool: "yootheme_builder_local_content_read", arguments: { kind: "entities", type: "post" } })`.)
205
+
206
+ `tools/list` therefore advertises 41 names (37 + 2 + 2). That's 37 L1
207
+ essentials + 2 L3 direct + 2 gateways. The total callable surface is 50
208
+ (41 advertised + 9 advanced reachable through the gateways). If the AI
162
209
  client reports "tool not found", you are almost certainly calling an
163
210
  advanced tool by its raw name. Wrap it in
164
211
  `yootheme_builder_advanced({ tool, arguments })` (or the read gateway for
@@ -183,6 +230,50 @@ guess where the agent is pointing:
183
230
  Treat `frontend_url: null` as "host plugin could not resolve a public URL for
184
231
  this template". Surface that honestly rather than fabricating one.
185
232
 
233
+ ## Resolve a URL back to the template that renders it
234
+
235
+ The two sections above go template → URL (forward). To go the OTHER way — the
236
+ user pastes a public URL and asks *"which layout renders this?"* — you have two
237
+ complementary tools.
238
+
239
+ **`yootheme_builder_pages_resolve({ url })` — the direct reverse lookup.** Paste
240
+ any public frontend URL and it returns
241
+ `{ template_id, post_id, type, frontend_url, context, resolved_via }` (or a
242
+ `url_not_resolvable` when it genuinely maps to no template — it degrades to an
243
+ honest null rather than guessing a wrong one). It handles **SEF / pretty
244
+ permalinks on BOTH WordPress and Joomla** (not just raw `?option=…&id=…` query
245
+ URLs), including deep sub-category paths like `/matches/first-men-s-team`. For a
246
+ **category or term URL it returns the SPECIFICALLY-ASSIGNED template** — the one
247
+ whose YOOtheme `query.catid` (Joomla) / `query.terms` (WordPress) binds THAT
248
+ category/term, honoring **include-child-categories** (a template assigned to a
249
+ parent category with child-inclusion covers its subcategories; the nearest
250
+ specific assignment wins). Only when nothing is specifically assigned does it
251
+ fall back to the governing generic theme template. Then feed the returned
252
+ `template_id` straight into `page_get_layout` / `element_list` / the write tools.
253
+
254
+ **`serves_context_kind` + `serves_context_ids` on `pages_list` rows — resolve it
255
+ yourself, zero extra calls.** Every `pages_list` row also carries these two Layer-C
256
+ fields so you can map a URL to its template WITHOUT a `pages_resolve` round-trip:
257
+
258
+ - **`serves_context_kind`** is the normalized, cross-platform context the template
259
+ serves — one of `single-post`, `single-page`, `category`, `tag`, `author`,
260
+ `search`, `post-archive`, `error-404`, `internal`, `unknown`. A WordPress row and
261
+ a Joomla row that serve the same logical context report the SAME kind (WP
262
+ `taxonomy-category` and Joomla `com_content.category` both → `category`).
263
+ - **`serves_context_ids`** is the SPECIFIC category/term ids that a category
264
+ template is assigned to (Joomla `query.catid`, WordPress `query.terms`). It is
265
+ **empty** for a generic archive or a generic category template that binds no
266
+ specific ids — that emptiness is honest, not a bug.
267
+
268
+ Matching workflow: derive the URL's context (a `/category/<id-or-slug>` →
269
+ `category` + its id; an article/post → `single-post`; etc.), then pick the
270
+ `pages_list` row whose `serves_context_kind` matches and whose
271
+ `serves_context_ids` contains that id — e.g. URL `/matches` (category 11) matches
272
+ the row `{ label: "Match Index", serves_context_kind: "category",
273
+ serves_context_ids: [11] }`. Use `pages_resolve` when you just want the answer;
274
+ use the `serves_context_*` columns when you already hold `pages_list` and want to
275
+ match locally.
276
+
186
277
  ## Scopes (Bearer key permissions)
187
278
 
188
279
  Every Bearer key has a scope, set at key creation time:
@@ -226,7 +317,7 @@ sites (WordPress and Joomla mixed). Without multi-site support you would need
226
317
  one MCP install per site, one set of env vars per site, and one AI-client
227
318
  restart per site you want to talk to. With multi-site:
228
319
 
229
- - One DXT install in Claude Desktop, one entry in your AI client config.
320
+ - One MCPB install in Claude Desktop, one entry in your AI client config.
230
321
  - One conversation can edit elements on `acme.com` and `beta.io` back-to-back.
231
322
  - Each site keeps its own Bearer key, platform, label, and 1Password reference.
232
323
  - Adding a new client site does not require a new MCP install.
@@ -351,7 +442,7 @@ one conversation.
351
442
  2. For each site, the agent runs the same sequence with `site_id` set:
352
443
  - `yootheme_builder_pages_list({ site_id: "wp-acme", fields: ["id", "label"] })`
353
444
  - `yootheme_builder_get_etag({ site_id: "wp-acme" })`
354
- - `yootheme_builder_element_update_settings({ site_id: "wp-acme", template_id: "home", element_path: "/0/children/0/children/0/children/0", props: { content: "New headline" }, merge: true, etag: "<etag>" })`
445
+ - `yootheme_builder_element_update_settings({ site_id: "wp-acme", template_id: "home", element_path: "/children/0/children/0/children/0/children/0", props: { content: "New headline" }, merge: true, etag: "<etag>" })`
355
446
  - `yootheme_builder_page_save({ site_id: "wp-acme", template_id: "home", etag: "<fresh>" })`
356
447
  - `yootheme_builder_page_publish({ site_id: "wp-acme", template_id: "home", etag: "<fresh>" })`
357
448
  3. The customer sees a stream of replies, each prefixed with the matching
@@ -364,6 +455,33 @@ for a focused diagnosis without touching the others.
364
455
 
365
456
  ---
366
457
 
458
+ ## Pages vs theme templates (know what you are editing)
459
+
460
+ `pages_list` returns TWO different kinds of editable layout, and every tool
461
+ takes a `template_id` that addresses either. Know which one you have before
462
+ you write — they have very different blast radius.
463
+
464
+ | Kind | `id` shape | `type` value(s) | What it is |
465
+ |------|-----------|-----------------|------------|
466
+ | **Page** | `page:<n>` (WP) / `article:<n>` (Joomla) | `page` | ONE individual page/article. Editing it changes only that page. Stored in the post/article content. |
467
+ | **Theme template** | a bare id (e.g. `I99YS8Ii`) | `single-post`, `archive-post`, `taxonomy-category`, `taxonomy-post_tag`, `author-archive`, `search`, `error-404`, … | A REUSABLE theme-level layout that applies to EVERY matching item. Stored in the theme options. |
468
+
469
+ **The safety rule: editing a theme template changes the layout for ALL
470
+ matching content, not one page.** If the user says "restyle the blog post
471
+ template", `pages_list` shows it as e.g. `{ id: "I99YS8Ii", label: "Post",
472
+ type: "single-post" }` — editing that `template_id` restyles EVERY blog post
473
+ on the site at once. That is often exactly what's wanted (that's what theme
474
+ templates are for), but confirm the intent — do not edit a `single-post` /
475
+ `taxonomy-*` / `archive-*` template when the user meant one specific page.
476
+ `template_summary` also returns `is_theme_template` to disambiguate cheaply.
477
+
478
+ - To edit ONE page, target a `page:<n>` / `article:<n>` id.
479
+ - To edit a reusable template (the design of all posts, all category
480
+ archives, the 404, …), target the bare id and understand the reach.
481
+ - `pages_create` creates a **page/article** (an individual layout). Theme
482
+ templates are defined in the theme itself and are not created via this MCP;
483
+ you edit the existing ones `pages_list` reports.
484
+
367
485
  ## Workflow 1: Build a hero section
368
486
 
369
487
  **Goal:** Add a fresh hero section (heading + sub-heading + CTA button)
@@ -379,24 +497,48 @@ to an existing page.
379
497
  named a specific page, match on `label` (exact then fuzzy).
380
498
  3. `yootheme_builder_get_etag()`: fetch the current top-level
381
499
  optimistic-lock ETag. Every write tool requires it via `etag`.
382
- 4. `yootheme_builder_element_add({ template_id: "<id>", parent_path: "", element_type: "section", props: { background: "primary" }, etag: "<etag>" })`:
383
- append a new section at the template root (`parent_path: ""`).
384
- Returns `{ path: "/0/children/N", etag: "<fresh>" }`.
500
+ 4. `yootheme_builder_element_add({ template_id: "<id>", parent_path: "", element_type: "section", props: { style: "primary" }, etag: "<etag>" })`:
501
+ append a new section at the template root (`parent_path: ""`). The
502
+ section background is set with `style` (enum: `default` / `muted` /
503
+ `primary` / `secondary`) — there is NO `background` key (it hard-rejects
504
+ as an unknown prop). Always call `element_type_get_schema("section")`
505
+ first to confirm the real prop keys.
506
+ Returns `{ rel_path: "/children/0/children/N", etag: "<fresh>" }`.
385
507
  5. `yootheme_builder_element_add({ template_id, parent_path: "<section-path>", element_type: "row", etag: "<fresh-etag>" })`:
386
508
  add a row inside the section. Use the etag returned by the
387
509
  previous write (etags rotate every mutation).
388
- 6. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "headline", props: { content: "<h1 text>" }, etag })`:
389
- add a headline.
510
+ 6. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "headline", props: { content: "<h1 text>", title_style: "h1" }, etag })`:
511
+ add a headline. `title_style: "h1"` uses the semantic type scale (the
512
+ tasteful default). See "Typography & heading styles" below before
513
+ reaching for a `heading-*` display size.
390
514
  7. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "text", props: { content: "<sub text>" }, etag })`:
391
515
  add a text element.
392
- 8. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "button", props: { content: "<cta>", link: "<url>" }, etag })`:
393
- add the CTA button.
516
+ 8. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "button", etag })`
517
+ then add a `button_item` CHILD for the actual label/link/style:
518
+ `yootheme_builder_element_add({ template_id, parent_path: "<button-path>", element_type: "button_item", props: { content: "<cta>", link: "<url>", button_style: "primary" }, etag })`.
519
+ The `button` element is a CONTAINER — its label, link and style live on
520
+ a `button_item` child, NOT on the button's own props. Adding a lone
521
+ `button` with `{content, link}` renders an empty button. (Same
522
+ container→item pattern as `grid`→`grid_item`, `list`→`list_item`.)
394
523
  9. `yootheme_builder_page_save({ template_id, etag })`:
395
524
  persist the working copy (visible in YOOtheme Customizer preview).
396
525
  `page_save` is a first-class L1 tool; call it directly.
397
526
  10. `yootheme_builder_page_publish({ template_id, etag })`:
398
527
  make the changes live on the front-end. Also a first-class L1 tool.
399
528
 
529
+ > **Building a whole page? Prefer ONE atomic `page_save` over ~30 incremental
530
+ > writes.** The step-by-step `element_add` sequence above is clearest for
531
+ > learning and for small edits, but each write is ETag-locked, so building a
532
+ > full multi-section page as 20-30 sequential writes invites `412` conflicts
533
+ > (especially when other agents edit the same site — the builder ETag is a
534
+ > single shared option). For a full page, assemble the complete
535
+ > `{type:"layout",children:[...]}` tree in memory and persist it in ONE call:
536
+ > `yootheme_builder_page_save({ template_id, layout: <full-tree> })` (ETag
537
+ > optional → last-write-wins, scoped to this page). `dry_run:true` preflights
538
+ > it first. A fast, robust pattern: `scaffold_section` one block, read it
539
+ > back to learn the exact node shape, then build the rest of the tree from
540
+ > that shape and `page_save` it whole.
541
+
400
542
  **Common pitfalls:**
401
543
 
402
544
  - **Wrong parameter names.** Every tool uses snake_case. Use
@@ -430,10 +572,10 @@ yootheme_builder_element_add({
430
572
  template_id: "home",
431
573
  parent_path: "",
432
574
  element_type: "section",
433
- props: { background: "primary" },
575
+ props: { style: "primary" }, // section background = `style`, NOT `background`
434
576
  etag: "abc123" // from yootheme_builder_get_etag
435
577
  })
436
- // Response: { path: "/0/children/3", etag: "def456" }
578
+ // Response: { rel_path: "/children/0/children/3", etag: "def456" }
437
579
  // → next call uses etag "def456"
438
580
 
439
581
  // Step 9. page_save is a first-class L1 tool; call it directly.
@@ -453,6 +595,206 @@ section as the last child of the template root.
453
595
 
454
596
  ---
455
597
 
598
+ ## Multi-column layouts (columns that sit side-by-side)
599
+
600
+ **The single most common layout mistake: a `row` with several `column`
601
+ children that reads as gaps / a broken column in the builder — or renders
602
+ STACKED (full-width, one under the other) on the front-end — instead of a
603
+ clean set of side-by-side columns.** This is a silent, layout-only defect:
604
+ the write succeeds (valid values), so you only catch it by looking at the
605
+ builder's left panel or the rendered page, never from a successful save.
606
+
607
+ **How a real YOOtheme layout defines columns — set BOTH, exactly like every
608
+ shipped demo layout does:**
609
+
610
+ 1. **`row.props.layout` — the grid string. This is the primary, load-bearing
611
+ piece.** It is the value the builder's *"Select a grid layout"* picker
612
+ writes: a comma-separated width per column, e.g. `"1-2,1-2"` (Halves),
613
+ `"1-3,1-3,1-3"` (Thirds), `"2-3,1-3"` (Thirds 2-1), `"1-4,1-4,1-4,1-4"`
614
+ (Quarters), `"3-5,2-5"` (a 3/5 + 2/5 hero split). The builder reads
615
+ `row.layout` to know a row HAS N columns. **A multi-column row with no
616
+ `row.layout` is read by the builder's Layout picker as "Whole" (one
617
+ full-width column) and painted collapsed with a big empty gap where the
618
+ other columns belong** — the "lauter Lücken im Layout" a customer sees in
619
+ the builder. `layout` is a `row` prop (NOT a column prop).
620
+
621
+ 2. **Per-column responsive `width_*` — the breakpoint refinement.** On each
622
+ `column`, set `width_default: "1-1"` (stack on phone) and
623
+ `width_medium: "<token>"` (go side-by-side from ≥960px). This is the
624
+ factory responsive default: readable single-column on a phone, multi-column
625
+ on tablet/desktop. The full breakpoint set (each maps to
626
+ `uk-width-{value}@breakpoint`):
627
+
628
+ | Prop | Breakpoint |
629
+ |------|------------|
630
+ | `width_default` | all sizes (phone-up) |
631
+ | `width_small` | ≥ 640px |
632
+ | `width_medium` | ≥ 960px |
633
+ | `width_large` | ≥ 1200px |
634
+ | `width_xlarge` | ≥ 1600px |
635
+
636
+ **Valid width tokens** (the same values used in the grid string; discover
637
+ them on the `column` schema via `element_type_get_schema` — they carry an
638
+ `enum`, so an invalid token is hard-rejected with a `422 invalid_enum_value`):
639
+ `1-1 1-2 1-3 1-4 1-5 1-6 2-3 2-5 3-4 3-5 4-5 5-6` and
640
+ `auto expand small medium large xlarge`. Match the row's grid string to the
641
+ columns' `width_medium` tokens (grid `"2-3,1-3"` ↔ column 1 `width_medium:
642
+ "2-3"`, column 2 `width_medium: "1-3"`).
643
+
644
+ **Single-column rows are different: NO `row.layout`, NO column width.** A
645
+ factory intro / testimonial / CTA is a full-width column (leave the column
646
+ width-less) inside a row with no grid string, and the content is centred by
647
+ setting `text_align: "center"` on the child `headline` / `text` elements —
648
+ never by narrowing the column. Narrowing a single column WITHOUT a matching
649
+ `row.layout` is exactly what leaves a gap in the builder.
650
+
651
+ **Easiest correct way — `scaffold_section` builds the whole factory-shaped
652
+ hull for you (row grid string + per-column responsive widths in one call):**
653
+
654
+ - `scaffold_section({ template_id, columns: 3, etag })` → a section whose row
655
+ carries `layout: "1-3,1-3,1-3"` and whose 3 columns each carry
656
+ `width_default: "1-1"` + `width_medium: "1-3"` → clean, gap-free, side-by-side
657
+ from ≥960px. `columns: N` distributes equal widths (`1-N` for N ≤ 6,
658
+ `expand` for N > 6).
659
+ - For an ASYMMETRIC row pass explicit tokens, one per column:
660
+ `scaffold_section({ template_id, columns: 2, column_widths: ["2-3", "1-3"], etag })`
661
+ → a row with `layout: "2-3,1-3"` and the matching column widths. A
662
+ wrong-length array or a bad token returns a friendly `400` before the write.
663
+ - `scaffold_section({ template_id, columns: 1, etag })` → a single full-width
664
+ column with no `row.layout` and no column width (the factory single-column
665
+ shape).
666
+
667
+ `scaffold_section` returns the column JSON-Pointer paths — drop your content
668
+ into them with `element_add({ parent_path: "<column-path>", ... })`. If you
669
+ build columns by hand with `element_add` instead, remember to set BOTH
670
+ `row.props.layout` (the grid string) AND each column's `width_default` /
671
+ `width_medium`, or the row reads as "Whole" and shows gaps in the builder.
672
+
673
+ **Success criterion:** in the builder's left panel the row shows N distinct
674
+ columns (not one column + an empty gap), and after publish the columns sit
675
+ side-by-side at desktop width. Measure with `getBoundingClientRect` (columns
676
+ share the same `top`, differ in `left`) — a fullPage screenshot hides both
677
+ the gap and the stacking.
678
+
679
+ ---
680
+
681
+ ## Typography & heading styles (restraint is the professional signal)
682
+
683
+ The `title_style` prop (on `headline`, and `title_style`/`content_style`/
684
+ `meta_style` on `grid`) mixes TWO different scales that look alike but mean
685
+ opposite things. Choosing the wrong one is the most common reason an
686
+ AI-built page looks amateurish — a wall of oversized text with no hierarchy.
687
+
688
+ - **`h1`–`h6` = the SEMANTIC type scale.** They follow the theme's designed
689
+ type ramp — tasteful, theme-consistent, restrained. **This is the default
690
+ a real YOOtheme designer uses.** A hero headline is typically `h1`; a
691
+ section title is `h2`; a card/feature title is `h3`. Reach for these
692
+ FIRST.
693
+ - **`heading-small` … `heading-3xlarge` = DISPLAY / billboard type**
694
+ (≈2.6rem up to ~8rem; on the front-end `heading-2xlarge` can render
695
+ ~150–180px). They are decoupled from semantics — pure visual impact.
696
+ `heading-2xlarge` / `heading-3xlarge` are for a SINGLE deliberate hero
697
+ statement, used ONCE per page, if at all.
698
+ - **`text-lead` / `text-large` / `text-meta` / `text-small`** are body-text
699
+ emphasis styles, not headings (e.g. an eyebrow label = `text-meta`, a lead
700
+ paragraph = `text-lead`).
701
+
702
+ **The principle: hierarchy comes from CONTRAST, not from making everything
703
+ large.** If every section title is `heading-2xlarge`, nothing stands out and
704
+ the page reads as a shouting match. Display sizes are a spice, not a staple:
705
+ give ONE hero moment the big display size and let everything else step down
706
+ to the semantic scale (`h2`/`h3`). Concretely, for a typical landing page:
707
+
708
+ | Element | Good default |
709
+ |---------|-------------|
710
+ | Eyebrow / overline label | `text-meta` |
711
+ | Hero headline | `h1` (or at most `heading-medium`/`heading-large` for extra punch) |
712
+ | Section title | `h2` |
713
+ | Card / feature / metric title | `h3` (a big metric number can take `heading-medium`, not `2xlarge`) |
714
+ | Lead paragraph | `text-lead` |
715
+
716
+ Do NOT repeat a `heading-*` display size across multiple sections. When in
717
+ doubt, use the semantic level (`h2`/`h3`) — it is always the safer,
718
+ more professional choice.
719
+
720
+ ### `title_style` (visual size) vs `title_element` (HTML tag / SEO)
721
+
722
+ These are **two independent axes** on a `headline`, and confusing them
723
+ produces SEO-fatal pages:
724
+
725
+ - **`title_style`** = how big the title *looks* (the section above).
726
+ - **`title_element`** = the actual HTML tag it renders as: `h1`–`h6`, or
727
+ `div` for non-heading display text. This is the **semantic / SEO** axis. A
728
+ `div` styled `heading-2xlarge` looks huge but is *not* a heading; an `h2`
729
+ styled `text-small` is still a level-2 heading.
730
+
731
+ **`title_element` DEFAULTS to `h1` when unset** — so if you leave it blank on
732
+ every headline, every headline becomes an `<h1>`, which is SEO-fatal (a page
733
+ must have **exactly one** `<h1>`). Set it explicitly on every headline:
734
+
735
+ | Headline role | `title_element` |
736
+ |---------------|-----------------|
737
+ | Hero headline (once per page) | `h1` |
738
+ | Section title | `h2` |
739
+ | Card / feature title | `h3` |
740
+ | Metric number / stat / other non-heading display text | `div` |
741
+
742
+ `title_style` and `title_element` are set **independently** — e.g. a hero is
743
+ `title_element: "h1"` with `title_style: "h1"` (or a `heading-*` display size
744
+ for punch); a big stat number is `title_element: "div"` with
745
+ `title_style: "heading-medium"`.
746
+
747
+ **This `h1` default is specific to `headline`.** Container elements
748
+ (`grid`, `panel`, `slideshow`, and the rest of the item-carrier family)
749
+ default to `h3`, not `h1`, when `title_element` is left unset. See
750
+ "Workflow 6 (SEO audit)" below for the full per-type default table
751
+ `pages_audit` uses to find missing/multiple h1 issues across a whole site.
752
+
753
+ ### Headline `content` is plain inline text — never wrapped in `<p>`
754
+
755
+ A `headline` renders its `content` **inside** the heading tag, so wrapping it
756
+ in a `<p>` produces invalid `<hN><p>…</p>`. The server **hard-rejects** a
757
+ headline whose `content` is a sole `<p>…</p>` block (422
758
+ `headline_content_block_p`). Store **bare inline text**, exactly like a
759
+ factory headline:
760
+
761
+ - ✅ `content: "Ready to get started?"`
762
+ - ✅ inline emphasis is fine: `content: "Save <strong>40%</strong> today"`
763
+ - ❌ `content: "<p>98%</p>"` — a `<p>` block belongs to the **`text`**
764
+ element, not a headline.
765
+
766
+ For paragraph / body copy (which *should* be a `<p>`), use the `text`
767
+ element, not a headline.
768
+
769
+ ---
770
+
771
+ ## Troubleshooting: builder panel is empty / "The builder is not available on this page"
772
+
773
+ If the YOOtheme Customizer's left Builder panel is empty or shows *"The
774
+ builder is not available on this page. It can only be used on pages, posts
775
+ and categories"* on a page you KNOW has a layout, the usual cause is a
776
+ **stale Customizer session**, NOT a broken layout. YOOtheme's Customizer
777
+ preview cookie is derived from the WordPress login-session token; when that
778
+ token rotates (re-login, session timeout, a different tab), the preview
779
+ renders as a plain page without the builder-data injection and the panel
780
+ reports "not available". **Fix: reload the Customizer tab** (or re-open the
781
+ builder from Pages → the page → Builder). The stored layout is intact —
782
+ confirm with `page_get_layout({ template_id })`, which reads it directly
783
+ and is unaffected by the browser session.
784
+
785
+ ---
786
+
787
+ ## Layout versioning (handled automatically — informational)
788
+
789
+ Every layout this server writes is stamped with the installed YOOtheme
790
+ version at the layout root, exactly like the builder UI does. You do not
791
+ need to set this. It prevents YOOtheme from treating a written layout as
792
+ pre-versioning legacy content and running its migration chain (which would
793
+ re-map current-schema values and bloat the tree) on every load. An existing
794
+ version on a layout you read-modify-write is preserved, never overwritten.
795
+
796
+ ---
797
+
456
798
  ## Workflow 2: Bind a dynamic source to a grid
457
799
 
458
800
  **Goal:** Wire an existing Grid (or other multi-item element) to a
@@ -465,7 +807,7 @@ renders dynamic items.
465
807
  2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` and
466
808
  `yootheme_builder_page_get_layout({ template_id: "<id>", flat: false })`:
467
809
  locate the target Grid. Note its JSON-Pointer `path` (e.g.
468
- `/0/children/2/children/0`).
810
+ `/children/0/children/2/children/0`).
469
811
  3. `yootheme_builder_element_get({ template_id, element_path })`:
470
812
  fetch the Grid's current props so you can preserve them. Binding
471
813
  sets `props.source` and leaves the rest alone.
@@ -528,15 +870,15 @@ renders dynamic items.
528
870
  // is a first-class L1 tool — call it directly.
529
871
  yootheme_builder_element_bind_source({
530
872
  template_id: "home",
531
- element_path: "/0/children/2/children/0",
873
+ element_path: "/children/0/children/2/children/0",
532
874
  source_name: "wp_posts",
533
875
  etag: "abc123"
534
876
  // source_id: "wordpress:wp_posts" // pass ONLY when name collides
535
877
  })
536
- // Response: { path: "/0/children/2/children/0", etag: "def456", has_binding: true }
878
+ // Response: { rel_path: "/children/0/children/2/children/0", etag: "def456", has_binding: true }
537
879
  // Verify (direct L1 call):
538
880
  yootheme_builder_element_get_binding({
539
- template_id: "home", element_path: "/0/children/2/children/0"
881
+ template_id: "home", element_path: "/children/0/children/2/children/0"
540
882
  })
541
883
  // → { source_name: "wp_posts", source_config: { ... }, ... }
542
884
  ```
@@ -666,20 +1008,20 @@ CMS-level template duplication (in wp-admin or Joomla administrator).
666
1008
  // Step 5. Clone the section element as a sibling.
667
1009
  yootheme_builder_element_clone({
668
1010
  template_id: "home",
669
- element_path: "/0/children/2", // the hero section to duplicate
1011
+ element_path: "/children/0/children/2", // the hero section to duplicate
670
1012
  etag: "abc123"
671
1013
  })
672
- // Response: { path: "/0/children/3", etag: "def456" }
1014
+ // Response: { rel_path: "/children/0/children/3", etag: "def456" }
673
1015
 
674
1016
  // Step 7. Tweak the clone (replace props entirely, or pass merge: true).
675
1017
  const current = yootheme_builder_element_get({
676
1018
  template_id: "home",
677
- element_path: "/0/children/3",
1019
+ element_path: "/children/0/children/3",
678
1020
  });
679
1021
  yootheme_builder_element_update_settings({
680
1022
  template_id: "home",
681
- element_path: "/0/children/3",
682
- props: { ...current.props, background: "secondary" },
1023
+ element_path: "/children/0/children/3",
1024
+ props: { ...current.props, style: "secondary" }, // section background = `style`
683
1025
  etag: "def456"
684
1026
  })
685
1027
  ```
@@ -882,15 +1224,19 @@ yootheme_builder_element_types_list({
882
1224
  yootheme_builder_element_type_get_schema({ element_type: "headline" })
883
1225
  // Returns the field definitions for the headline's `props`.
884
1226
 
885
- // Step 6. Place the element.
1227
+ // Step 6. Place the element. This is the HERO headline, so title_element:"h1"
1228
+ // — the ONE h1 the page is allowed. A section title would be "h2", a card
1229
+ // title "h3", a metric number "div". `content` is bare inline text (never a
1230
+ // <p> block — see "Typography & heading styles"). If you omit title_element it
1231
+ // DEFAULTS to h1, so set it explicitly on every headline to avoid multiple h1s.
886
1232
  yootheme_builder_element_add({
887
1233
  template_id: "home",
888
- parent_path: "/0/children/2", // row inside section
1234
+ parent_path: "/children/0/children/2", // row inside section
889
1235
  element_type: "headline",
890
- props: { content: "Welcome", tag: "h1" },
1236
+ props: { content: "Welcome", title_element: "h1" },
891
1237
  etag: "abc123"
892
1238
  })
893
- // Response: { path: "/0/children/2/children/0", etag: "def456" }
1239
+ // Response: { rel_path: "/children/0/children/2/children/0", etag: "def456" }
894
1240
  ```
895
1241
 
896
1242
  **Edge case:** A child theme can override a built-in element's
@@ -906,7 +1252,161 @@ and the props payload you passed.
906
1252
 
907
1253
  ---
908
1254
 
909
- ## When something doesn't fit one of these 5 workflows
1255
+ ## Workflow 6: SEO audit (pages_audit)
1256
+
1257
+ **Goal:** Run one deterministic, site-wide sweep for structure, content,
1258
+ SEO, and link issues, drill into the pages a finding names, and fix them
1259
+ one page at a time through the existing write tools this server already
1260
+ exposes. Never analyze findings by hand and never bulk-apply a fix.
1261
+
1262
+ **Canonical tool-call sequence (real parameter names, snake_case):**
1263
+
1264
+ 1. `yootheme_builder_advanced_read({ tool: "yootheme_builder_pages_audit", arguments: { checks: ["seo", "structure"] } })`
1265
+ sweeps the whole site in one call. `pages_audit` is a read-only
1266
+ advanced tool, so reach it through the read gateway (the write
1267
+ gateway routes it too, but the read gateway never triggers a
1268
+ write-approval prompt). Omit `checks`/`types` to run every category
1269
+ against every scanned template.
1270
+ 2. Read the response's `findings` array. Each finding carries a `code`,
1271
+ `category`, `severity`, a `count`, up to 30 affected `pages`, and
1272
+ (where a safe automatic fix exists) a `fix_hint` naming the exact
1273
+ write tool and args template to run. `duplicate-layout` and
1274
+ `title-collision` never carry a `fix_hint` — they need a human
1275
+ decision (rename, merge, or intentionally keep as-is), not an
1276
+ automatic patch.
1277
+ 3. `yootheme_builder_template_summary({ template_id })` drills into one
1278
+ page a finding names. It carries the same `heading_levels`,
1279
+ `images_missing_alt`, and `disabled_elements` counters the audit
1280
+ used, so you can see exactly which nodes are affected before
1281
+ writing.
1282
+ 4. Fix that ONE page via the write tool named in `fix_hint.tool`, with
1283
+ `fix_hint.args_template` as a starting point (`element_update_settings`,
1284
+ `element_add`, etc), ETag-locked like every other write in this
1285
+ server.
1286
+ 5. `yootheme_builder_page_publish({ template_id, etag })` republishes.
1287
+ The response's `quick_check` block (default on) re-checks THIS page
1288
+ with the same per-page codes, so you can confirm the fix landed
1289
+ without a second full sweep.
1290
+
1291
+ **Common pitfalls:**
1292
+
1293
+ - **Promising a full SEO audit.** `pages_audit` covers Builder structure
1294
+ and CMS basics only: heading hierarchy, empty/thin pages, missing
1295
+ image alt text, duplicate layouts, title collisions, non-pretty URL
1296
+ fallbacks, stale content, and dead in-page anchors. Every response
1297
+ carries a fixed disclaimer field:
1298
+ `"Meta title/description/OG live in your CMS SEO plugin — not covered here."`
1299
+ It does not read or evaluate Yoast/RankMath/any SEO-plugin meta
1300
+ title, meta description, or Open Graph tags. Say this scope boundary
1301
+ out loud and point the user at their SEO plugin for that layer.
1302
+ - **Confusing the CONTENT count with `pages_list.elements_count`.**
1303
+ `pages_list.elements_count` counts every node, including
1304
+ `section`/`row`/`column`/`fragment` layout wrappers. `pages_audit`
1305
+ instead uses a narrower CONTENT count (`elements_content` in the
1306
+ evidence) that excludes `section`/`row`/`column`/`fragment`/`layout`
1307
+ wrapper nodes: an empty section nested in three columns is correctly
1308
+ `0` content elements, not "not empty" just because the wrappers
1309
+ exist. Both numbers ride in the evidence (`elements_total` and
1310
+ `elements_content`). `thin-content` fires when `elements_content` is
1311
+ below `thin_content_threshold` (default 8); `empty-page` fires at
1312
+ `elements_content === 0`.
1313
+ - **The h1-default trap.** Only `headline` defaults to `h1` when
1314
+ `title_element` is unset. A container element (`grid`, `panel`,
1315
+ `slideshow`, and the rest) left unset does NOT become an `<h1>` — it
1316
+ becomes an `<h3>` (or stays a non-heading `div` for
1317
+ `description_list`). See the per-type default table below and
1318
+ "`title_style` vs `title_element`" above (that section's "defaults
1319
+ to `h1`" rule is specific to `headline` only). `meta_element` is a
1320
+ SECOND, independent heading channel on the same container types,
1321
+ defaulting to `div` and counting only when explicitly set to
1322
+ `h1`-`h6` with `show_meta` not `false`.
1323
+ - **Treating a bound-but-empty slot as missing.** Bound title/meta
1324
+ slots count as filled even when the static prop text is empty — a
1325
+ title bound to a dynamic source field is not "missing" just because
1326
+ the builder-side prop is blank. The same exemption applies to
1327
+ `image_alt`/`background_image_alt`: `images-missing-alt` never flags
1328
+ a bound image-alt slot, only an unbound, empty, or whitespace-only
1329
+ alt on a static image.
1330
+ - **Flagging every `#`-prefixed value as a dead anchor.** A
1331
+ `dead-anchor` finding fires only for a genuinely dangling `#name`
1332
+ link whose target element `id` does not exist anywhere on the same
1333
+ page. It explicitly excludes a bare `#` (a placeholder, never a real
1334
+ target), `mailto:`/`tel:` links, `http(s)://` external links, and
1335
+ cross-page links like `?page_id=…#…` (that anchor lives on a
1336
+ different page's render and cannot be verified from this page's own
1337
+ `props.id` set).
1338
+ - **Treating `missing-h1` as critical, or double-flagging it.**
1339
+ Severity is calibrated from live empirical review, not per-rule
1340
+ guessing: `no-headings` (zero headings anywhere on the page) is
1341
+ critical, but `missing-h1` (headings exist, but none is an effective
1342
+ h1) is only a warning, and it is suppressed entirely when
1343
+ `no-headings` already fired for the same page. Real sites
1344
+ legitimately have h2/h3-only pages with no h1.
1345
+ - **Treating `quick_check` findings as a publish failure.** `quick_check`
1346
+ is advisory only: publish and save always succeed regardless of what
1347
+ it reports. This is not the blocking render-precheck (which still
1348
+ gates on genuinely broken output) — SEO/structure findings are
1349
+ hints, not corruption. `quick_check` only ever includes checks whose
1350
+ `scope` is `"per-page"`; `duplicate-layout` and `title-collision`
1351
+ need the whole site swept to mean anything, so they never appear
1352
+ here — instead the block carries a fixed note:
1353
+ `"run pages_audit for site-wide checks"`. `page_publish` attaches
1354
+ `quick_check` by default (set `quick_check: false` to opt out, e.g.
1355
+ scripted bulk publishes); `page_save` carries it only when you opt
1356
+ in with `quick_check: true`.
1357
+
1358
+ **Per-type `title_element` default table (the correctness crux of every
1359
+ heading finding):**
1360
+
1361
+ | Element type(s) | `title_element` default | Counts as heading? |
1362
+ |---|---|---|
1363
+ | `headline` | `h1` | Yes, unless explicitly overridden. |
1364
+ | `comments` (WP source) | `h2` | Yes, always active. |
1365
+ | `alert`, `gallery`, `grid`, `map`, `overlay`, `overlay-slider`, `panel`, `panel-slider`, `popover`, `slideshow`, `switcher` | `h3` | Yes, when `show_title` is not `false` and the title is non-empty or bound. |
1366
+ | `description_list` | `div` | No. |
1367
+ | `section` (Section Title) | (no field; hardcoded `<div class="tm-section-title">`) | No, ever (counted only as an informational `section_titles` tally). |
1368
+
1369
+ `div` never counts as a heading whether it came from an explicit value
1370
+ or a type default.
1371
+
1372
+ **Worked example (tool-call snippet):**
1373
+
1374
+ ```jsonc
1375
+ // Step 1. Sweep the site for SEO + structure issues.
1376
+ yootheme_builder_advanced_read({
1377
+ tool: "yootheme_builder_pages_audit",
1378
+ arguments: { checks: ["seo", "structure"] }
1379
+ })
1380
+ // A finding: { code: "missing-h1", category: "structure", severity: "warning",
1381
+ // count: 3, pages: [{ id: "page:42", label: "About" }, ...],
1382
+ // fix_hint: { tool: "element_update_settings",
1383
+ // args_template: { props: { title_element: "h1" } }, note: "..." } }
1384
+
1385
+ // Step 3. Drill into the flagged page.
1386
+ yootheme_builder_template_summary({ template_id: "page:42" })
1387
+ // heading_levels: { h1: 0, h2: 2, h3: 5, div: 1 }, images_missing_alt: 2
1388
+
1389
+ // Step 4. Fix that ONE page via the write tool the fix_hint named.
1390
+ yootheme_builder_element_update_settings({
1391
+ template_id: "page:42",
1392
+ element_path: "/children/0/children/0",
1393
+ props: { title_element: "h1" },
1394
+ etag: "abc123"
1395
+ })
1396
+
1397
+ // Step 5. Republish; quick_check re-confirms this page is clean.
1398
+ yootheme_builder_page_publish({ template_id: "page:42", etag: "def456" })
1399
+ // Response: { ..., quick_check: { clean: true } }
1400
+ ```
1401
+
1402
+ **Success criterion:** A re-run of `pages_audit` (or the `quick_check`
1403
+ block on the next publish) no longer lists the fixed finding for that
1404
+ page, and the response's `summary.critical`/`warning`/`info` totals go
1405
+ down.
1406
+
1407
+ ---
1408
+
1409
+ ## When something doesn't fit one of these 6 workflows
910
1410
 
911
1411
  - **Move an element** (intra-template reorder/reparent): use
912
1412
  `yootheme_builder_element_move({ template_id, element_path,
@@ -951,6 +1451,11 @@ and the props payload you passed.
951
1451
  `yootheme_builder_advanced({ tool: "yootheme_builder_clean_implode_directives", arguments: { template_id, element_path, etag } })`.
952
1452
  Returns the audit log + a fresh ETag; idempotent (`cleaned_count: 0` when
953
1453
  there is nothing to remove).
1454
+ - **Start from a pre-designed layout or element preset instead of
1455
+ hand-building** (the user wants a "Home"/"Contact"/"Services" page, or a
1456
+ ready-made Accordion/Alert preset, not a section built element-by-element):
1457
+ see "Layout Library" below (`library_browse` → `library_get` →
1458
+ `library_insert` → adapt).
954
1459
 
955
1460
  If the user asks for something none of the above covers (e.g. global
956
1461
  theme settings, menu management, media library), tell them clearly:
@@ -964,24 +1469,32 @@ Every element is addressed by a JSON-Pointer `path` (RFC-6901-style).
964
1469
  The read tools hand you the path; the write tools consume it. Learn the
965
1470
  grammar once and you never guess an address again.
966
1471
 
967
- **The shape.** The canonical address is index-first: the first segment is a
968
- bare child index, then `/children/<index>` for each level down. A two-level
969
- path is `/0/children/1` (child 0 of the layout root, then child 1 of that); a
970
- three-level path is `/0/children/2/children/0`. Every Worked example in this
971
- skill and every write-tool response uses this index-first form.
1472
+ **The canonical shape (use this): rel_path.** A `rel_path` is a proper
1473
+ RFC-6901 pointer relative to the layout root: `/children/<index>` for each
1474
+ level down. A two-level path is `/children/0/children/1` (child 0 of the
1475
+ layout root, then child 1 of that); a three-level path is
1476
+ `/children/0/children/2/children/0`. Each `/children/N` descends one level
1477
+ into the Nth child (0-based). This is the form `element_list` emits as
1478
+ `rel_path`, and the form every worked example in this skill uses.
972
1479
 
973
- - **Relative (rel_path):** `/0/children/2/children/0` is the path FROM the
974
- layout root. Each `/children/N` descends one level into the Nth child
975
- (0-based).
976
1480
  - **Fully-qualified:** `/templates/<template_id>/layout/children/0/...` is the
977
- same address with the template prefix.
978
-
979
- **Both forms are accepted.** `element_list` / `page_get_schema` emit `path`
980
- fully-qualified and `rel_path` with a leading `/children/0/...` (a `/children`
981
- prefix on the first segment too); write-tool responses use the index-first
982
- `/0/children/...` form. You may pass EITHER back as `element_path` /
983
- `parent_path`: the server normalizes them (the `normalizeElementPath` rule),
984
- and a leading slash is optional (`0/children/1` works too).
1481
+ same address with the template prefix. Read/write tools return the
1482
+ fully-qualified pointer as `path`; you can pass it straight back.
1483
+
1484
+ **What the tools EMIT:** `element_list` / `page_get_schema` return both `path`
1485
+ (fully-qualified) and `rel_path` (the canonical `/children/0/...` form); every
1486
+ write-tool response (`element_add`, `element_update_settings`, `element_clone`,
1487
+ `element_move`, `element_bind_source`, `page_dynamize`, `scaffold_section`) and
1488
+ `element_get` return the fully-qualified `path` plus the canonical `rel_path`.
1489
+ Copy either straight back into the next call.
1490
+
1491
+ **What the server ACCEPTS (be liberal):** as `element_path` / `parent_path`
1492
+ you may pass the canonical `rel_path` (`/children/0/...`), the fully-qualified
1493
+ pointer (`/templates/<id>/layout/...`), OR the terse index-first form
1494
+ (`/0/children/1`, where a leading bare index is the Nth layout-root child) —
1495
+ all three normalize to the same node, and a leading slash is optional. Prefer
1496
+ `rel_path`: it is the canonical, standards-correct form and what the tools
1497
+ hand you.
985
1498
 
986
1499
  **The two special roots:**
987
1500
 
@@ -991,7 +1504,7 @@ and a leading slash is optional (`0/children/1` works too).
991
1504
  layout node itself, which is not an addressable element. Use `""` for
992
1505
  "the top".
993
1506
 
994
- **Reading the index.** In `/0/children/2/children/1`: child 0 of the layout,
1507
+ **Reading the index.** In `/children/0/children/2/children/1`: child 0 of the layout,
995
1508
  then child 2 of THAT, then child 1 of THAT. Indices are positional and shift
996
1509
  when siblings are added or removed; re-read after a structural mutation before
997
1510
  reusing a path (see the ETag handshake below).
@@ -1100,11 +1613,578 @@ styles, the `#parent` bindings) survives untouched.
1100
1613
  - **Reusing a stale etag.** Feed `new_etag` from the response into your
1101
1614
  next write.
1102
1615
 
1616
+ ---
1617
+
1618
+ ## Layout Library (browse → get → insert → adapt)
1619
+
1620
+ **Goal:** Start a page or section from a pre-designed YOOtheme Pro Library
1621
+ layout or single-element preset instead of hand-building it with
1622
+ `element_add` calls — then adapt the demo content to the customer's brand.
1623
+
1624
+ Four tools, always used in this order (the write-capable third step is
1625
+ the only one that mutates anything):
1626
+
1627
+ 1. `yootheme_builder_library_browse` (read) — search the catalog by
1628
+ taxonomy filters, get back rows with an `id`.
1629
+ 2. `yootheme_builder_library_get` (read) — fetch ONE item's full tree +
1630
+ its section list, using the `id` a browse row gave you.
1631
+ 3. `yootheme_builder_library_insert` (write) — compose + persist that
1632
+ item onto a page or existing template through the same hardened
1633
+ write chain every other element/page write goes through.
1634
+ 4. `yootheme_builder_library_save_as` — the REVERSE direction (a page you
1635
+ built → a reusable library). By default it saves into your OWN local
1636
+ "WootsUp AI Layouts" store (`destination: "ai"`, no apikey — see the
1637
+ dedicated subsection below), NOT YOOtheme's native My Layouts. Reachable
1638
+ ONLY via the gateway:
1639
+ `yootheme_builder_advanced({ tool: "yootheme_builder_library_save_as", arguments: { source: { template_id, element_path }, name: "Hero Section" } })`.
1640
+ It is deliberately NOT a first-class `tools/list` entry (keeps the
1641
+ tools/list token-budget clear); browse/get/insert ARE first-class.
1642
+
1643
+ Two `source`s feed browse/get/insert: `"catalog"` (default — YOOtheme's
1644
+ remote Pro Library) and `"ai"` (your OWN local "WootsUp AI Layouts", no
1645
+ apikey). See "WootsUp AI Layouts" below.
1646
+
1647
+ ### Prerequisite: the REMOTE catalog needs a Library API key configured
1648
+
1649
+ Browse, get, AND insert against the remote catalog (`source: "catalog"`,
1650
+ the default) all depend on a YOOtheme Pro Library API key configured on the
1651
+ connected site (the customer's own theme `apikey`, set once in the YOOtheme
1652
+ Customizer — not something this MCP server manages). If it's missing,
1653
+ `library_browse` does not error — it returns `{ items: [], total: 0 }`.
1654
+ Treat `total: 0` as "ask the customer to check the theme apikey field in the
1655
+ Customizer", not a network/auth failure to retry blindly. **This apikey
1656
+ requirement applies ONLY to the remote catalog** — the local AI-Layouts
1657
+ store (next subsection) needs no apikey at all.
1658
+
1659
+ ### WootsUp AI Layouts — your OWN local library (no apikey, save → reuse)
1660
+
1661
+ The catalog above (`source: "catalog"`, the default) is YOOtheme's REMOTE
1662
+ Pro Library and needs the theme apikey. There is a SECOND source that is
1663
+ entirely LOCAL to the connected site: **"WootsUp AI Layouts"**, the plugin's
1664
+ own isolated store. It works WITHOUT any apikey and it NEVER touches
1665
+ YOOtheme's native "My Layouts". This is the loop that lets an agent build a
1666
+ section once and reuse it across pages — even on a site that has no Library
1667
+ apikey at all.
1668
+
1669
+ - **`source` axis on `library_browse` / `library_get` / `library_insert`:**
1670
+ `source: "catalog"` (default) is the remote Pro Library (needs the apikey);
1671
+ `source: "ai"` is your OWN "WootsUp AI Layouts", served from the local
1672
+ store with no apikey. The `id` you pass to `library_get` /
1673
+ `library_insert` MUST come from a `library_browse` with the SAME `source`.
1674
+ The `"ai"` source honours only `query` (name substring) + `kind`
1675
+ (`layout` | `element`); the catalog layout/element taxonomy filters
1676
+ (`topic`/`type`/`role`/`website`) and `kind: "section"` are REJECTED with
1677
+ a 400 (an honest error, not a silent no-op) — the local store has no
1678
+ taxonomy, only what you saved. (`purpose`/`has_effects` are section-only
1679
+ filters that don't apply to a `layout`/`element` browse at all, so —
1680
+ exactly as on the catalog path — they are silently ignored on
1681
+ `source: "ai"`, never reaching the store; they are NOT rejected.)
1682
+ - **`destination` axis on `library_save_as`:** `destination: "ai"` (DEFAULT —
1683
+ save into the isolated "WootsUp AI Layouts" store, reusable via
1684
+ `library_browse` / `library_insert` with `source: "ai"`, and it NEVER
1685
+ pollutes YOOtheme's native My Layouts) or `destination: "native"` (the
1686
+ escape hatch — save into YOOtheme's built-in "My Layouts"). Because `"ai"`
1687
+ is the default, a plain `library_save_as` already writes to YOUR store,
1688
+ not the theme's.
1689
+ - **Composed AS-SAVED.** A `source: "ai"` insert replays the tree exactly as
1690
+ you saved it — its images are NOT re-resolved (they are already the
1691
+ customer's own images / placeholders), so `library_insert`'s `images`
1692
+ param is a no-op for `source: "ai"`.
1693
+
1694
+ **Round-trip (save your own layout, then reuse it — no apikey):**
1695
+
1696
+ ```jsonc
1697
+ // 1. Save a section you built (or adapted from the catalog) into YOUR store.
1698
+ // destination:"ai" is the default, shown here for clarity. save_as is
1699
+ // gateway-only.
1700
+ yootheme_builder_advanced({ tool: "yootheme_builder_library_save_as", arguments: {
1701
+ source: { template_id: "home", element_path: "/children/2" },
1702
+ name: "Candle Hero",
1703
+ destination: "ai"
1704
+ } })
1705
+ // → { library_id: "a1b2c3d4", name: "Candle Hero", storage: "ai_library.a1b2c3d4", invalid_source_possible: false }
1706
+
1707
+ // 2. Later, on ANY page (even one with no Library apikey) browse YOUR store …
1708
+ yootheme_builder_library_browse({ source: "ai", query: "hero" })
1709
+ // → { items: [{ id: "a1b2c3d4", name: "Candle Hero", kind: "layout" }], total: 1 }
1710
+
1711
+ // 3. … and insert it — same hardened write chain as a catalog insert.
1712
+ yootheme_builder_library_insert({
1713
+ id: "a1b2c3d4",
1714
+ source: "ai",
1715
+ target: { template_id: "about", element_path: "" },
1716
+ etag: "<etag>"
1717
+ })
1718
+ ```
1719
+
1720
+ **When to use which store:** reach for `source: "catalog"` to START from
1721
+ YOOtheme's professionally-designed demos (needs the apikey); reach for
1722
+ `source: "ai"` to REUSE a layout you already saved on this site — your own
1723
+ reusable building blocks, apikey or not.
1724
+
1725
+ ### The taxonomy filters on `library_browse`
1726
+
1727
+ `kind` picks which catalog you're browsing: `"layout"` (full pre-designed
1728
+ pages, the default), `"element"` (single-element presets), or **`"section"`
1729
+ (the SECTION CORPUS — every individual section of every Pro layout,
1730
+ classified by purpose)**. The remaining filters are exact-match taxonomy
1731
+ axes, and one of them has a name that does NOT match YOOtheme's own public
1732
+ vocabulary — know this before you guess a param name from the YOOtheme docs:
1733
+
1734
+ | Param | Axis (kind) | Example values |
1735
+ |----------------|--------------------------|----------------------------------------------|
1736
+ | `topic` | industry (all kinds) | `"Health & Fitness"`, `"Beauty & Fashion"` |
1737
+ | `type` | site purpose (layout+section) | `"Company"`, `"E-commerce"`, `"Magazine"` |
1738
+ | `role` | page role (layouts) | `"Home"`, `"Contact"`, `"Services"` |
1739
+ | `purpose` | **section role (sections)** | `"hero"`, `"features"`, `"cta"`, `"gallery"`, `"pricing"`, `"faq"`, `"contact"`, `"team"`, `"stats"`, `"testimonial"`, `"logos"`, `"content"` |
1740
+ | `has_effects` | **section has parallax/sticky/etc (sections, bool)** | `true` / `false` |
1741
+ | `element_type` | preset element (presets) | `"accordion"`, `"alert"`, `"headline"` |
1742
+ | `website` | source demo (all) | `"Makai"`, `"FC Greenfield"`, `"Balou"` |
1743
+ | `query` | substring on name (all) | `"hero"` |
1744
+
1745
+ **`kind: "section"` is how you COMPOSE a page from individually-chosen
1746
+ sections** instead of dropping one whole demo layout. A section row's `id`
1747
+ is the composite `"<layoutId>#<sectionIndex>"` (e.g. `"955bd3dc#0"`); each
1748
+ row also carries `purpose`, `topic`/`type`/`website`, `visual_effects`, and
1749
+ a `thumbnail_url` (its parent layout's preview). Feed that composite `id`
1750
+ straight into `library_get` / `library_insert` with `kind: "section"`.
1751
+
1752
+ **`role` is YOOtheme's "layout" taxonomy axis, renamed.** YOOtheme's own
1753
+ catalog calls this 4th axis "layout" (Home/Footer/Post/Services/Contact/…
1754
+ — the page's ROLE), but the MCP tool parameter is `role`, not `layout` —
1755
+ `kind: "layout"` already occupies that word for "which catalog". Passing
1756
+ `layout: "Home"` to `library_browse` is silently ignored (unknown key
1757
+ rejected by the schema); use `role: "Home"`.
1758
+
1759
+ `limit` (1-100, default 25) + `cursor` (from a previous response's
1760
+ `next_cursor`) paginate; `total: 0` (with an otherwise-valid filter
1761
+ combination) usually means the filter combination matched nothing, not a
1762
+ missing key — narrow or drop filters to check.
1763
+
1764
+ ### Composing a PROFESSIONAL page — the selection ladder (do NOT grab the first result)
1765
+
1766
+ The catalog returns rows in a FIXED order. The first rows are NOT "the
1767
+ best" — they are just first, and one demo (e.g. Balou) sits at the top of
1768
+ most loosely-filtered results. Grab the top row and you ship that one demo
1769
+ relabeled. A professional result comes from FIT, not position:
1770
+
1771
+ 1. **Read the brief for INDUSTRY + TONE.** "Handmade scented candles,
1772
+ warm/boutique" → industry ≈ retail/e-commerce, tone ≈ warm, premium,
1773
+ calm. "SaaS analytics dashboard" → type Company/Software, tone clean/
1774
+ minimal/techy. Name the tone explicitly before you browse.
1775
+ 2. **Filter to the relevant demos** with `type` (E-commerce/Company/
1776
+ Magazine…) + `topic` (industry). This narrows hundreds of layouts to the
1777
+ few that could fit.
1778
+ 3. **EVALUATE MULTIPLE candidate websites — never take the first.** Scan the
1779
+ distinct `website` values in the filtered rows (an E-commerce filter
1780
+ surfaces Balou, Woolberry, Tech Space, …). For your 2-3 finalists pull the
1781
+ thumbnail — `thumbnail_url` is free; `library_get({ include_thumbnail:
1782
+ true })` returns the actual image block (~950 tokens, finalists ONLY) —
1783
+ and JUDGE which demo's aesthetic matches the brief's TONE. A playful
1784
+ kids-brand demo (cartoon mascots, bright primaries) is the WRONG base for
1785
+ a premium/calm brand even when it is technically the right `type`.
1786
+ 4. **Commit to ONE `website` for the whole page.** Sections from different
1787
+ demos carry different spacing rhythm, type scale, and decorative
1788
+ language; mixing them reads as inconsistent. Pick the single best-fit
1789
+ `website` and compose from ITS sections (`kind: "section"`, `website:
1790
+ "X"`), or load its whole matching layout. Mix websites only when you
1791
+ deliberately need a section no single demo provides — then reconcile the
1792
+ look by eye.
1793
+ 5. **Whole layout vs section-compose.** If ONE demo layout already matches
1794
+ the page you need (hero → features → about → contact), load it whole
1795
+ (`kind: "layout"`) for guaranteed internal consistency. If you need a
1796
+ bespoke structure (this demo's hero + that demo's pricing table),
1797
+ assemble it from `kind: "section"` rows.
1798
+
1799
+ ### Make it the CUSTOMER's brand, not the demo's
1800
+
1801
+ A loaded demo carries that demo's SPECIFIC identity. "Adapting" is NOT just
1802
+ rewriting headlines — you must strip the demo's fingerprints, or the page
1803
+ reads as an obviously-relabeled template:
1804
+
1805
+ - **Off-brand decorative graphics.** Many demos scatter decorative SVG
1806
+ illustrations (a kids-shop's cartoon rockets/planets/clouds; a hand-drawn
1807
+ mascot). Those belong to THAT brand. A section row's `visual_effects`
1808
+ listing `floating_decoration` is the signal it carries them. After
1809
+ inserting, locate them (`page_get_layout({ mode: "outline" })` or
1810
+ `layout_query`) and `element_delete` the ones that don't fit the
1811
+ customer's brand. A premium candle shop with cartoon rockets drifting past
1812
+ its hero is instantly "not real".
1813
+ - **Demo product/subject photography.** `curated` downloads the demo's OWN
1814
+ photos — including its specific products (a knitwear demo's baby suits, a
1815
+ realty demo's houses). You cannot honestly relabel a teddy-bear photo as
1816
+ "Vanilla Candle". For product/subject slots, clear those images to
1817
+ placeholder (awaiting the customer's real photos) rather than shipping a
1818
+ mismatched photo. Atmospheric/texture shots (a calm interior, hands at
1819
+ work) often transfer; product-identity shots do not.
1820
+ - **Copy.** Rewrite EVERY headline, subheading, button, and body string to
1821
+ the customer's brand and offering — leave NO demo copy ("Knit & More",
1822
+ "Inspired by our children") anywhere.
1823
+ - **The professionalism signal is the IMAGERY.** Structure and style come
1824
+ from the Library for free; what separates "finished" from "template" is
1825
+ on-brand images. A clean placeholder reads as "ready for your photos"; a
1826
+ MISMATCHED demo photo reads as "broken". When you cannot supply the
1827
+ customer's real image, prefer the honest placeholder over the wrong photo
1828
+ — and TELL the customer which slots need their photography.
1829
+ - **Not every section degrades gracefully to a placeholder — CHOOSE ONES
1830
+ THAT DO.** This is decisive for the hero, which sets the whole first
1831
+ impression. A single `image` element renders YOOtheme's clean placeholder
1832
+ graphic when empty (looks intentional). But an image-DRIVEN section —
1833
+ a `slideshow` / `gallery` / full-bleed background-image hero — renders
1834
+ BLANK (an empty white void, not even a placeholder graphic) when its
1835
+ images are placeholders. A blank slideshow hero reads as broken, not
1836
+ "ready for photos". So when you will leave a section on placeholders
1837
+ (i.e. you don't have the customer's photos yet), PREFER sections whose
1838
+ design holds up WITHOUT a photo: a text-on-solid-`style` hero (dark or
1839
+ muted section + headline + CTA), a typographic/`content` section, an
1840
+ icon/feature grid. AVOID committing to a `slideshow`/`gallery`/full-bleed
1841
+ photo hero unless you actually have images to fill it. Check a candidate
1842
+ section's `element_signature` (a `slideshow`/`gallery`/`overlay-slider`
1843
+ signature = image-driven) before you make it your imageless hero. A calm
1844
+ text hero with clean type beats a blank photo slot every time.
1845
+ - **CONTRAST is non-negotiable — dark text on a dark panel is invisible.**
1846
+ A section originally designed as text-over-a-photo often carries dark text
1847
+ meant to sit on a light image. When you clear that image and the section
1848
+ falls back to a dark `style` (`secondary` / `primary` resolve dark on most
1849
+ themes), the text stays dark → dark-on-dark → the hero looks empty/broken.
1850
+ Whenever a section sits on a DARK background, set the SECTION's
1851
+ `text_color: "light"` (it cascades to all descendants) so the headline,
1852
+ subhead and CTA read; on a LIGHT/`muted` background leave `text_color`
1853
+ default (dark). This is a section-level prop and the ONE style token you
1854
+ DO adjust when re-theming a hero (it is a legibility fix, not a brand
1855
+ override). After building an imageless hero, sanity-check: is the headline
1856
+ clearly readable against its background? If you cannot tell, the customer
1857
+ can't either — fix the contrast. For a hero over a real PHOTO (not a solid
1858
+ style), text_color alone is not enough; see "A photo hero needs a scrim"
1859
+ under the image-sourcing subsection.
1860
+
1861
+ ### `library_get`: the natural next call
1862
+
1863
+ `library_get({ id })` (the `id` comes straight off a `library_browse`
1864
+ row) returns `{ id, kind, name, version, tree, sections,
1865
+ images_manifest_count }`. `sections` lists the item's top-level
1866
+ `tree.children` as `{ index, name, path, element_count }` — layouts only;
1867
+ a preset (`kind: "element"`) always returns `sections: []` because a
1868
+ preset IS one element, not a container of sections. Use a `sections[]`
1869
+ entry's `index` as `library_insert`'s `section_index` to splice in ONE
1870
+ section instead of the whole layout (e.g. just the "Hero" section from a
1871
+ 6-section Home layout).
1872
+
1873
+ ### Load-semantics: what you get is NOT the demo thumbnail
1874
+
1875
+ **A Library item ADOPTS the connected site's currently-active Style**
1876
+ (the YOOtheme Customizer's global UIkit theming) when it renders — it
1877
+ does **not** carry the demo site's colors with it. The catalog item's
1878
+ props are semantic UIkit tokens (`style: "primary"` / `"muted"` /
1879
+ `"secondary"`, `text_color: "light"`, UIkit utility classes), never
1880
+ hardcoded hex colors. Those tokens resolve against whatever style the
1881
+ CONNECTED site currently has active, so the same "Balou Home" layout
1882
+ renders in the customer's own brand colors, not Balou's. Do not tell a
1883
+ customer "this will look like the screenshot" — it will look like their
1884
+ site.
1885
+
1886
+ **When you adapt the inserted content, preserve those semantic tokens —
1887
+ never replace them with a hardcoded color.** Swapping `style: "primary"`
1888
+ for a literal `#1a73e8` (or similar) breaks themeability: the section
1889
+ stops following the site's Customizer style entirely, and a future
1890
+ Customizer style change silently skips it. "Adapting" a Library item
1891
+ means changing CONTENT (copy, images, links) — never the style/
1892
+ `text_color` tokens, the responsive `width_*` props, or any of the other
1893
+ structural/authoring props documented above (Multi-column layouts,
1894
+ Typography). If a specific override is genuinely wanted, that's a
1895
+ deliberate `element_update_settings` call the customer asked for, not a
1896
+ default step of "adapting" a Library insert.
1897
+
1898
+ **Collapsing is a feature — don't defeat it with dummy content.** An
1899
+ empty/unbound field in an inserted section collapses cleanly at
1900
+ render-time (Element → Column → Row → Section cascade; a 3-column grid
1901
+ with 2 empty columns renders as one full-width column). Leave a field you
1902
+ have nothing to put there GENUINELY empty rather than stuffing placeholder
1903
+ text into it "so it doesn't look broken" — the collapsing behavior is
1904
+ what makes a Library section adapt gracefully to less content than the
1905
+ demo had.
1906
+
1907
+ ### Images: `placeholder` (default) vs `curated`
1908
+
1909
+ `library_insert`'s `images` param controls what happens to the item's
1910
+ demo image references (`image`, `image_svg`, `poster`, and several
1911
+ element-specific media props — all point at short-lived signed CDN URLs
1912
+ at fetch time, never persisted raw):
1913
+
1914
+ - **`"placeholder"` (the default — zero network I/O).** Every image prop
1915
+ is cleared to an empty value; YOOtheme's own placeholder graphic renders
1916
+ in its place. This is the right choice for a REAL customer site — the
1917
+ agent (or the customer) fills the placeholders with the customer's own
1918
+ photos afterward. Nothing is downloaded, nothing sideloads into the
1919
+ Media Library.
1920
+ - **`"curated"` — downloads every UNIQUE demo image once into the site's
1921
+ Media Library** (a signed URL reused across multiple props/nodes, e.g.
1922
+ the same hero photo as both a section background and a card thumbnail,
1923
+ is imported exactly once, not N times). Use this for a QUICK DEMO where
1924
+ looking finished immediately matters more than using the customer's own
1925
+ photography. Requires the same Library API key as browse/get/insert —
1926
+ no separate key. A per-image download failure degrades ONLY that one
1927
+ image to a placeholder and is counted in the response; it never aborts
1928
+ the rest of the insert.
1929
+
1930
+ The insert response's `images_resolved: { downloaded, placeholders }`
1931
+ tells you exactly what happened — check it rather than assuming every
1932
+ image round-tripped.
1933
+
1934
+ ### Fill image slots with real on-topic photos (search → import → bind)
1935
+
1936
+ The professionalism signal is on-brand IMAGERY (see "Make it the CUSTOMER's brand"). You are not limited to leaving placeholders: you can pull REAL, on-topic photography from the SAME sources the YOOtheme Pro image dialog uses (Unsplash, Pexels, and YOOtheme Pro Images) and bind it into the layout. This is what turns a correct-but-bare skeleton into a RICH, finished-looking page.
1937
+
1938
+ **The flow (three tools):**
1939
+ 1. `yootheme_builder_image_search({ source, query, orientation })` returns candidate rows, each with a `full_url` (the image to import) plus dimensions and author. `source`: `unsplash` or `pexels` for real photography (the primary rich sources); `proimages` for YOOtheme's own curated decorative and background assets. Use `orientation: "landscape"` for heroes and banners, `"portrait"` for tall cards.
1940
+ 2. JUDGE the candidates by FIT and TONE, exactly like you choose a section, and never grab the first result. Search deliberately by the brief's INDUSTRY plus TONE: a coffee roaster searches `"coffee roasting"` / `"espresso pour"` / `"cafe interior"` (warm, editorial); a law firm searches `"modern office architecture"` (clean, restrained). A generic `query: "business"` returns generic stock that reads as filler.
1941
+ 3. `yootheme_builder_image_import({ url: "<full_url>" })` sideloads the chosen photo into the site's OWN Media Library and returns `{ local_url }`. Only Unsplash, Pexels, and YOOtheme image hosts can be imported (a security allowlist); other URLs are rejected.
1942
+ 4. Bind it: `yootheme_builder_element_update_settings({ template_id, element_path, props: { image: "<local_url>" }, merge: true })`.
1943
+
1944
+ **Real photo vs honest placeholder:**
1945
+ - A real, ON-TOPIC photo beats a placeholder every time; it is the single biggest lift from "template" to "finished". Fill the hero and feature-section image slots with searched photos that match the brief.
1946
+ - A MISMATCHED photo is worse than a placeholder. If you cannot find a genuinely on-topic image for a slot (for example a specific product the customer sells), leave the honest placeholder and tell the customer that slot needs their own photography. Never bind an off-topic photo just to fill space.
1947
+ - This does not change imageless-resilience: still avoid committing a slideshow / gallery / full-bleed-photo hero UNLESS you are filling it with real images. If you ARE filling it, a photo hero is now a strong choice.
1948
+ - CONTRAST still applies: a photo hero with overlaid text needs a legible contrast (a dark overlay, or the section `text_color`), the same check as an imageless hero.
1949
+
1950
+ Real photos are the customer's brand made visible: search for THEIR industry and tone, import the best fit, and leave honest placeholders only where a real photo genuinely cannot be found.
1951
+
1952
+ **A photo hero needs a scrim, not just `text_color: light`.** When a hero uses a real PHOTO as its background (not a solid section `style`), `text_color: "light"` alone is often not enough: a photo with bright regions (a flame, a bright sky, a white wall) leaves overlaid text unreadable where a line crosses that bright area. Real photo heroes need a SCRIM: a semi-transparent dark overlay between the photo and the text, so light text reads across the WHOLE block. Set it via the section/image element's own overlay/gradient prop (discover the exact key with `element_type_get_schema` on the element type carrying the image, e.g. an `overlay` or image-modifier prop), or place the headline group inside an inner panel with a translucent dark background. After binding a photo hero, sanity-check EVERY line (headline AND subheadline/meta), not just the top line: if any line sits over a bright part of the photo, add or darken the scrim. A headline that reads but a subheadline that washes out over the bright center is still a fail.
1953
+
1954
+ **If you write labels into a photo-only component, turn the labels ON.** Some Library sections are image-ONLY by design: a slideshow/carousel/grid/gallery with `show_title: false` and `show_content: false` (the demo shows only the photos, with any titles hidden or hover-only). If you repurpose such a section as a LABELED grid (named specialties, services, team, each with a caption under its photo), the titles and descriptions you write are STORED in the layout but NOT DISPLAYED until you set `show_title: true` (and `show_content: true` for descriptions) on that element. After filling such a section with real photos AND written labels, read the element back (`element_get`) and check `show_title` / `show_content`: if you added captions, enable them; if you genuinely want image-only, leave them off. Never leave written labels invisible.
1955
+
1956
+ ### Composing RICH, VARIED sections: the Pro-DNA palette (choose and vary, never default to bare)
1957
+
1958
+ The most common way an AI-built page falls short of a real Pro layout: it defaults to the SAME bare shape for every section (`section -> row -> column -> a grid of 3 plain text cards`, default props). Real Pro layouts are RICH and VARIED. This is a PALETTE plus a set of polish features so you CHOOSE the pattern that fits each brief and VARY it. It is NOT a fixed template. Two different briefs, and two different sections of the same page, should look different. Sameness is a failure here just as much as bareness.
1959
+
1960
+ **Where the variety comes from (in this order):**
1961
+ 1. Compose from the real Library sections FIRST. The section corpus (`library_browse({kind:"section"})`) is about 2000 individually-designed, already-rich sections across every demo. Selecting well among them for fit, tone, and variety, and preserving their design, is the primary path to a varied Pro-quality page. Do NOT strip their rich features (below).
1962
+ 2. When you hand-build a section (no corpus section fits, or you build from native elements), reach into the palette below and pick the rich pattern that fits. Never the bare-minimum version. Vary your choices across the page.
1963
+
1964
+ **The polish features Pro sections use constantly (these ARE what "rich" means): preserve them when composing, apply them when hand-building. The `panel` props named below are VERIFIED against the LIVE YOOtheme Pro 5.x `panel` schema (`element_type_get_schema("panel")` → 81 fields). `element_type_get_schema` is the source of truth — always confirm the exact key AND its enum values there before writing; a guessed key is hard-rejected and a guessed VALUE silently no-ops.**
1965
+ - **Card styling — THE single biggest "flat vs rich" lever: `panel_style` on a `panel`.** A bare `panel` (or a plain column of text) is what makes a section look flat; a `panel_style` turns it into a real styled CARD. Verified enum: `card-default` / `card-primary` / `card-secondary` / `card-hover` (hover-lift card) / `card-overlay` / `tile-default` / `tile-muted` / `tile-primary` / `tile-secondary`. A feature / team / pricing "card" is a `panel` WITH a `panel_style`, not three bare text columns.
1966
+ - **Hover** (on a `panel`; the image-hover props require a `link` + `panel_link`/`image_link`, per the schema `enable` rules): `hover_image` (swap the image on hover), `image_transition: scale-up`|`scale-down` (image zoom on hover), `title_hover_style: reset`|`heading`. `panel_style: card-hover` also gives a hover lift on its own.
1967
+ - **Depth**: `image_box_shadow` (`small`…`xlarge`) and `image_box_decoration` (`default`/`primary`/`secondary`/`shadow`/`mask`) — these apply to a panel image only when NO `panel_style` is set (they are mutually gated: use a `panel_style` OR the box-shadow/decoration, not both).
1968
+ - **Parallax** on section background images: the strongest single signal (~85% of feature/gallery/team sections carry it). Keep it when a Library section has it; on a hand-built image hero, add it (discover the section's parallax/image props via `element_type_get_schema("section")`).
1969
+ - **Entrance animation**: `animation` on the `section` (verified on the live section schema). Grids/galleries carry a per-item entrance-animation prop too — confirm its exact key on the `grid` / `gallery` schema before writing (do not assume `item_animation`).
1970
+ - **Deliberate spacing rhythm**: a panel's OWN margin props — `title_margin` / `meta_margin` / `content_margin` / `image_margin` (enum `xsmall`…`xlarge` / `remove`) — plus the `grid` element's row/column gap props. Never ship default spacing everywhere; confirm the exact keys on each element's schema.
1971
+ - **Section contrast**: alternate section `style` (`default` / `muted` / `primary` / `secondary`) so adjacent sections do not blur together; `text_color: light` on a dark section.
1972
+ - **Precise image control** (verified on the panel): `image_focal_point`, `image_align`, `image_width`, `image_border` (`rounded`/`circle`/`pill`).
1973
+ - **SVG shape dividers** between sections where it fits.
1974
+
1975
+ **Per-purpose PALETTE: a menu of rich options to CHOOSE from, not a mandate. Pick what fits the brief, and vary.**
1976
+ - Hero: a headline-stack over a parallax image, OR a `slideshow`, OR an `overlay`. Often full viewport-height with `text_color:light`. (Not: a lone headline on a flat solid.)
1977
+ - Features or Services: a grid of `panel`s each with a `panel_style` (e.g. `card-default` or `card-hover`), an icon or image, a `title` + `content`, and hover (`hover_image` / `image_transition`); OR a `slideshow` of feature panels; OR an alternating image-and-text rhythm. (Not: 3 plain text columns.)
1978
+ - Gallery or Portfolio: a filterable `gallery`, OR an overlay-panel grid with hover, OR a `slideshow`. (Not: a plain image grid.)
1979
+ - Testimonials: a quotation with image, OR an `overlay`, OR a `panel` slider. (Not: one flat quote.)
1980
+ - Team: a card grid of `panel`s with `panel_style: card-*` (photo=`image`, name=`title`, role=`meta`, hover). (Not: bare photos with captions.)
1981
+ - Pricing: a row of `panel`s with `panel_style: card-*` + a button, OR a comparison table.
1982
+ - FAQ: an `accordion`.
1983
+ - Stats: `countdown` or big-number panels.
1984
+ - Logos: a `gallery` or `list` of many logo items.
1985
+
1986
+ **The vary-mandate:** do not apply the same option to every section or every brief. A calm premium brand and a bold sporty brand get different patterns and different intensity of motion. Match the pattern, the polish, and the density to THIS brief.
1987
+
1988
+ **This pays off only on the right foundation:** a rich active Customizer Style (fonts and colors) and a matching YOOtheme version (the Library is authored for YOOtheme 5.x; build and test on a 5.x site). On a bare Style or an old version the polish cannot render.
1989
+
1990
+ ### `target`: where an item lands (and the strict Preset → Column rule)
1991
+
1992
+ `library_insert`'s `target` is EXACTLY ONE of three shapes, and which
1993
+ ones are legal depends on `kind`:
1994
+
1995
+ | `target` shape | Accepts `kind: "layout"` | Accepts `kind: "element"` (preset) |
1996
+ |---|---|---|
1997
+ | `{ new_page: { title, status? } }` | ✓ creates a page, layout's sections become its content | ✗ rejected — a preset is not a page |
1998
+ | `{ template_id, element_path? }` | ✓ ONLY the layout ROOT is section-accepting (`element_path: ""` or omitted) | ✓ ONLY a **Column** — every other node type is rejected |
1999
+ | `{ sublayout: { template_id, element_path } }` | ✓ the fetched layout's sections are stripped to bare ROWS first (a sublayout/fragment accepts rows, not sections) | ✗ rejected — a sublayout is not a Column |
2000
+
2001
+ **A preset (`kind: "element"`) MUST target a Column. There is no
2002
+ auto-wrap.** Pointing a preset at a section, row, layout root, new_page,
2003
+ or sublayout returns a `422 target_not_a_column` naming what the target
2004
+ actually is. The fix is always the same: scaffold a fresh
2005
+ `section → row → column` hull with `yootheme_builder_scaffold_section`
2006
+ first, then point `element_path` at ONE of the returned `column_paths`:
2007
+
2008
+ ```jsonc
2009
+ // A preset pointed at the layout root — REJECTED, no auto-wrap.
2010
+ yootheme_builder_library_insert({
2011
+ id: "8cf55920",
2012
+ kind: "element",
2013
+ target: { template_id: "home", element_path: "" }
2014
+ })
2015
+ // → 422 target_not_a_column: 'target at "/" is a "layout"'.
2016
+
2017
+ // Fix: scaffold a section->row->column hull, then target the new Column.
2018
+ yootheme_builder_scaffold_section({ template_id: "home", columns: 1, etag: "<etag>" })
2019
+ // Response includes column_paths: ["/children/4/children/0/children/0"]
2020
+
2021
+ yootheme_builder_library_insert({
2022
+ id: "8cf55920",
2023
+ kind: "element",
2024
+ target: { template_id: "home", element_path: "/children/4/children/0/children/0" },
2025
+ etag: "<fresh>"
2026
+ })
2027
+ ```
2028
+
2029
+ **Sublayout targets accept only rows.** A sublayout/fragment element
2030
+ (see "JSON-Pointer path grammar" and the Multi-Items material above)
2031
+ holds row-groups, not sections — `library_insert` handles the strip for
2032
+ you automatically when `target.sublayout` is used; you never call a
2033
+ separate strip step. Point `element_path` at the actual sublayout/
2034
+ fragment node itself, not its parent — pointing at the wrong level is a
2035
+ `422 target_not_a_sublayout`.
2036
+
2037
+ ### Adapting a Library insert is STATIC content editing, not dynamizing
2038
+
2039
+ **Every Library layout and preset is authored STATIC** — no source
2040
+ bindings, no `#parent` parent-source, no multiplication, no conditions.
2041
+ Adapting one after insert means editing copy/images/links on the fixed
2042
+ nodes it already has (`element_update_settings` on the specific
2043
+ headline/text/image `element_path`s), the same as editing any hand-built
2044
+ section.
2045
+
2046
+ **Making an inserted section DATA-DRIVEN (repeat over a Source's items —
2047
+ an archive-style listing) is a deliberate, separate follow-up call, not
2048
+ something insert does for you.** Use the existing dynamize workflow
2049
+ documented in "Dynamize an existing section" just above this one
2050
+ (`yootheme_builder_page_dynamize`) to re-target a Library-inserted
2051
+ section's leaf fields onto a Source, or bind a Multi-Items container
2052
+ directly with `yootheme_builder_element_bind_source` (Workflow 2) if the
2053
+ inserted item already has a grid/list container you want to re-point at
2054
+ live data instead of its demo items.
2055
+
2056
+ **Common pitfalls:**
2057
+
2058
+ - **Assuming the inserted layout will look like the demo screenshot.**
2059
+ It adopts the connected site's active Style — different fonts, colors,
2060
+ spacing than the catalog thumbnail. Tell the customer that up front.
2061
+ - **Hardcoding a color while "adapting" content.** Keep `style` /
2062
+ `text_color` / UIkit classes exactly as inserted; only touch copy,
2063
+ images, and links.
2064
+ - **Pointing a preset (`kind: "element"`) at anything but a Column** and
2065
+ expecting an auto-wrap. There isn't one — scaffold first.
2066
+ - **Choosing `images: "curated"` for a production customer site.** That's
2067
+ for quick demos; a real site wants `placeholder` + the customer's own
2068
+ photography.
2069
+ - **Stuffing placeholder copy into a field you have nothing for**, to
2070
+ defeat collapsing. Leave it empty; collapsing is the intended behavior.
2071
+ - **Expecting `library_insert` to make anything dynamic.** It composes
2072
+ STATIC content only — dynamizing is the separate `page_dynamize` /
2073
+ `element_bind_source` call.
2074
+
2075
+ **Worked example (browse → get → insert → adapt):**
2076
+
2077
+ ```jsonc
2078
+ // 1. Browse: filter by industry + page-role, layouts only (kind default).
2079
+ yootheme_builder_library_browse({ topic: "Health & Fitness", role: "Home", limit: 10 })
2080
+ // Response: { items: [{ id: "955bd3dc", name: "Home", kind: "layout",
2081
+ // meta: { topic: "Health & Fitness", type: "Company", role: "Home", website: "Makai", image: "<cdn-image-url>" },
2082
+ // thumbnail_url: "<cdn-thumbnail-url>" }], total: 64, next_cursor: null }
2083
+
2084
+ // 2. Fetch the chosen item's tree + section list.
2085
+ yootheme_builder_library_get({ id: "955bd3dc" })
2086
+ // Response: { id: "955bd3dc", kind: "layout", name: "Home", version: "5.0.9",
2087
+ // tree: { type: "layout", children: [ /* ... */ ] },
2088
+ // sections: [
2089
+ // { index: 0, name: "Hero", path: "/children/0", element_count: 12 },
2090
+ // { index: 1, name: "Services", path: "/children/1", element_count: 18 }
2091
+ // ],
2092
+ // images_manifest_count: 9 }
2093
+
2094
+ // 3. Insert only the "Hero" section onto an existing page's layout root.
2095
+ yootheme_builder_library_insert({
2096
+ id: "955bd3dc",
2097
+ target: { template_id: "home", element_path: "" },
2098
+ section_index: 0,
2099
+ images: "placeholder",
2100
+ etag: "<etag>"
2101
+ })
2102
+ // Response: { template_id: "home", inserted_path: "/children/3", element_count: 12,
2103
+ // images_mode: "placeholder", images_resolved: { downloaded: 0, placeholders: 6 },
2104
+ // new_etag: "<fresh>", frontend_url: null }
2105
+
2106
+ // 4. Adapt: swap the demo headline for the customer's own copy. Read the
2107
+ // inserted section back first (page_get_layout / element_get) to find the
2108
+ // real headline's element_path, then edit CONTENT only — style untouched.
2109
+ yootheme_builder_element_update_settings({
2110
+ template_id: "home",
2111
+ element_path: "/children/3/children/0/children/0",
2112
+ props: { content: "<customer's real headline>" },
2113
+ merge: true,
2114
+ etag: "<fresh>"
2115
+ })
2116
+ ```
2117
+
2118
+ **Success criterion:** the inserted section renders in the CONNECTED
2119
+ site's active Style (not the catalog demo's colors), every field the
2120
+ customer supplied content for shows that content, and every field they
2121
+ did not touch either shows a clean placeholder (`images: "placeholder"`)
2122
+ or collapses (empty unbound field) — never a broken image icon or a
2123
+ dangling demo string.
2124
+
2125
+ ## Look at what you built — the visual check (and two capture traps)
2126
+
2127
+ `render_preview` proves the page RENDERED and that a string reached the
2128
+ HTML. It does **not** prove it LOOKS right. Text can be present and
2129
+ invisible (muted grey on white); a section can be structurally perfect and
2130
+ still read as a dead band. **If your client has a browser tool, screenshot
2131
+ the page's `frontend_url` and LOOK at it before you call the work done.**
2132
+ That is not optional polish — it is the step that catches the failures
2133
+ every structural check reports as fine.
2134
+
2135
+ Two capture traps make a CORRECT page look broken. Both are artifacts of
2136
+ the SCREENSHOT, not of the layout — verified live against YOOtheme Pro:
2137
+
2138
+ **Trap 1 — scrollspy + lazy-load: a no-scroll full-page shot is mostly
2139
+ blank.** YOOtheme animates sections in with `uk-scrollspy`: elements sit at
2140
+ `opacity: 0` until scrolled into view, and images lazy-load. A full-page
2141
+ screenshot that never scrolls captures them at opacity 0 — you get a long
2142
+ white page with only the hero visible. **Fix: scroll the full document
2143
+ height in viewport-sized steps first (that fires scrollspy + lazy-load),
2144
+ then capture.**
2145
+
2146
+ **Trap 2 — `fixed` parallax bands can never be stitched.** A section using
2147
+ `image_effect: "fixed"` (CSS `background-attachment: fixed`) does not
2148
+ survive a stitched full-page capture at all — scrolling does NOT help. It
2149
+ comes out as a pure white void. **Fix: for any fixed-effect section, scroll
2150
+ it into view and capture the VIEWPORT only (a band shot).**
2151
+
2152
+ **Never judge — and never DELETE — a section based on a full-page
2153
+ thumbnail.** A real agent came within one call of deleting a page's best
2154
+ section (a full-bleed parallax band carrying the founder quote) because the
2155
+ full-page shot showed it as an empty white gap.
2156
+
2157
+ Once you can actually see the page, hunt the defects no tool reports:
2158
+
2159
+ - **Invisible / low-contrast text** — e.g. a caption left on muted grey
2160
+ over white. Fix with an explicit contrast prop (e.g. `meta_color:
2161
+ "emphasis"`), never by deleting the caption.
2162
+ - **Reading order** — a column that leads with a tall image buries its own
2163
+ headline, so the neighbouring column reads first and the story inverts.
2164
+ Fix with `element_move` so both columns start with their headline at the
2165
+ same height.
2166
+ - **No display moment** — every headline semantic and same-sized reads
2167
+ flat. Give exactly ONE hero headline a display `title_style` while
2168
+ KEEPING its `title_element` (size ≠ tag — see Typography; SEO untouched).
2169
+ - **Section rhythm** — two adjacent same-background sections blur together;
2170
+ alternate one (`style: "muted"`).
2171
+ - **Idiom coherence** — a section borrowed from a different demo can carry
2172
+ a foreign visual language (e.g. hairline SaaS metric cards inside warm
2173
+ editorial photography). Cut it or restyle it; do not leave the seam.
2174
+ - **Copy contradictions** — check the page's own facts against each other
2175
+ ("Fifteen years" under a "SINCE 2014" eyebrow) and drop claims repeated
2176
+ in three sections.
2177
+
2178
+ **If you have no browser tool, say so plainly.** Report that you verified
2179
+ the page structurally (`render_preview` + `expect`) but did NOT see it, and
2180
+ ask the customer to eyeball it. Never imply a visual check you did not
2181
+ perform.
2182
+
1103
2183
  ## Appendix: Tool Catalog (auto-generated)
1104
2184
 
1105
2185
  <!-- TOOL-CATALOG:BEGIN -->
1106
2186
 
1107
- **39 catalogued tools** plus the `yootheme_builder_advanced` (write) + `yootheme_builder_advanced_read` (read-only) gateways = **41 reachable via `tools/list`** (32 L1 + 2 L3 + 2 gateways = 36 advertised; the gateways route to 5 additional advanced tools, bringing the total to 41 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.
2187
+ **48 catalogued tools** plus the `yootheme_builder_advanced` (write) + `yootheme_builder_advanced_read` (read-only) gateways = **50 reachable via `tools/list`** (37 L1 + 2 L3 + 2 gateways = 41 advertised; the gateways route to 9 additional advanced tools, bringing the total to 50 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.
1108
2188
 
1109
2189
  | Tool | Kind | Input keys | Description |
1110
2190
  | --- | --- | --- | --- |
@@ -1122,24 +2202,33 @@ styles, the `#parent` bindings) survives untouched.
1122
2202
  | `yootheme_builder_element_type_get_schema` | read+idempotent | `element_type`, `site_id`, `type_name` | Fetch a type's prop schema before `element_add`/`element_update_settings`: an unknown prop key on a known schema is hard-rejected (not silently dropped); the error always lists `valid_prop_keys`, plus a `did_you_mean` hint when a close field exists. Returns `{name,type,label?}`. Use `element_type` (`type_name` is a deprecated alias). Operates on the default site unless site_id is provided. |
1123
2203
  | `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. Operates on the default site unless site_id is provided. |
1124
2204
  | `yootheme_builder_element_unbind_source` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Remove the source binding from an element. Clears `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. |
1125
- | `yootheme_builder_element_update_settings` | idempotent | `element_path`, `etag`, `merge`, `props`, `site_id`, `template_id` | 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. Operates on the default site unless site_id is provided. |
2205
+ | `yootheme_builder_element_update_settings` | idempotent | `element_path`, `etag`, `merge`, `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. |
1126
2206
  | `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current ETag (state revision) for the YOOtheme builder. Returns sha256+revision string used for optimistic locking on writes. Pass the returned value back as `etag` on any write tool (page_save, page_publish, element_add, element_update_settings, element_clone, element_move, element_delete). The server returns HTTP 412 if the ETag has changed since you read it. Keywords: get etag, current etag, state revision, optimistic lock, version stamp. Operates on the default site unless site_id is provided. |
1127
2207
  | `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. |
2208
+ | `yootheme_builder_image_import` | idempotent | `site_id`, `url` | Sideload a chosen photo (typically a `full_url` from yootheme_builder_image_search) into the site's own Media Library. Returns `{imported:true, local_url}`. Bind local_url into an element 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. |
2209
+ | `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. |
1128
2210
  | `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, …), current binding level (none\|container\|item), and a recommended_fix when the binding sits on the container instead of the child. 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). Operates on the default site unless site_id is provided. |
1129
2211
  | `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. |
1130
2212
  | `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 syntax, 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). 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. |
2213
+ | `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. |
2214
+ | `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. |
2215
+ | `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. |
2216
+ | `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. |
2217
+ | `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. |
2218
+ | `yootheme_builder_local_content_types` | read+idempotent | `fields`, `kind`, `max_chars`, `site_id` | Discover the site's NATIVE content landscape — post types/articles, taxonomies (categories/tags) and custom-field groups (ACF/com_fields) native to WordPress/Joomla (no API Mapper). Returns {content_types, taxonomies, field_groups, capabilities}; rows carry `bindable` + `source_name`. CALL FIRST to see WHAT exists, then local_content_read for records, then element_bind_source to wire the source. Narrow with `kind`, slim with `fields[]`. E.g. {kind:"content_types"} → [{name:"post",label:"Posts",bindable:true,source_name:"wp_posts"}]. Operates on the default site unless site_id is provided. |
1131
2219
  | `yootheme_builder_page_dynamize` | mutating | `conditions`, `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 (Played/Next/Later) dynamizes in ONE call via source_map ({match, source_name, leaf_map?}[]). Sub-query panels: key by "<subquery>.<leaf>". Returns page + `summary`. Requires page.title. Operates on the default site unless site_id is provided. |
1132
2220
  | `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` (numeric post/article id), or `url`. For dynamize/bind prefer `mode:"bindings_only"` (binding skeleton, no style props). Default `mode:"full"` returns nested `{layout, etag}`; `flat:true` gives a depth-first `{elements:[...]}` array (with `fields[]`). A repeated unchanged read auto-skips (`force:true` re-fetches). `max_chars` bounds the text; a clipped read returns `next_cursor` (pass back as `cursor`) to page the rest. Operates on the default site unless site_id is provided. |
1133
2221
  | `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. |
1134
- | `yootheme_builder_page_publish` | idempotent | `etag`, `site_id`, `template_id` | Publish a template — persist state, flush YT + WP caches, snapshot the published-state ETag. ETag optional — when provided, 412 on conflict; when omitted, last-write-wins. Recommended for collaborative edits. Operates on the default site unless site_id is provided. |
1135
- | `yootheme_builder_page_save` | idempotent | `dry_run`, `etag`, `layout`, `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` — NOT the full template node page_get_layout returns (rejected 422, would blank the page). Validated + ETag-locked; on reject nothing persists. An unrenderable layout is refused PRE-WRITE (422). `dry_run:true` preflights; the success response carries `previous_layout` for 1-call rollback. Omit `layout` to re-run transforms only. ETag optional (412 on conflict; else last-write-wins). Operates on the default site unless site_id is provided. |
2222
+ | `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. ETag optional — when provided, 412 on conflict; when omitted, last-write-wins. Recommended for collaborative edits. Operates on the default site unless site_id is provided. |
2223
+ | `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` — NOT the full template node page_get_layout returns (rejected 422, would blank the page). Validated + ETag-locked; on reject nothing persists. An unrenderable layout is refused PRE-WRITE (422). `dry_run:true` preflights; the success response carries `previous_layout` for 1-call rollback. Omit `layout` to re-run transforms only. ETag optional (412 on conflict; else last-write-wins). Operates on the default site unless site_id is provided. |
2224
+ | `yootheme_builder_pages_audit` | read+idempotent | `checks`, `max_chars`, `site_id`, `stale_after_months`, `thin_content_threshold`, `types` | One-call site-wide SEO/structure/content/links audit — severity-sorted findings with machine-executable fix_hints (missing/multiple h1, empty/thin pages, missing image alt, duplicate layouts, title collisions, non-pretty URLs, stale content, dead anchors). Filter with `types`/`checks`. Disclaimer: SEO-plugin meta (title/description/OG) NOT covered. Drill in via template_summary; fix one page at a time via each finding's fix_hint. Operates on the default site unless site_id is provided. |
1136
2225
  | `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. |
1137
- | `yootheme_builder_pages_delete` | idempotent | `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. |
1138
- | `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 (template_id, label, type, element count, frontend_url, builder_url 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. |
1139
- | `yootheme_builder_pages_resolve` | read+idempotent | `site_id`, `url` | Resolve a public page/article OR archive URL to the template_id that renders it — paste a URL, get a layout-addressable id in one call. A singular page resolves to its per-page layout; a CPT archive (/faq/), taxonomy/category/tag term, or author archive resolves to the governing THEME TEMPLATE. Returns {template_id, post_id (null for theme templates), type, frontend_url, context (matched YT type), resolved_via (per_page\|theme_template)}. Only home/search/date 404 (use pages_list). page_get_layout/page_get_schema also accept `url`. Operates on the default site unless site_id is provided. |
2226
+ | `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. |
2227
+ | `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. |
2228
+ | `yootheme_builder_pages_resolve` | read+idempotent | `site_id`, `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. Returns {template_id, post_id, type, frontend_url, context, resolved_via}. Operates on the default site unless site_id is provided. |
1140
2229
  | `yootheme_builder_rename_source` | idempotent | `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 (not a whole-layout page_save) — 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}. ETag req. Operates on the default site unless site_id is provided. |
1141
2230
  | `yootheme_builder_render_preview` | read+idempotent | `expect`, `site_id`, `template_id`, `url` | Render-check a saved template over the authenticated channel — does it actually render, with how many items, or is it blank? Address by `template_id` (from pages_list) OR `url` (resolved for you). Returns {rendered, item_count, empty, error?, cache_purge_hint}; with `expect`, also {matched, snippet}. Use AFTER a page_save / element_bind_source to catch a silently-empty live page (a bad binding "saves" then renders blank); the swallowed YOOtheme render error is surfaced in `error`. Read-only — renders without persisting. Operates on the default site unless site_id is provided. |
1142
- | `yootheme_builder_scaffold_section` | mutating | `columns`, `etag`, `site_id`, `template_id` | Create a VALID section → row → column hull in ONE atomic call and return the column JSON-Pointer path(s) to drop content into. This is the EASIEST correct way to start a new layout block: YOOtheme content must live in section → row → column, and adding content anywhere else (root, bare section, bare row) is rejected. Pass `columns` (1-12, default 1) for a multi-column row. Requires ETag. Operates on the default site unless site_id is provided. |
2231
+ | `yootheme_builder_scaffold_section` | mutating | `column_widths`, `columns`, `etag`, `site_id`, `template_id` | Create a VALID section → row → column hull in ONE atomic call and return the column JSON-Pointer path(s) to drop content into. This is the EASIEST correct way to start a new layout block: YOOtheme content must live in section → row → column, and adding content anywhere else (root, bare section, bare row) is rejected. Pass `columns` (1-12, default 1) for a multi-column row; columns are auto-distributed equal-width so they render side-by-side, or pass `column_widths` (one token per column) for an asymmetric row. Requires ETag. Operates on the default site unless site_id is provided. |
1143
2232
  | `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. (site_id is accepted for schema-uniformity but ignored by this tool.) |
1144
2233
  | `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. |
1145
2234
  | `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). Each argument may carry `enum_values` (allowed values, e.g. ["asc","desc"]); pick from those, don't guess. CALL BEFORE binding on an archive/category template — turns a ~25-call code-dive into one. 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. |