@wootsup/yt-builder-mcp 1.7.2 → 1.9.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 (99) hide show
  1. package/README.md +8 -8
  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/setup-cli.d.ts +8 -0
  24. package/dist/setup-cli.d.ts.map +1 -1
  25. package/dist/setup-cli.js +58 -6
  26. package/dist/setup-cli.js.map +1 -1
  27. package/dist/setup-npx-spec.d.ts +1 -1
  28. package/dist/setup-npx-spec.js +1 -1
  29. package/dist/setup-prompts.d.ts +11 -0
  30. package/dist/setup-prompts.d.ts.map +1 -1
  31. package/dist/setup-prompts.js +28 -0
  32. package/dist/setup-prompts.js.map +1 -1
  33. package/dist/setup-wizard-defaults.d.ts.map +1 -1
  34. package/dist/setup-wizard-defaults.js +40 -9
  35. package/dist/setup-wizard-defaults.js.map +1 -1
  36. package/dist/setup-wizard-handshake.d.ts +5 -4
  37. package/dist/setup-wizard-handshake.d.ts.map +1 -1
  38. package/dist/setup-wizard-handshake.js +28 -24
  39. package/dist/setup-wizard-handshake.js.map +1 -1
  40. package/dist/setup-wizard-types.d.ts +48 -9
  41. package/dist/setup-wizard-types.d.ts.map +1 -1
  42. package/dist/setup-wizard.d.ts.map +1 -1
  43. package/dist/setup-wizard.js +62 -10
  44. package/dist/setup-wizard.js.map +1 -1
  45. package/dist/sites/env-bridge.d.ts +19 -0
  46. package/dist/sites/env-bridge.d.ts.map +1 -1
  47. package/dist/sites/env-bridge.js +29 -15
  48. package/dist/sites/env-bridge.js.map +1 -1
  49. package/dist/sites/store.d.ts +1 -1
  50. package/dist/sites/store.js +1 -1
  51. package/dist/skill-loader.d.ts +2 -2
  52. package/dist/skill-loader.js +2 -2
  53. package/dist/tools/elements/builders.d.ts.map +1 -1
  54. package/dist/tools/elements/builders.js +21 -2
  55. package/dist/tools/elements/builders.js.map +1 -1
  56. package/dist/tools/elements/handlers-write.d.ts +2 -0
  57. package/dist/tools/elements/handlers-write.d.ts.map +1 -1
  58. package/dist/tools/elements/handlers-write.js +16 -4
  59. package/dist/tools/elements/handlers-write.js.map +1 -1
  60. package/dist/tools/elements/handlers.d.ts +1 -0
  61. package/dist/tools/elements/handlers.d.ts.map +1 -1
  62. package/dist/tools/elements/handlers.js +14 -0
  63. package/dist/tools/elements/handlers.js.map +1 -1
  64. package/dist/tools/format/pages-format.d.ts.map +1 -1
  65. package/dist/tools/format/pages-format.js +21 -0
  66. package/dist/tools/format/pages-format.js.map +1 -1
  67. package/dist/tools/index.d.ts.map +1 -1
  68. package/dist/tools/index.js +8 -0
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/tools/library.d.ts +279 -0
  71. package/dist/tools/library.d.ts.map +1 -0
  72. package/dist/tools/library.js +1060 -0
  73. package/dist/tools/library.js.map +1 -0
  74. package/dist/tools/local-content/builders.d.ts +19 -0
  75. package/dist/tools/local-content/builders.d.ts.map +1 -0
  76. package/dist/tools/local-content/builders.js +127 -0
  77. package/dist/tools/local-content/builders.js.map +1 -0
  78. package/dist/tools/local-content/handlers.d.ts +54 -0
  79. package/dist/tools/local-content/handlers.d.ts.map +1 -0
  80. package/dist/tools/local-content/handlers.js +287 -0
  81. package/dist/tools/local-content/handlers.js.map +1 -0
  82. package/dist/tools/local-content/index.d.ts +17 -0
  83. package/dist/tools/local-content/index.d.ts.map +1 -0
  84. package/dist/tools/local-content/index.js +16 -0
  85. package/dist/tools/local-content/index.js.map +1 -0
  86. package/dist/tools/local-content/schemas.d.ts +82 -0
  87. package/dist/tools/local-content/schemas.d.ts.map +1 -0
  88. package/dist/tools/local-content/schemas.js +89 -0
  89. package/dist/tools/local-content/schemas.js.map +1 -0
  90. package/dist/tools/pages/builders.d.ts.map +1 -1
  91. package/dist/tools/pages/builders.js +16 -10
  92. package/dist/tools/pages/builders.js.map +1 -1
  93. package/dist/tools/sources/handlers-bind.d.ts +2 -0
  94. package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
  95. package/dist/tools/sources/handlers-bind.js +18 -0
  96. package/dist/tools/sources/handlers-bind.js.map +1 -1
  97. package/manifest.json +4 -3
  98. package/package.json +4 -3
  99. package/skills/yt-builder-mcp/SKILL.md +998 -78
@@ -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 47 typed, scoped,
16
+ idempotent tools (49 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 8 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 8; 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) 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.
@@ -32,12 +33,12 @@ Roo Code, Claude Code, Codex CLI or any other MCP-aware AI client. Setup is
32
33
  Joomla 5/6 host plugin:
33
34
 
34
35
  1. The user installs the host plugin for their CMS:
35
- - **WordPress**. Install the `yt-builder-mcp` plugin (downloadable from the
36
- [GitHub repository](https://github.com/wootsup/yt-builder-mcp)) and generate
37
- a Bearer key in **wp-admin → Tools → "YT Builder MCP" → Bearer Keys**.
38
- - **Joomla 5/6**. Install the `yt-builder-mcp` package (downloadable from the
39
- [GitHub repository](https://github.com/wootsup/yt-builder-mcp)) and generate
40
- a Bearer key in **Components → YT Builder MCP → Bearer Keys**. The package
36
+ - **WordPress**. Install the `yt-builder-mcp` plugin (download + install guide
37
+ at [wootsup.com/docs](https://wootsup.com/docs/mcp/yootheme-wordpress/installation))
38
+ and generate a Bearer key in **wp-admin → Tools → "YT Builder MCP" → Bearer Keys**.
39
+ - **Joomla 5/6**. Install the `yt-builder-mcp` package (download + install guide
40
+ at [wootsup.com/docs](https://wootsup.com/docs/mcp/yootheme-joomla/installation))
41
+ and generate a Bearer key in **Components → YT Builder MCP → Bearer Keys**. The package
41
42
  installs three sub-extensions (system plugin, webservices plugin, component).
42
43
  2. The user runs `npx -y @wootsup/yt-builder-mcp setup` once; the wizard probes
43
44
  the host plugin, validates the key, and writes the MCP server entry into every
@@ -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 8 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,28 @@ 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
+ - **8 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`, and the two Local
131
+ Content Read tools `local_content_types` / `local_content_read`).
111
132
  Reachable through one gateway tool:
112
133
  `yootheme_builder_advanced({ tool: "<name>", arguments: { ... } })`. For
113
134
  example, to inspect a panel/grid_item `< SUBLAYOUT`:
@@ -116,18 +137,29 @@ The server exposes:
116
137
  AND-combined `type`/`name_matches`/`has_binding`/`max_depth`/`under_section`):
117
138
  `yootheme_builder_advanced({ tool: "yootheme_builder_layout_query", arguments: { template_id, type: "headline", has_binding: false, under_section: "Hero" } })`
118
139
  (it's read-only, so the read gateway `yootheme_builder_advanced_read({ tool: "yootheme_builder_layout_query", … })` works too).
140
+ And to save a page/subtree INTO "My Layouts" (Task 7 — the reverse of
141
+ `library_insert`; omit `element_path` to save the whole page):
142
+ `yootheme_builder_advanced({ tool: "yootheme_builder_library_save_as", arguments: { source: { template_id, element_path }, name: "Hero Section" } })`.
143
+ It flags `invalid_source_possible: true` when the saved subtree carries
144
+ bound dynamic sources that may not exist wherever it is later reloaded —
145
+ re-bind after inserting it elsewhere.
119
146
 
120
147
  The core write/refactor + render-verify tools the Glöckle HANDOFF-4/5 ask for
121
148
  are now **L1 — call them directly by name**, not through a gateway:
122
149
  - To render-check a saved page over the authenticated channel (F5+N5 — does
123
150
  it render, with how many items, or is it silently blank? where an external
124
151
  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`).
152
+ `yootheme_builder_render_preview({ template_id })` directly or address it by
153
+ a public URL with `yootheme_builder_render_preview({ url })` (the URL is
154
+ resolved to its template for you, exactly like `page_get_layout`), so if you
155
+ only hold a resolved category/archive URL you can render-check it in ONE call
156
+ without first converting it to a template_id. It takes no post/term target: it
157
+ renders YOOtheme's implicit fallback context, so a source-bound template comes
158
+ back non-empty. Treat a non-empty render as the success signal, not "blank
159
+ despite a binding". `item_count` counts the TOP-LEVEL repeat-source rows only
160
+ (a static grid/list nested in a repeated card no longer inflates it). Pass
161
+ `expect: "<substring>"` to confirm a specific edit reached the HTML in one call
162
+ (the response adds `matched` + `snippet`).
131
163
  - To surgically SET a precomposed `source` + the native item-level
132
164
  `source_extended.props` carrier on one element at a pointer, ETag-locked,
133
165
  leaving the rest of the layout byte-unchanged (F2 — the one-call source edit
@@ -152,13 +184,20 @@ The server exposes:
152
184
  - **2 gateway tools**: `yootheme_builder_advanced` (write-capable; can route
153
185
  into delete/unbind, so hosts gate it) and `yootheme_builder_advanced_read`
154
186
  (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:
187
+ inspect_sublayout, layout_query, local_content_types, local_content_read
188
+ and is read-only-annotated so hosts auto-allow it). Prefer the read gateway
189
+ for pure inspection:
157
190
  `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
191
+ Discover the site's NATIVE content the same way — the landscape then the records:
192
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_local_content_types" })`
193
+ then `yootheme_builder_advanced_read({ tool: "yootheme_builder_local_content_read", arguments: { kind: "terms", taxonomy: "category" } })`.
194
+ (The write gateway routes them too, if you prefer one entry:
195
+ `yootheme_builder_advanced({ tool: "yootheme_builder_local_content_types" })` and
196
+ `yootheme_builder_advanced({ tool: "yootheme_builder_local_content_read", arguments: { kind: "entities", type: "post" } })`.)
197
+
198
+ `tools/list` therefore advertises 41 names (37 + 2 + 2). That's 37 L1
199
+ essentials + 2 L3 direct + 2 gateways. The total callable surface is 49
200
+ (41 advertised + 8 advanced reachable through the gateways). If the AI
162
201
  client reports "tool not found", you are almost certainly calling an
163
202
  advanced tool by its raw name. Wrap it in
164
203
  `yootheme_builder_advanced({ tool, arguments })` (or the read gateway for
@@ -183,6 +222,50 @@ guess where the agent is pointing:
183
222
  Treat `frontend_url: null` as "host plugin could not resolve a public URL for
184
223
  this template". Surface that honestly rather than fabricating one.
185
224
 
225
+ ## Resolve a URL back to the template that renders it
226
+
227
+ The two sections above go template → URL (forward). To go the OTHER way — the
228
+ user pastes a public URL and asks *"which layout renders this?"* — you have two
229
+ complementary tools.
230
+
231
+ **`yootheme_builder_pages_resolve({ url })` — the direct reverse lookup.** Paste
232
+ any public frontend URL and it returns
233
+ `{ template_id, post_id, type, frontend_url, context, resolved_via }` (or a
234
+ `url_not_resolvable` when it genuinely maps to no template — it degrades to an
235
+ honest null rather than guessing a wrong one). It handles **SEF / pretty
236
+ permalinks on BOTH WordPress and Joomla** (not just raw `?option=…&id=…` query
237
+ URLs), including deep sub-category paths like `/matches/first-men-s-team`. For a
238
+ **category or term URL it returns the SPECIFICALLY-ASSIGNED template** — the one
239
+ whose YOOtheme `query.catid` (Joomla) / `query.terms` (WordPress) binds THAT
240
+ category/term, honoring **include-child-categories** (a template assigned to a
241
+ parent category with child-inclusion covers its subcategories; the nearest
242
+ specific assignment wins). Only when nothing is specifically assigned does it
243
+ fall back to the governing generic theme template. Then feed the returned
244
+ `template_id` straight into `page_get_layout` / `element_list` / the write tools.
245
+
246
+ **`serves_context_kind` + `serves_context_ids` on `pages_list` rows — resolve it
247
+ yourself, zero extra calls.** Every `pages_list` row also carries these two Layer-C
248
+ fields so you can map a URL to its template WITHOUT a `pages_resolve` round-trip:
249
+
250
+ - **`serves_context_kind`** is the normalized, cross-platform context the template
251
+ serves — one of `single-post`, `single-page`, `category`, `tag`, `author`,
252
+ `search`, `post-archive`, `error-404`, `internal`, `unknown`. A WordPress row and
253
+ a Joomla row that serve the same logical context report the SAME kind (WP
254
+ `taxonomy-category` and Joomla `com_content.category` both → `category`).
255
+ - **`serves_context_ids`** is the SPECIFIC category/term ids that a category
256
+ template is assigned to (Joomla `query.catid`, WordPress `query.terms`). It is
257
+ **empty** for a generic archive or a generic category template that binds no
258
+ specific ids — that emptiness is honest, not a bug.
259
+
260
+ Matching workflow: derive the URL's context (a `/category/<id-or-slug>` →
261
+ `category` + its id; an article/post → `single-post`; etc.), then pick the
262
+ `pages_list` row whose `serves_context_kind` matches and whose
263
+ `serves_context_ids` contains that id — e.g. URL `/matches` (category 11) matches
264
+ the row `{ label: "Match Index", serves_context_kind: "category",
265
+ serves_context_ids: [11] }`. Use `pages_resolve` when you just want the answer;
266
+ use the `serves_context_*` columns when you already hold `pages_list` and want to
267
+ match locally.
268
+
186
269
  ## Scopes (Bearer key permissions)
187
270
 
188
271
  Every Bearer key has a scope, set at key creation time:
@@ -226,7 +309,7 @@ sites (WordPress and Joomla mixed). Without multi-site support you would need
226
309
  one MCP install per site, one set of env vars per site, and one AI-client
227
310
  restart per site you want to talk to. With multi-site:
228
311
 
229
- - One DXT install in Claude Desktop, one entry in your AI client config.
312
+ - One MCPB install in Claude Desktop, one entry in your AI client config.
230
313
  - One conversation can edit elements on `acme.com` and `beta.io` back-to-back.
231
314
  - Each site keeps its own Bearer key, platform, label, and 1Password reference.
232
315
  - Adding a new client site does not require a new MCP install.
@@ -351,7 +434,7 @@ one conversation.
351
434
  2. For each site, the agent runs the same sequence with `site_id` set:
352
435
  - `yootheme_builder_pages_list({ site_id: "wp-acme", fields: ["id", "label"] })`
353
436
  - `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>" })`
437
+ - `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
438
  - `yootheme_builder_page_save({ site_id: "wp-acme", template_id: "home", etag: "<fresh>" })`
356
439
  - `yootheme_builder_page_publish({ site_id: "wp-acme", template_id: "home", etag: "<fresh>" })`
357
440
  3. The customer sees a stream of replies, each prefixed with the matching
@@ -364,6 +447,33 @@ for a focused diagnosis without touching the others.
364
447
 
365
448
  ---
366
449
 
450
+ ## Pages vs theme templates (know what you are editing)
451
+
452
+ `pages_list` returns TWO different kinds of editable layout, and every tool
453
+ takes a `template_id` that addresses either. Know which one you have before
454
+ you write — they have very different blast radius.
455
+
456
+ | Kind | `id` shape | `type` value(s) | What it is |
457
+ |------|-----------|-----------------|------------|
458
+ | **Page** | `page:<n>` (WP) / `article:<n>` (Joomla) | `page` | ONE individual page/article. Editing it changes only that page. Stored in the post/article content. |
459
+ | **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. |
460
+
461
+ **The safety rule: editing a theme template changes the layout for ALL
462
+ matching content, not one page.** If the user says "restyle the blog post
463
+ template", `pages_list` shows it as e.g. `{ id: "I99YS8Ii", label: "Post",
464
+ type: "single-post" }` — editing that `template_id` restyles EVERY blog post
465
+ on the site at once. That is often exactly what's wanted (that's what theme
466
+ templates are for), but confirm the intent — do not edit a `single-post` /
467
+ `taxonomy-*` / `archive-*` template when the user meant one specific page.
468
+ `template_summary` also returns `is_theme_template` to disambiguate cheaply.
469
+
470
+ - To edit ONE page, target a `page:<n>` / `article:<n>` id.
471
+ - To edit a reusable template (the design of all posts, all category
472
+ archives, the 404, …), target the bare id and understand the reach.
473
+ - `pages_create` creates a **page/article** (an individual layout). Theme
474
+ templates are defined in the theme itself and are not created via this MCP;
475
+ you edit the existing ones `pages_list` reports.
476
+
367
477
  ## Workflow 1: Build a hero section
368
478
 
369
479
  **Goal:** Add a fresh hero section (heading + sub-heading + CTA button)
@@ -379,24 +489,48 @@ to an existing page.
379
489
  named a specific page, match on `label` (exact then fuzzy).
380
490
  3. `yootheme_builder_get_etag()`: fetch the current top-level
381
491
  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>" }`.
492
+ 4. `yootheme_builder_element_add({ template_id: "<id>", parent_path: "", element_type: "section", props: { style: "primary" }, etag: "<etag>" })`:
493
+ append a new section at the template root (`parent_path: ""`). The
494
+ section background is set with `style` (enum: `default` / `muted` /
495
+ `primary` / `secondary`) — there is NO `background` key (it hard-rejects
496
+ as an unknown prop). Always call `element_type_get_schema("section")`
497
+ first to confirm the real prop keys.
498
+ Returns `{ rel_path: "/children/0/children/N", etag: "<fresh>" }`.
385
499
  5. `yootheme_builder_element_add({ template_id, parent_path: "<section-path>", element_type: "row", etag: "<fresh-etag>" })`:
386
500
  add a row inside the section. Use the etag returned by the
387
501
  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.
502
+ 6. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "headline", props: { content: "<h1 text>", title_style: "h1" }, etag })`:
503
+ add a headline. `title_style: "h1"` uses the semantic type scale (the
504
+ tasteful default). See "Typography & heading styles" below before
505
+ reaching for a `heading-*` display size.
390
506
  7. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "text", props: { content: "<sub text>" }, etag })`:
391
507
  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.
508
+ 8. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "button", etag })`
509
+ then add a `button_item` CHILD for the actual label/link/style:
510
+ `yootheme_builder_element_add({ template_id, parent_path: "<button-path>", element_type: "button_item", props: { content: "<cta>", link: "<url>", button_style: "primary" }, etag })`.
511
+ The `button` element is a CONTAINER — its label, link and style live on
512
+ a `button_item` child, NOT on the button's own props. Adding a lone
513
+ `button` with `{content, link}` renders an empty button. (Same
514
+ container→item pattern as `grid`→`grid_item`, `list`→`list_item`.)
394
515
  9. `yootheme_builder_page_save({ template_id, etag })`:
395
516
  persist the working copy (visible in YOOtheme Customizer preview).
396
517
  `page_save` is a first-class L1 tool; call it directly.
397
518
  10. `yootheme_builder_page_publish({ template_id, etag })`:
398
519
  make the changes live on the front-end. Also a first-class L1 tool.
399
520
 
521
+ > **Building a whole page? Prefer ONE atomic `page_save` over ~30 incremental
522
+ > writes.** The step-by-step `element_add` sequence above is clearest for
523
+ > learning and for small edits, but each write is ETag-locked, so building a
524
+ > full multi-section page as 20-30 sequential writes invites `412` conflicts
525
+ > (especially when other agents edit the same site — the builder ETag is a
526
+ > single shared option). For a full page, assemble the complete
527
+ > `{type:"layout",children:[...]}` tree in memory and persist it in ONE call:
528
+ > `yootheme_builder_page_save({ template_id, layout: <full-tree> })` (ETag
529
+ > optional → last-write-wins, scoped to this page). `dry_run:true` preflights
530
+ > it first. A fast, robust pattern: `scaffold_section` one block, read it
531
+ > back to learn the exact node shape, then build the rest of the tree from
532
+ > that shape and `page_save` it whole.
533
+
400
534
  **Common pitfalls:**
401
535
 
402
536
  - **Wrong parameter names.** Every tool uses snake_case. Use
@@ -430,10 +564,10 @@ yootheme_builder_element_add({
430
564
  template_id: "home",
431
565
  parent_path: "",
432
566
  element_type: "section",
433
- props: { background: "primary" },
567
+ props: { style: "primary" }, // section background = `style`, NOT `background`
434
568
  etag: "abc123" // from yootheme_builder_get_etag
435
569
  })
436
- // Response: { path: "/0/children/3", etag: "def456" }
570
+ // Response: { rel_path: "/children/0/children/3", etag: "def456" }
437
571
  // → next call uses etag "def456"
438
572
 
439
573
  // Step 9. page_save is a first-class L1 tool; call it directly.
@@ -453,6 +587,200 @@ section as the last child of the template root.
453
587
 
454
588
  ---
455
589
 
590
+ ## Multi-column layouts (columns that sit side-by-side)
591
+
592
+ **The single most common layout mistake: a `row` with several `column`
593
+ children that reads as gaps / a broken column in the builder — or renders
594
+ STACKED (full-width, one under the other) on the front-end — instead of a
595
+ clean set of side-by-side columns.** This is a silent, layout-only defect:
596
+ the write succeeds (valid values), so you only catch it by looking at the
597
+ builder's left panel or the rendered page, never from a successful save.
598
+
599
+ **How a real YOOtheme layout defines columns — set BOTH, exactly like every
600
+ shipped demo layout does:**
601
+
602
+ 1. **`row.props.layout` — the grid string. This is the primary, load-bearing
603
+ piece.** It is the value the builder's *"Select a grid layout"* picker
604
+ writes: a comma-separated width per column, e.g. `"1-2,1-2"` (Halves),
605
+ `"1-3,1-3,1-3"` (Thirds), `"2-3,1-3"` (Thirds 2-1), `"1-4,1-4,1-4,1-4"`
606
+ (Quarters), `"3-5,2-5"` (a 3/5 + 2/5 hero split). The builder reads
607
+ `row.layout` to know a row HAS N columns. **A multi-column row with no
608
+ `row.layout` is read by the builder's Layout picker as "Whole" (one
609
+ full-width column) and painted collapsed with a big empty gap where the
610
+ other columns belong** — the "lauter Lücken im Layout" a customer sees in
611
+ the builder. `layout` is a `row` prop (NOT a column prop).
612
+
613
+ 2. **Per-column responsive `width_*` — the breakpoint refinement.** On each
614
+ `column`, set `width_default: "1-1"` (stack on phone) and
615
+ `width_medium: "<token>"` (go side-by-side from ≥960px). This is the
616
+ factory responsive default: readable single-column on a phone, multi-column
617
+ on tablet/desktop. The full breakpoint set (each maps to
618
+ `uk-width-{value}@breakpoint`):
619
+
620
+ | Prop | Breakpoint |
621
+ |------|------------|
622
+ | `width_default` | all sizes (phone-up) |
623
+ | `width_small` | ≥ 640px |
624
+ | `width_medium` | ≥ 960px |
625
+ | `width_large` | ≥ 1200px |
626
+ | `width_xlarge` | ≥ 1600px |
627
+
628
+ **Valid width tokens** (the same values used in the grid string; discover
629
+ them on the `column` schema via `element_type_get_schema` — they carry an
630
+ `enum`, so an invalid token is hard-rejected with a `422 invalid_enum_value`):
631
+ `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
632
+ `auto expand small medium large xlarge`. Match the row's grid string to the
633
+ columns' `width_medium` tokens (grid `"2-3,1-3"` ↔ column 1 `width_medium:
634
+ "2-3"`, column 2 `width_medium: "1-3"`).
635
+
636
+ **Single-column rows are different: NO `row.layout`, NO column width.** A
637
+ factory intro / testimonial / CTA is a full-width column (leave the column
638
+ width-less) inside a row with no grid string, and the content is centred by
639
+ setting `text_align: "center"` on the child `headline` / `text` elements —
640
+ never by narrowing the column. Narrowing a single column WITHOUT a matching
641
+ `row.layout` is exactly what leaves a gap in the builder.
642
+
643
+ **Easiest correct way — `scaffold_section` builds the whole factory-shaped
644
+ hull for you (row grid string + per-column responsive widths in one call):**
645
+
646
+ - `scaffold_section({ template_id, columns: 3, etag })` → a section whose row
647
+ carries `layout: "1-3,1-3,1-3"` and whose 3 columns each carry
648
+ `width_default: "1-1"` + `width_medium: "1-3"` → clean, gap-free, side-by-side
649
+ from ≥960px. `columns: N` distributes equal widths (`1-N` for N ≤ 6,
650
+ `expand` for N > 6).
651
+ - For an ASYMMETRIC row pass explicit tokens, one per column:
652
+ `scaffold_section({ template_id, columns: 2, column_widths: ["2-3", "1-3"], etag })`
653
+ → a row with `layout: "2-3,1-3"` and the matching column widths. A
654
+ wrong-length array or a bad token returns a friendly `400` before the write.
655
+ - `scaffold_section({ template_id, columns: 1, etag })` → a single full-width
656
+ column with no `row.layout` and no column width (the factory single-column
657
+ shape).
658
+
659
+ `scaffold_section` returns the column JSON-Pointer paths — drop your content
660
+ into them with `element_add({ parent_path: "<column-path>", ... })`. If you
661
+ build columns by hand with `element_add` instead, remember to set BOTH
662
+ `row.props.layout` (the grid string) AND each column's `width_default` /
663
+ `width_medium`, or the row reads as "Whole" and shows gaps in the builder.
664
+
665
+ **Success criterion:** in the builder's left panel the row shows N distinct
666
+ columns (not one column + an empty gap), and after publish the columns sit
667
+ side-by-side at desktop width. Measure with `getBoundingClientRect` (columns
668
+ share the same `top`, differ in `left`) — a fullPage screenshot hides both
669
+ the gap and the stacking.
670
+
671
+ ---
672
+
673
+ ## Typography & heading styles (restraint is the professional signal)
674
+
675
+ The `title_style` prop (on `headline`, and `title_style`/`content_style`/
676
+ `meta_style` on `grid`) mixes TWO different scales that look alike but mean
677
+ opposite things. Choosing the wrong one is the most common reason an
678
+ AI-built page looks amateurish — a wall of oversized text with no hierarchy.
679
+
680
+ - **`h1`–`h6` = the SEMANTIC type scale.** They follow the theme's designed
681
+ type ramp — tasteful, theme-consistent, restrained. **This is the default
682
+ a real YOOtheme designer uses.** A hero headline is typically `h1`; a
683
+ section title is `h2`; a card/feature title is `h3`. Reach for these
684
+ FIRST.
685
+ - **`heading-small` … `heading-3xlarge` = DISPLAY / billboard type**
686
+ (≈2.6rem up to ~8rem; on the front-end `heading-2xlarge` can render
687
+ ~150–180px). They are decoupled from semantics — pure visual impact.
688
+ `heading-2xlarge` / `heading-3xlarge` are for a SINGLE deliberate hero
689
+ statement, used ONCE per page, if at all.
690
+ - **`text-lead` / `text-large` / `text-meta` / `text-small`** are body-text
691
+ emphasis styles, not headings (e.g. an eyebrow label = `text-meta`, a lead
692
+ paragraph = `text-lead`).
693
+
694
+ **The principle: hierarchy comes from CONTRAST, not from making everything
695
+ large.** If every section title is `heading-2xlarge`, nothing stands out and
696
+ the page reads as a shouting match. Display sizes are a spice, not a staple:
697
+ give ONE hero moment the big display size and let everything else step down
698
+ to the semantic scale (`h2`/`h3`). Concretely, for a typical landing page:
699
+
700
+ | Element | Good default |
701
+ |---------|-------------|
702
+ | Eyebrow / overline label | `text-meta` |
703
+ | Hero headline | `h1` (or at most `heading-medium`/`heading-large` for extra punch) |
704
+ | Section title | `h2` |
705
+ | Card / feature / metric title | `h3` (a big metric number can take `heading-medium`, not `2xlarge`) |
706
+ | Lead paragraph | `text-lead` |
707
+
708
+ Do NOT repeat a `heading-*` display size across multiple sections. When in
709
+ doubt, use the semantic level (`h2`/`h3`) — it is always the safer,
710
+ more professional choice.
711
+
712
+ ### `title_style` (visual size) vs `title_element` (HTML tag / SEO)
713
+
714
+ These are **two independent axes** on a `headline`, and confusing them
715
+ produces SEO-fatal pages:
716
+
717
+ - **`title_style`** = how big the title *looks* (the section above).
718
+ - **`title_element`** = the actual HTML tag it renders as: `h1`–`h6`, or
719
+ `div` for non-heading display text. This is the **semantic / SEO** axis. A
720
+ `div` styled `heading-2xlarge` looks huge but is *not* a heading; an `h2`
721
+ styled `text-small` is still a level-2 heading.
722
+
723
+ **`title_element` DEFAULTS to `h1` when unset** — so if you leave it blank on
724
+ every headline, every headline becomes an `<h1>`, which is SEO-fatal (a page
725
+ must have **exactly one** `<h1>`). Set it explicitly on every headline:
726
+
727
+ | Headline role | `title_element` |
728
+ |---------------|-----------------|
729
+ | Hero headline (once per page) | `h1` |
730
+ | Section title | `h2` |
731
+ | Card / feature title | `h3` |
732
+ | Metric number / stat / other non-heading display text | `div` |
733
+
734
+ `title_style` and `title_element` are set **independently** — e.g. a hero is
735
+ `title_element: "h1"` with `title_style: "h1"` (or a `heading-*` display size
736
+ for punch); a big stat number is `title_element: "div"` with
737
+ `title_style: "heading-medium"`.
738
+
739
+ ### Headline `content` is plain inline text — never wrapped in `<p>`
740
+
741
+ A `headline` renders its `content` **inside** the heading tag, so wrapping it
742
+ in a `<p>` produces invalid `<hN><p>…</p>`. The server **hard-rejects** a
743
+ headline whose `content` is a sole `<p>…</p>` block (422
744
+ `headline_content_block_p`). Store **bare inline text**, exactly like a
745
+ factory headline:
746
+
747
+ - ✅ `content: "Ready to get started?"`
748
+ - ✅ inline emphasis is fine: `content: "Save <strong>40%</strong> today"`
749
+ - ❌ `content: "<p>98%</p>"` — a `<p>` block belongs to the **`text`**
750
+ element, not a headline.
751
+
752
+ For paragraph / body copy (which *should* be a `<p>`), use the `text`
753
+ element, not a headline.
754
+
755
+ ---
756
+
757
+ ## Troubleshooting: builder panel is empty / "The builder is not available on this page"
758
+
759
+ If the YOOtheme Customizer's left Builder panel is empty or shows *"The
760
+ builder is not available on this page. It can only be used on pages, posts
761
+ and categories"* on a page you KNOW has a layout, the usual cause is a
762
+ **stale Customizer session**, NOT a broken layout. YOOtheme's Customizer
763
+ preview cookie is derived from the WordPress login-session token; when that
764
+ token rotates (re-login, session timeout, a different tab), the preview
765
+ renders as a plain page without the builder-data injection and the panel
766
+ reports "not available". **Fix: reload the Customizer tab** (or re-open the
767
+ builder from Pages → the page → Builder). The stored layout is intact —
768
+ confirm with `page_get_layout({ template_id })`, which reads it directly
769
+ and is unaffected by the browser session.
770
+
771
+ ---
772
+
773
+ ## Layout versioning (handled automatically — informational)
774
+
775
+ Every layout this server writes is stamped with the installed YOOtheme
776
+ version at the layout root, exactly like the builder UI does. You do not
777
+ need to set this. It prevents YOOtheme from treating a written layout as
778
+ pre-versioning legacy content and running its migration chain (which would
779
+ re-map current-schema values and bloat the tree) on every load. An existing
780
+ version on a layout you read-modify-write is preserved, never overwritten.
781
+
782
+ ---
783
+
456
784
  ## Workflow 2: Bind a dynamic source to a grid
457
785
 
458
786
  **Goal:** Wire an existing Grid (or other multi-item element) to a
@@ -465,7 +793,7 @@ renders dynamic items.
465
793
  2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` and
466
794
  `yootheme_builder_page_get_layout({ template_id: "<id>", flat: false })`:
467
795
  locate the target Grid. Note its JSON-Pointer `path` (e.g.
468
- `/0/children/2/children/0`).
796
+ `/children/0/children/2/children/0`).
469
797
  3. `yootheme_builder_element_get({ template_id, element_path })`:
470
798
  fetch the Grid's current props so you can preserve them. Binding
471
799
  sets `props.source` and leaves the rest alone.
@@ -528,15 +856,15 @@ renders dynamic items.
528
856
  // is a first-class L1 tool — call it directly.
529
857
  yootheme_builder_element_bind_source({
530
858
  template_id: "home",
531
- element_path: "/0/children/2/children/0",
859
+ element_path: "/children/0/children/2/children/0",
532
860
  source_name: "wp_posts",
533
861
  etag: "abc123"
534
862
  // source_id: "wordpress:wp_posts" // pass ONLY when name collides
535
863
  })
536
- // Response: { path: "/0/children/2/children/0", etag: "def456", has_binding: true }
864
+ // Response: { rel_path: "/children/0/children/2/children/0", etag: "def456", has_binding: true }
537
865
  // Verify (direct L1 call):
538
866
  yootheme_builder_element_get_binding({
539
- template_id: "home", element_path: "/0/children/2/children/0"
867
+ template_id: "home", element_path: "/children/0/children/2/children/0"
540
868
  })
541
869
  // → { source_name: "wp_posts", source_config: { ... }, ... }
542
870
  ```
@@ -666,20 +994,20 @@ CMS-level template duplication (in wp-admin or Joomla administrator).
666
994
  // Step 5. Clone the section element as a sibling.
667
995
  yootheme_builder_element_clone({
668
996
  template_id: "home",
669
- element_path: "/0/children/2", // the hero section to duplicate
997
+ element_path: "/children/0/children/2", // the hero section to duplicate
670
998
  etag: "abc123"
671
999
  })
672
- // Response: { path: "/0/children/3", etag: "def456" }
1000
+ // Response: { rel_path: "/children/0/children/3", etag: "def456" }
673
1001
 
674
1002
  // Step 7. Tweak the clone (replace props entirely, or pass merge: true).
675
1003
  const current = yootheme_builder_element_get({
676
1004
  template_id: "home",
677
- element_path: "/0/children/3",
1005
+ element_path: "/children/0/children/3",
678
1006
  });
679
1007
  yootheme_builder_element_update_settings({
680
1008
  template_id: "home",
681
- element_path: "/0/children/3",
682
- props: { ...current.props, background: "secondary" },
1009
+ element_path: "/children/0/children/3",
1010
+ props: { ...current.props, style: "secondary" }, // section background = `style`
683
1011
  etag: "def456"
684
1012
  })
685
1013
  ```
@@ -882,15 +1210,19 @@ yootheme_builder_element_types_list({
882
1210
  yootheme_builder_element_type_get_schema({ element_type: "headline" })
883
1211
  // Returns the field definitions for the headline's `props`.
884
1212
 
885
- // Step 6. Place the element.
1213
+ // Step 6. Place the element. This is the HERO headline, so title_element:"h1"
1214
+ // — the ONE h1 the page is allowed. A section title would be "h2", a card
1215
+ // title "h3", a metric number "div". `content` is bare inline text (never a
1216
+ // <p> block — see "Typography & heading styles"). If you omit title_element it
1217
+ // DEFAULTS to h1, so set it explicitly on every headline to avoid multiple h1s.
886
1218
  yootheme_builder_element_add({
887
1219
  template_id: "home",
888
- parent_path: "/0/children/2", // row inside section
1220
+ parent_path: "/children/0/children/2", // row inside section
889
1221
  element_type: "headline",
890
- props: { content: "Welcome", tag: "h1" },
1222
+ props: { content: "Welcome", title_element: "h1" },
891
1223
  etag: "abc123"
892
1224
  })
893
- // Response: { path: "/0/children/2/children/0", etag: "def456" }
1225
+ // Response: { rel_path: "/children/0/children/2/children/0", etag: "def456" }
894
1226
  ```
895
1227
 
896
1228
  **Edge case:** A child theme can override a built-in element's
@@ -951,6 +1283,11 @@ and the props payload you passed.
951
1283
  `yootheme_builder_advanced({ tool: "yootheme_builder_clean_implode_directives", arguments: { template_id, element_path, etag } })`.
952
1284
  Returns the audit log + a fresh ETag; idempotent (`cleaned_count: 0` when
953
1285
  there is nothing to remove).
1286
+ - **Start from a pre-designed layout or element preset instead of
1287
+ hand-building** (the user wants a "Home"/"Contact"/"Services" page, or a
1288
+ ready-made Accordion/Alert preset, not a section built element-by-element):
1289
+ see "Layout Library" below (`library_browse` → `library_get` →
1290
+ `library_insert` → adapt).
954
1291
 
955
1292
  If the user asks for something none of the above covers (e.g. global
956
1293
  theme settings, menu management, media library), tell them clearly:
@@ -964,24 +1301,32 @@ Every element is addressed by a JSON-Pointer `path` (RFC-6901-style).
964
1301
  The read tools hand you the path; the write tools consume it. Learn the
965
1302
  grammar once and you never guess an address again.
966
1303
 
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.
1304
+ **The canonical shape (use this): rel_path.** A `rel_path` is a proper
1305
+ RFC-6901 pointer relative to the layout root: `/children/<index>` for each
1306
+ level down. A two-level path is `/children/0/children/1` (child 0 of the
1307
+ layout root, then child 1 of that); a three-level path is
1308
+ `/children/0/children/2/children/0`. Each `/children/N` descends one level
1309
+ into the Nth child (0-based). This is the form `element_list` emits as
1310
+ `rel_path`, and the form every worked example in this skill uses.
972
1311
 
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
1312
  - **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).
1313
+ same address with the template prefix. Read/write tools return the
1314
+ fully-qualified pointer as `path`; you can pass it straight back.
1315
+
1316
+ **What the tools EMIT:** `element_list` / `page_get_schema` return both `path`
1317
+ (fully-qualified) and `rel_path` (the canonical `/children/0/...` form); every
1318
+ write-tool response (`element_add`, `element_update_settings`, `element_clone`,
1319
+ `element_move`, `element_bind_source`, `page_dynamize`, `scaffold_section`) and
1320
+ `element_get` return the fully-qualified `path` plus the canonical `rel_path`.
1321
+ Copy either straight back into the next call.
1322
+
1323
+ **What the server ACCEPTS (be liberal):** as `element_path` / `parent_path`
1324
+ you may pass the canonical `rel_path` (`/children/0/...`), the fully-qualified
1325
+ pointer (`/templates/<id>/layout/...`), OR the terse index-first form
1326
+ (`/0/children/1`, where a leading bare index is the Nth layout-root child) —
1327
+ all three normalize to the same node, and a leading slash is optional. Prefer
1328
+ `rel_path`: it is the canonical, standards-correct form and what the tools
1329
+ hand you.
985
1330
 
986
1331
  **The two special roots:**
987
1332
 
@@ -991,7 +1336,7 @@ and a leading slash is optional (`0/children/1` works too).
991
1336
  layout node itself, which is not an addressable element. Use `""` for
992
1337
  "the top".
993
1338
 
994
- **Reading the index.** In `/0/children/2/children/1`: child 0 of the layout,
1339
+ **Reading the index.** In `/children/0/children/2/children/1`: child 0 of the layout,
995
1340
  then child 2 of THAT, then child 1 of THAT. Indices are positional and shift
996
1341
  when siblings are added or removed; re-read after a structural mutation before
997
1342
  reusing a path (see the ETag handshake below).
@@ -1100,11 +1445,578 @@ styles, the `#parent` bindings) survives untouched.
1100
1445
  - **Reusing a stale etag.** Feed `new_etag` from the response into your
1101
1446
  next write.
1102
1447
 
1448
+ ---
1449
+
1450
+ ## Layout Library (browse → get → insert → adapt)
1451
+
1452
+ **Goal:** Start a page or section from a pre-designed YOOtheme Pro Library
1453
+ layout or single-element preset instead of hand-building it with
1454
+ `element_add` calls — then adapt the demo content to the customer's brand.
1455
+
1456
+ Four tools, always used in this order (the write-capable third step is
1457
+ the only one that mutates anything):
1458
+
1459
+ 1. `yootheme_builder_library_browse` (read) — search the catalog by
1460
+ taxonomy filters, get back rows with an `id`.
1461
+ 2. `yootheme_builder_library_get` (read) — fetch ONE item's full tree +
1462
+ its section list, using the `id` a browse row gave you.
1463
+ 3. `yootheme_builder_library_insert` (write) — compose + persist that
1464
+ item onto a page or existing template through the same hardened
1465
+ write chain every other element/page write goes through.
1466
+ 4. `yootheme_builder_library_save_as` — the REVERSE direction (a page you
1467
+ built → a reusable library). By default it saves into your OWN local
1468
+ "WootsUp AI Layouts" store (`destination: "ai"`, no apikey — see the
1469
+ dedicated subsection below), NOT YOOtheme's native My Layouts. Reachable
1470
+ ONLY via the gateway:
1471
+ `yootheme_builder_advanced({ tool: "yootheme_builder_library_save_as", arguments: { source: { template_id, element_path }, name: "Hero Section" } })`.
1472
+ It is deliberately NOT a first-class `tools/list` entry (keeps the
1473
+ tools/list token-budget clear); browse/get/insert ARE first-class.
1474
+
1475
+ Two `source`s feed browse/get/insert: `"catalog"` (default — YOOtheme's
1476
+ remote Pro Library) and `"ai"` (your OWN local "WootsUp AI Layouts", no
1477
+ apikey). See "WootsUp AI Layouts" below.
1478
+
1479
+ ### Prerequisite: the REMOTE catalog needs a Library API key configured
1480
+
1481
+ Browse, get, AND insert against the remote catalog (`source: "catalog"`,
1482
+ the default) all depend on a YOOtheme Pro Library API key configured on the
1483
+ connected site (the customer's own theme `apikey`, set once in the YOOtheme
1484
+ Customizer — not something this MCP server manages). If it's missing,
1485
+ `library_browse` does not error — it returns `{ items: [], total: 0 }`.
1486
+ Treat `total: 0` as "ask the customer to check the theme apikey field in the
1487
+ Customizer", not a network/auth failure to retry blindly. **This apikey
1488
+ requirement applies ONLY to the remote catalog** — the local AI-Layouts
1489
+ store (next subsection) needs no apikey at all.
1490
+
1491
+ ### WootsUp AI Layouts — your OWN local library (no apikey, save → reuse)
1492
+
1493
+ The catalog above (`source: "catalog"`, the default) is YOOtheme's REMOTE
1494
+ Pro Library and needs the theme apikey. There is a SECOND source that is
1495
+ entirely LOCAL to the connected site: **"WootsUp AI Layouts"**, the plugin's
1496
+ own isolated store. It works WITHOUT any apikey and it NEVER touches
1497
+ YOOtheme's native "My Layouts". This is the loop that lets an agent build a
1498
+ section once and reuse it across pages — even on a site that has no Library
1499
+ apikey at all.
1500
+
1501
+ - **`source` axis on `library_browse` / `library_get` / `library_insert`:**
1502
+ `source: "catalog"` (default) is the remote Pro Library (needs the apikey);
1503
+ `source: "ai"` is your OWN "WootsUp AI Layouts", served from the local
1504
+ store with no apikey. The `id` you pass to `library_get` /
1505
+ `library_insert` MUST come from a `library_browse` with the SAME `source`.
1506
+ The `"ai"` source honours only `query` (name substring) + `kind`
1507
+ (`layout` | `element`); the catalog layout/element taxonomy filters
1508
+ (`topic`/`type`/`role`/`website`) and `kind: "section"` are REJECTED with
1509
+ a 400 (an honest error, not a silent no-op) — the local store has no
1510
+ taxonomy, only what you saved. (`purpose`/`has_effects` are section-only
1511
+ filters that don't apply to a `layout`/`element` browse at all, so —
1512
+ exactly as on the catalog path — they are silently ignored on
1513
+ `source: "ai"`, never reaching the store; they are NOT rejected.)
1514
+ - **`destination` axis on `library_save_as`:** `destination: "ai"` (DEFAULT —
1515
+ save into the isolated "WootsUp AI Layouts" store, reusable via
1516
+ `library_browse` / `library_insert` with `source: "ai"`, and it NEVER
1517
+ pollutes YOOtheme's native My Layouts) or `destination: "native"` (the
1518
+ escape hatch — save into YOOtheme's built-in "My Layouts"). Because `"ai"`
1519
+ is the default, a plain `library_save_as` already writes to YOUR store,
1520
+ not the theme's.
1521
+ - **Composed AS-SAVED.** A `source: "ai"` insert replays the tree exactly as
1522
+ you saved it — its images are NOT re-resolved (they are already the
1523
+ customer's own images / placeholders), so `library_insert`'s `images`
1524
+ param is a no-op for `source: "ai"`.
1525
+
1526
+ **Round-trip (save your own layout, then reuse it — no apikey):**
1527
+
1528
+ ```jsonc
1529
+ // 1. Save a section you built (or adapted from the catalog) into YOUR store.
1530
+ // destination:"ai" is the default, shown here for clarity. save_as is
1531
+ // gateway-only.
1532
+ yootheme_builder_advanced({ tool: "yootheme_builder_library_save_as", arguments: {
1533
+ source: { template_id: "home", element_path: "/children/2" },
1534
+ name: "Candle Hero",
1535
+ destination: "ai"
1536
+ } })
1537
+ // → { library_id: "a1b2c3d4", name: "Candle Hero", storage: "ai_library.a1b2c3d4", invalid_source_possible: false }
1538
+
1539
+ // 2. Later, on ANY page (even one with no Library apikey) browse YOUR store …
1540
+ yootheme_builder_library_browse({ source: "ai", query: "hero" })
1541
+ // → { items: [{ id: "a1b2c3d4", name: "Candle Hero", kind: "layout" }], total: 1 }
1542
+
1543
+ // 3. … and insert it — same hardened write chain as a catalog insert.
1544
+ yootheme_builder_library_insert({
1545
+ id: "a1b2c3d4",
1546
+ source: "ai",
1547
+ target: { template_id: "about", element_path: "" },
1548
+ etag: "<etag>"
1549
+ })
1550
+ ```
1551
+
1552
+ **When to use which store:** reach for `source: "catalog"` to START from
1553
+ YOOtheme's professionally-designed demos (needs the apikey); reach for
1554
+ `source: "ai"` to REUSE a layout you already saved on this site — your own
1555
+ reusable building blocks, apikey or not.
1556
+
1557
+ ### The taxonomy filters on `library_browse`
1558
+
1559
+ `kind` picks which catalog you're browsing: `"layout"` (full pre-designed
1560
+ pages, the default), `"element"` (single-element presets), or **`"section"`
1561
+ (the SECTION CORPUS — every individual section of every Pro layout,
1562
+ classified by purpose)**. The remaining filters are exact-match taxonomy
1563
+ axes, and one of them has a name that does NOT match YOOtheme's own public
1564
+ vocabulary — know this before you guess a param name from the YOOtheme docs:
1565
+
1566
+ | Param | Axis (kind) | Example values |
1567
+ |----------------|--------------------------|----------------------------------------------|
1568
+ | `topic` | industry (all kinds) | `"Health & Fitness"`, `"Beauty & Fashion"` |
1569
+ | `type` | site purpose (layout+section) | `"Company"`, `"E-commerce"`, `"Magazine"` |
1570
+ | `role` | page role (layouts) | `"Home"`, `"Contact"`, `"Services"` |
1571
+ | `purpose` | **section role (sections)** | `"hero"`, `"features"`, `"cta"`, `"gallery"`, `"pricing"`, `"faq"`, `"contact"`, `"team"`, `"stats"`, `"testimonial"`, `"logos"`, `"content"` |
1572
+ | `has_effects` | **section has parallax/sticky/etc (sections, bool)** | `true` / `false` |
1573
+ | `element_type` | preset element (presets) | `"accordion"`, `"alert"`, `"headline"` |
1574
+ | `website` | source demo (all) | `"Makai"`, `"FC Greenfield"`, `"Balou"` |
1575
+ | `query` | substring on name (all) | `"hero"` |
1576
+
1577
+ **`kind: "section"` is how you COMPOSE a page from individually-chosen
1578
+ sections** instead of dropping one whole demo layout. A section row's `id`
1579
+ is the composite `"<layoutId>#<sectionIndex>"` (e.g. `"955bd3dc#0"`); each
1580
+ row also carries `purpose`, `topic`/`type`/`website`, `visual_effects`, and
1581
+ a `thumbnail_url` (its parent layout's preview). Feed that composite `id`
1582
+ straight into `library_get` / `library_insert` with `kind: "section"`.
1583
+
1584
+ **`role` is YOOtheme's "layout" taxonomy axis, renamed.** YOOtheme's own
1585
+ catalog calls this 4th axis "layout" (Home/Footer/Post/Services/Contact/…
1586
+ — the page's ROLE), but the MCP tool parameter is `role`, not `layout` —
1587
+ `kind: "layout"` already occupies that word for "which catalog". Passing
1588
+ `layout: "Home"` to `library_browse` is silently ignored (unknown key
1589
+ rejected by the schema); use `role: "Home"`.
1590
+
1591
+ `limit` (1-100, default 25) + `cursor` (from a previous response's
1592
+ `next_cursor`) paginate; `total: 0` (with an otherwise-valid filter
1593
+ combination) usually means the filter combination matched nothing, not a
1594
+ missing key — narrow or drop filters to check.
1595
+
1596
+ ### Composing a PROFESSIONAL page — the selection ladder (do NOT grab the first result)
1597
+
1598
+ The catalog returns rows in a FIXED order. The first rows are NOT "the
1599
+ best" — they are just first, and one demo (e.g. Balou) sits at the top of
1600
+ most loosely-filtered results. Grab the top row and you ship that one demo
1601
+ relabeled. A professional result comes from FIT, not position:
1602
+
1603
+ 1. **Read the brief for INDUSTRY + TONE.** "Handmade scented candles,
1604
+ warm/boutique" → industry ≈ retail/e-commerce, tone ≈ warm, premium,
1605
+ calm. "SaaS analytics dashboard" → type Company/Software, tone clean/
1606
+ minimal/techy. Name the tone explicitly before you browse.
1607
+ 2. **Filter to the relevant demos** with `type` (E-commerce/Company/
1608
+ Magazine…) + `topic` (industry). This narrows hundreds of layouts to the
1609
+ few that could fit.
1610
+ 3. **EVALUATE MULTIPLE candidate websites — never take the first.** Scan the
1611
+ distinct `website` values in the filtered rows (an E-commerce filter
1612
+ surfaces Balou, Woolberry, Tech Space, …). For your 2-3 finalists pull the
1613
+ thumbnail — `thumbnail_url` is free; `library_get({ include_thumbnail:
1614
+ true })` returns the actual image block (~950 tokens, finalists ONLY) —
1615
+ and JUDGE which demo's aesthetic matches the brief's TONE. A playful
1616
+ kids-brand demo (cartoon mascots, bright primaries) is the WRONG base for
1617
+ a premium/calm brand even when it is technically the right `type`.
1618
+ 4. **Commit to ONE `website` for the whole page.** Sections from different
1619
+ demos carry different spacing rhythm, type scale, and decorative
1620
+ language; mixing them reads as inconsistent. Pick the single best-fit
1621
+ `website` and compose from ITS sections (`kind: "section"`, `website:
1622
+ "X"`), or load its whole matching layout. Mix websites only when you
1623
+ deliberately need a section no single demo provides — then reconcile the
1624
+ look by eye.
1625
+ 5. **Whole layout vs section-compose.** If ONE demo layout already matches
1626
+ the page you need (hero → features → about → contact), load it whole
1627
+ (`kind: "layout"`) for guaranteed internal consistency. If you need a
1628
+ bespoke structure (this demo's hero + that demo's pricing table),
1629
+ assemble it from `kind: "section"` rows.
1630
+
1631
+ ### Make it the CUSTOMER's brand, not the demo's
1632
+
1633
+ A loaded demo carries that demo's SPECIFIC identity. "Adapting" is NOT just
1634
+ rewriting headlines — you must strip the demo's fingerprints, or the page
1635
+ reads as an obviously-relabeled template:
1636
+
1637
+ - **Off-brand decorative graphics.** Many demos scatter decorative SVG
1638
+ illustrations (a kids-shop's cartoon rockets/planets/clouds; a hand-drawn
1639
+ mascot). Those belong to THAT brand. A section row's `visual_effects`
1640
+ listing `floating_decoration` is the signal it carries them. After
1641
+ inserting, locate them (`page_get_layout({ mode: "outline" })` or
1642
+ `layout_query`) and `element_delete` the ones that don't fit the
1643
+ customer's brand. A premium candle shop with cartoon rockets drifting past
1644
+ its hero is instantly "not real".
1645
+ - **Demo product/subject photography.** `curated` downloads the demo's OWN
1646
+ photos — including its specific products (a knitwear demo's baby suits, a
1647
+ realty demo's houses). You cannot honestly relabel a teddy-bear photo as
1648
+ "Vanilla Candle". For product/subject slots, clear those images to
1649
+ placeholder (awaiting the customer's real photos) rather than shipping a
1650
+ mismatched photo. Atmospheric/texture shots (a calm interior, hands at
1651
+ work) often transfer; product-identity shots do not.
1652
+ - **Copy.** Rewrite EVERY headline, subheading, button, and body string to
1653
+ the customer's brand and offering — leave NO demo copy ("Knit & More",
1654
+ "Inspired by our children") anywhere.
1655
+ - **The professionalism signal is the IMAGERY.** Structure and style come
1656
+ from the Library for free; what separates "finished" from "template" is
1657
+ on-brand images. A clean placeholder reads as "ready for your photos"; a
1658
+ MISMATCHED demo photo reads as "broken". When you cannot supply the
1659
+ customer's real image, prefer the honest placeholder over the wrong photo
1660
+ — and TELL the customer which slots need their photography.
1661
+ - **Not every section degrades gracefully to a placeholder — CHOOSE ONES
1662
+ THAT DO.** This is decisive for the hero, which sets the whole first
1663
+ impression. A single `image` element renders YOOtheme's clean placeholder
1664
+ graphic when empty (looks intentional). But an image-DRIVEN section —
1665
+ a `slideshow` / `gallery` / full-bleed background-image hero — renders
1666
+ BLANK (an empty white void, not even a placeholder graphic) when its
1667
+ images are placeholders. A blank slideshow hero reads as broken, not
1668
+ "ready for photos". So when you will leave a section on placeholders
1669
+ (i.e. you don't have the customer's photos yet), PREFER sections whose
1670
+ design holds up WITHOUT a photo: a text-on-solid-`style` hero (dark or
1671
+ muted section + headline + CTA), a typographic/`content` section, an
1672
+ icon/feature grid. AVOID committing to a `slideshow`/`gallery`/full-bleed
1673
+ photo hero unless you actually have images to fill it. Check a candidate
1674
+ section's `element_signature` (a `slideshow`/`gallery`/`overlay-slider`
1675
+ signature = image-driven) before you make it your imageless hero. A calm
1676
+ text hero with clean type beats a blank photo slot every time.
1677
+ - **CONTRAST is non-negotiable — dark text on a dark panel is invisible.**
1678
+ A section originally designed as text-over-a-photo often carries dark text
1679
+ meant to sit on a light image. When you clear that image and the section
1680
+ falls back to a dark `style` (`secondary` / `primary` resolve dark on most
1681
+ themes), the text stays dark → dark-on-dark → the hero looks empty/broken.
1682
+ Whenever a section sits on a DARK background, set the SECTION's
1683
+ `text_color: "light"` (it cascades to all descendants) so the headline,
1684
+ subhead and CTA read; on a LIGHT/`muted` background leave `text_color`
1685
+ default (dark). This is a section-level prop and the ONE style token you
1686
+ DO adjust when re-theming a hero (it is a legibility fix, not a brand
1687
+ override). After building an imageless hero, sanity-check: is the headline
1688
+ clearly readable against its background? If you cannot tell, the customer
1689
+ can't either — fix the contrast. For a hero over a real PHOTO (not a solid
1690
+ style), text_color alone is not enough; see "A photo hero needs a scrim"
1691
+ under the image-sourcing subsection.
1692
+
1693
+ ### `library_get`: the natural next call
1694
+
1695
+ `library_get({ id })` (the `id` comes straight off a `library_browse`
1696
+ row) returns `{ id, kind, name, version, tree, sections,
1697
+ images_manifest_count }`. `sections` lists the item's top-level
1698
+ `tree.children` as `{ index, name, path, element_count }` — layouts only;
1699
+ a preset (`kind: "element"`) always returns `sections: []` because a
1700
+ preset IS one element, not a container of sections. Use a `sections[]`
1701
+ entry's `index` as `library_insert`'s `section_index` to splice in ONE
1702
+ section instead of the whole layout (e.g. just the "Hero" section from a
1703
+ 6-section Home layout).
1704
+
1705
+ ### Load-semantics: what you get is NOT the demo thumbnail
1706
+
1707
+ **A Library item ADOPTS the connected site's currently-active Style**
1708
+ (the YOOtheme Customizer's global UIkit theming) when it renders — it
1709
+ does **not** carry the demo site's colors with it. The catalog item's
1710
+ props are semantic UIkit tokens (`style: "primary"` / `"muted"` /
1711
+ `"secondary"`, `text_color: "light"`, UIkit utility classes), never
1712
+ hardcoded hex colors. Those tokens resolve against whatever style the
1713
+ CONNECTED site currently has active, so the same "Balou Home" layout
1714
+ renders in the customer's own brand colors, not Balou's. Do not tell a
1715
+ customer "this will look like the screenshot" — it will look like their
1716
+ site.
1717
+
1718
+ **When you adapt the inserted content, preserve those semantic tokens —
1719
+ never replace them with a hardcoded color.** Swapping `style: "primary"`
1720
+ for a literal `#1a73e8` (or similar) breaks themeability: the section
1721
+ stops following the site's Customizer style entirely, and a future
1722
+ Customizer style change silently skips it. "Adapting" a Library item
1723
+ means changing CONTENT (copy, images, links) — never the style/
1724
+ `text_color` tokens, the responsive `width_*` props, or any of the other
1725
+ structural/authoring props documented above (Multi-column layouts,
1726
+ Typography). If a specific override is genuinely wanted, that's a
1727
+ deliberate `element_update_settings` call the customer asked for, not a
1728
+ default step of "adapting" a Library insert.
1729
+
1730
+ **Collapsing is a feature — don't defeat it with dummy content.** An
1731
+ empty/unbound field in an inserted section collapses cleanly at
1732
+ render-time (Element → Column → Row → Section cascade; a 3-column grid
1733
+ with 2 empty columns renders as one full-width column). Leave a field you
1734
+ have nothing to put there GENUINELY empty rather than stuffing placeholder
1735
+ text into it "so it doesn't look broken" — the collapsing behavior is
1736
+ what makes a Library section adapt gracefully to less content than the
1737
+ demo had.
1738
+
1739
+ ### Images: `placeholder` (default) vs `curated`
1740
+
1741
+ `library_insert`'s `images` param controls what happens to the item's
1742
+ demo image references (`image`, `image_svg`, `poster`, and several
1743
+ element-specific media props — all point at short-lived signed CDN URLs
1744
+ at fetch time, never persisted raw):
1745
+
1746
+ - **`"placeholder"` (the default — zero network I/O).** Every image prop
1747
+ is cleared to an empty value; YOOtheme's own placeholder graphic renders
1748
+ in its place. This is the right choice for a REAL customer site — the
1749
+ agent (or the customer) fills the placeholders with the customer's own
1750
+ photos afterward. Nothing is downloaded, nothing sideloads into the
1751
+ Media Library.
1752
+ - **`"curated"` — downloads every UNIQUE demo image once into the site's
1753
+ Media Library** (a signed URL reused across multiple props/nodes, e.g.
1754
+ the same hero photo as both a section background and a card thumbnail,
1755
+ is imported exactly once, not N times). Use this for a QUICK DEMO where
1756
+ looking finished immediately matters more than using the customer's own
1757
+ photography. Requires the same Library API key as browse/get/insert —
1758
+ no separate key. A per-image download failure degrades ONLY that one
1759
+ image to a placeholder and is counted in the response; it never aborts
1760
+ the rest of the insert.
1761
+
1762
+ The insert response's `images_resolved: { downloaded, placeholders }`
1763
+ tells you exactly what happened — check it rather than assuming every
1764
+ image round-tripped.
1765
+
1766
+ ### Fill image slots with real on-topic photos (search → import → bind)
1767
+
1768
+ 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.
1769
+
1770
+ **The flow (three tools):**
1771
+ 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.
1772
+ 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.
1773
+ 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.
1774
+ 4. Bind it: `yootheme_builder_element_update_settings({ template_id, element_path, props: { image: "<local_url>" }, merge: true })`.
1775
+
1776
+ **Real photo vs honest placeholder:**
1777
+ - 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.
1778
+ - 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.
1779
+ - 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.
1780
+ - 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.
1781
+
1782
+ 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.
1783
+
1784
+ **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.
1785
+
1786
+ **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.
1787
+
1788
+ ### Composing RICH, VARIED sections: the Pro-DNA palette (choose and vary, never default to bare)
1789
+
1790
+ 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.
1791
+
1792
+ **Where the variety comes from (in this order):**
1793
+ 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).
1794
+ 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.
1795
+
1796
+ **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.**
1797
+ - **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.
1798
+ - **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.
1799
+ - **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).
1800
+ - **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")`).
1801
+ - **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`).
1802
+ - **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.
1803
+ - **Section contrast**: alternate section `style` (`default` / `muted` / `primary` / `secondary`) so adjacent sections do not blur together; `text_color: light` on a dark section.
1804
+ - **Precise image control** (verified on the panel): `image_focal_point`, `image_align`, `image_width`, `image_border` (`rounded`/`circle`/`pill`).
1805
+ - **SVG shape dividers** between sections where it fits.
1806
+
1807
+ **Per-purpose PALETTE: a menu of rich options to CHOOSE from, not a mandate. Pick what fits the brief, and vary.**
1808
+ - 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.)
1809
+ - 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.)
1810
+ - Gallery or Portfolio: a filterable `gallery`, OR an overlay-panel grid with hover, OR a `slideshow`. (Not: a plain image grid.)
1811
+ - Testimonials: a quotation with image, OR an `overlay`, OR a `panel` slider. (Not: one flat quote.)
1812
+ - Team: a card grid of `panel`s with `panel_style: card-*` (photo=`image`, name=`title`, role=`meta`, hover). (Not: bare photos with captions.)
1813
+ - Pricing: a row of `panel`s with `panel_style: card-*` + a button, OR a comparison table.
1814
+ - FAQ: an `accordion`.
1815
+ - Stats: `countdown` or big-number panels.
1816
+ - Logos: a `gallery` or `list` of many logo items.
1817
+
1818
+ **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.
1819
+
1820
+ **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.
1821
+
1822
+ ### `target`: where an item lands (and the strict Preset → Column rule)
1823
+
1824
+ `library_insert`'s `target` is EXACTLY ONE of three shapes, and which
1825
+ ones are legal depends on `kind`:
1826
+
1827
+ | `target` shape | Accepts `kind: "layout"` | Accepts `kind: "element"` (preset) |
1828
+ |---|---|---|
1829
+ | `{ new_page: { title, status? } }` | ✓ creates a page, layout's sections become its content | ✗ rejected — a preset is not a page |
1830
+ | `{ template_id, element_path? }` | ✓ ONLY the layout ROOT is section-accepting (`element_path: ""` or omitted) | ✓ ONLY a **Column** — every other node type is rejected |
1831
+ | `{ 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 |
1832
+
1833
+ **A preset (`kind: "element"`) MUST target a Column. There is no
1834
+ auto-wrap.** Pointing a preset at a section, row, layout root, new_page,
1835
+ or sublayout returns a `422 target_not_a_column` naming what the target
1836
+ actually is. The fix is always the same: scaffold a fresh
1837
+ `section → row → column` hull with `yootheme_builder_scaffold_section`
1838
+ first, then point `element_path` at ONE of the returned `column_paths`:
1839
+
1840
+ ```jsonc
1841
+ // A preset pointed at the layout root — REJECTED, no auto-wrap.
1842
+ yootheme_builder_library_insert({
1843
+ id: "8cf55920",
1844
+ kind: "element",
1845
+ target: { template_id: "home", element_path: "" }
1846
+ })
1847
+ // → 422 target_not_a_column: 'target at "/" is a "layout"'.
1848
+
1849
+ // Fix: scaffold a section->row->column hull, then target the new Column.
1850
+ yootheme_builder_scaffold_section({ template_id: "home", columns: 1, etag: "<etag>" })
1851
+ // Response includes column_paths: ["/children/4/children/0/children/0"]
1852
+
1853
+ yootheme_builder_library_insert({
1854
+ id: "8cf55920",
1855
+ kind: "element",
1856
+ target: { template_id: "home", element_path: "/children/4/children/0/children/0" },
1857
+ etag: "<fresh>"
1858
+ })
1859
+ ```
1860
+
1861
+ **Sublayout targets accept only rows.** A sublayout/fragment element
1862
+ (see "JSON-Pointer path grammar" and the Multi-Items material above)
1863
+ holds row-groups, not sections — `library_insert` handles the strip for
1864
+ you automatically when `target.sublayout` is used; you never call a
1865
+ separate strip step. Point `element_path` at the actual sublayout/
1866
+ fragment node itself, not its parent — pointing at the wrong level is a
1867
+ `422 target_not_a_sublayout`.
1868
+
1869
+ ### Adapting a Library insert is STATIC content editing, not dynamizing
1870
+
1871
+ **Every Library layout and preset is authored STATIC** — no source
1872
+ bindings, no `#parent` parent-source, no multiplication, no conditions.
1873
+ Adapting one after insert means editing copy/images/links on the fixed
1874
+ nodes it already has (`element_update_settings` on the specific
1875
+ headline/text/image `element_path`s), the same as editing any hand-built
1876
+ section.
1877
+
1878
+ **Making an inserted section DATA-DRIVEN (repeat over a Source's items —
1879
+ an archive-style listing) is a deliberate, separate follow-up call, not
1880
+ something insert does for you.** Use the existing dynamize workflow
1881
+ documented in "Dynamize an existing section" just above this one
1882
+ (`yootheme_builder_page_dynamize`) to re-target a Library-inserted
1883
+ section's leaf fields onto a Source, or bind a Multi-Items container
1884
+ directly with `yootheme_builder_element_bind_source` (Workflow 2) if the
1885
+ inserted item already has a grid/list container you want to re-point at
1886
+ live data instead of its demo items.
1887
+
1888
+ **Common pitfalls:**
1889
+
1890
+ - **Assuming the inserted layout will look like the demo screenshot.**
1891
+ It adopts the connected site's active Style — different fonts, colors,
1892
+ spacing than the catalog thumbnail. Tell the customer that up front.
1893
+ - **Hardcoding a color while "adapting" content.** Keep `style` /
1894
+ `text_color` / UIkit classes exactly as inserted; only touch copy,
1895
+ images, and links.
1896
+ - **Pointing a preset (`kind: "element"`) at anything but a Column** and
1897
+ expecting an auto-wrap. There isn't one — scaffold first.
1898
+ - **Choosing `images: "curated"` for a production customer site.** That's
1899
+ for quick demos; a real site wants `placeholder` + the customer's own
1900
+ photography.
1901
+ - **Stuffing placeholder copy into a field you have nothing for**, to
1902
+ defeat collapsing. Leave it empty; collapsing is the intended behavior.
1903
+ - **Expecting `library_insert` to make anything dynamic.** It composes
1904
+ STATIC content only — dynamizing is the separate `page_dynamize` /
1905
+ `element_bind_source` call.
1906
+
1907
+ **Worked example (browse → get → insert → adapt):**
1908
+
1909
+ ```jsonc
1910
+ // 1. Browse: filter by industry + page-role, layouts only (kind default).
1911
+ yootheme_builder_library_browse({ topic: "Health & Fitness", role: "Home", limit: 10 })
1912
+ // Response: { items: [{ id: "955bd3dc", name: "Home", kind: "layout",
1913
+ // meta: { topic: "Health & Fitness", type: "Company", role: "Home", website: "Makai", image: "<cdn-image-url>" },
1914
+ // thumbnail_url: "<cdn-thumbnail-url>" }], total: 64, next_cursor: null }
1915
+
1916
+ // 2. Fetch the chosen item's tree + section list.
1917
+ yootheme_builder_library_get({ id: "955bd3dc" })
1918
+ // Response: { id: "955bd3dc", kind: "layout", name: "Home", version: "5.0.9",
1919
+ // tree: { type: "layout", children: [ /* ... */ ] },
1920
+ // sections: [
1921
+ // { index: 0, name: "Hero", path: "/children/0", element_count: 12 },
1922
+ // { index: 1, name: "Services", path: "/children/1", element_count: 18 }
1923
+ // ],
1924
+ // images_manifest_count: 9 }
1925
+
1926
+ // 3. Insert only the "Hero" section onto an existing page's layout root.
1927
+ yootheme_builder_library_insert({
1928
+ id: "955bd3dc",
1929
+ target: { template_id: "home", element_path: "" },
1930
+ section_index: 0,
1931
+ images: "placeholder",
1932
+ etag: "<etag>"
1933
+ })
1934
+ // Response: { template_id: "home", inserted_path: "/children/3", element_count: 12,
1935
+ // images_mode: "placeholder", images_resolved: { downloaded: 0, placeholders: 6 },
1936
+ // new_etag: "<fresh>", frontend_url: null }
1937
+
1938
+ // 4. Adapt: swap the demo headline for the customer's own copy. Read the
1939
+ // inserted section back first (page_get_layout / element_get) to find the
1940
+ // real headline's element_path, then edit CONTENT only — style untouched.
1941
+ yootheme_builder_element_update_settings({
1942
+ template_id: "home",
1943
+ element_path: "/children/3/children/0/children/0",
1944
+ props: { content: "<customer's real headline>" },
1945
+ merge: true,
1946
+ etag: "<fresh>"
1947
+ })
1948
+ ```
1949
+
1950
+ **Success criterion:** the inserted section renders in the CONNECTED
1951
+ site's active Style (not the catalog demo's colors), every field the
1952
+ customer supplied content for shows that content, and every field they
1953
+ did not touch either shows a clean placeholder (`images: "placeholder"`)
1954
+ or collapses (empty unbound field) — never a broken image icon or a
1955
+ dangling demo string.
1956
+
1957
+ ## Look at what you built — the visual check (and two capture traps)
1958
+
1959
+ `render_preview` proves the page RENDERED and that a string reached the
1960
+ HTML. It does **not** prove it LOOKS right. Text can be present and
1961
+ invisible (muted grey on white); a section can be structurally perfect and
1962
+ still read as a dead band. **If your client has a browser tool, screenshot
1963
+ the page's `frontend_url` and LOOK at it before you call the work done.**
1964
+ That is not optional polish — it is the step that catches the failures
1965
+ every structural check reports as fine.
1966
+
1967
+ Two capture traps make a CORRECT page look broken. Both are artifacts of
1968
+ the SCREENSHOT, not of the layout — verified live against YOOtheme Pro:
1969
+
1970
+ **Trap 1 — scrollspy + lazy-load: a no-scroll full-page shot is mostly
1971
+ blank.** YOOtheme animates sections in with `uk-scrollspy`: elements sit at
1972
+ `opacity: 0` until scrolled into view, and images lazy-load. A full-page
1973
+ screenshot that never scrolls captures them at opacity 0 — you get a long
1974
+ white page with only the hero visible. **Fix: scroll the full document
1975
+ height in viewport-sized steps first (that fires scrollspy + lazy-load),
1976
+ then capture.**
1977
+
1978
+ **Trap 2 — `fixed` parallax bands can never be stitched.** A section using
1979
+ `image_effect: "fixed"` (CSS `background-attachment: fixed`) does not
1980
+ survive a stitched full-page capture at all — scrolling does NOT help. It
1981
+ comes out as a pure white void. **Fix: for any fixed-effect section, scroll
1982
+ it into view and capture the VIEWPORT only (a band shot).**
1983
+
1984
+ **Never judge — and never DELETE — a section based on a full-page
1985
+ thumbnail.** A real agent came within one call of deleting a page's best
1986
+ section (a full-bleed parallax band carrying the founder quote) because the
1987
+ full-page shot showed it as an empty white gap.
1988
+
1989
+ Once you can actually see the page, hunt the defects no tool reports:
1990
+
1991
+ - **Invisible / low-contrast text** — e.g. a caption left on muted grey
1992
+ over white. Fix with an explicit contrast prop (e.g. `meta_color:
1993
+ "emphasis"`), never by deleting the caption.
1994
+ - **Reading order** — a column that leads with a tall image buries its own
1995
+ headline, so the neighbouring column reads first and the story inverts.
1996
+ Fix with `element_move` so both columns start with their headline at the
1997
+ same height.
1998
+ - **No display moment** — every headline semantic and same-sized reads
1999
+ flat. Give exactly ONE hero headline a display `title_style` while
2000
+ KEEPING its `title_element` (size ≠ tag — see Typography; SEO untouched).
2001
+ - **Section rhythm** — two adjacent same-background sections blur together;
2002
+ alternate one (`style: "muted"`).
2003
+ - **Idiom coherence** — a section borrowed from a different demo can carry
2004
+ a foreign visual language (e.g. hairline SaaS metric cards inside warm
2005
+ editorial photography). Cut it or restyle it; do not leave the seam.
2006
+ - **Copy contradictions** — check the page's own facts against each other
2007
+ ("Fifteen years" under a "SINCE 2014" eyebrow) and drop claims repeated
2008
+ in three sections.
2009
+
2010
+ **If you have no browser tool, say so plainly.** Report that you verified
2011
+ the page structurally (`render_preview` + `expect`) but did NOT see it, and
2012
+ ask the customer to eyeball it. Never imply a visual check you did not
2013
+ perform.
2014
+
1103
2015
  ## Appendix: Tool Catalog (auto-generated)
1104
2016
 
1105
2017
  <!-- TOOL-CATALOG:BEGIN -->
1106
2018
 
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.
2019
+ **47 catalogued tools** plus the `yootheme_builder_advanced` (write) + `yootheme_builder_advanced_read` (read-only) gateways = **49 reachable via `tools/list`** (37 L1 + 2 L3 + 2 gateways = 41 advertised; the gateways route to 8 additional advanced tools, bringing the total to 49 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
2020
 
1109
2021
  | Tool | Kind | Input keys | Description |
1110
2022
  | --- | --- | --- | --- |
@@ -1122,24 +2034,32 @@ styles, the `#parent` bindings) survives untouched.
1122
2034
  | `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
2035
  | `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
2036
  | `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. |
2037
+ | `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
2038
  | `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
2039
  | `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. |
2040
+ | `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. |
2041
+ | `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
2042
  | `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
2043
  | `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
2044
  | `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. |
2045
+ | `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. |
2046
+ | `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. |
2047
+ | `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. |
2048
+ | `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. |
2049
+ | `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. |
2050
+ | `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
2051
  | `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
2052
  | `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
2053
  | `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
2054
  | `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
2055
  | `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. |
1136
2056
  | `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. |
2057
+ | `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. |
2058
+ | `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. |
2059
+ | `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
2060
  | `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
2061
  | `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. |
2062
+ | `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
2063
  | `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
2064
  | `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
2065
  | `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. |