@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.
- package/README.md +8 -8
- package/dist/client.d.ts +6 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +38 -2
- package/dist/client.js.map +1 -1
- package/dist/gateway/advanced-tool/domains.d.ts +14 -2
- package/dist/gateway/advanced-tool/domains.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/domains.js +20 -5
- package/dist/gateway/advanced-tool/domains.js.map +1 -1
- package/dist/gateway/essentials.d.ts +1 -1
- package/dist/gateway/essentials.d.ts.map +1 -1
- package/dist/gateway/essentials.js +27 -0
- package/dist/gateway/essentials.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/platform/detect.d.ts +1 -1
- package/dist/platform/detect.js +1 -1
- package/dist/platform/index.d.ts +1 -1
- package/dist/platform/index.js +1 -1
- package/dist/proxy/mode.d.ts.map +1 -1
- package/dist/proxy/mode.js +25 -3
- package/dist/proxy/mode.js.map +1 -1
- package/dist/setup-cli.d.ts +8 -0
- package/dist/setup-cli.d.ts.map +1 -1
- package/dist/setup-cli.js +58 -6
- package/dist/setup-cli.js.map +1 -1
- package/dist/setup-npx-spec.d.ts +1 -1
- package/dist/setup-npx-spec.js +1 -1
- package/dist/setup-prompts.d.ts +11 -0
- package/dist/setup-prompts.d.ts.map +1 -1
- package/dist/setup-prompts.js +28 -0
- package/dist/setup-prompts.js.map +1 -1
- package/dist/setup-wizard-defaults.d.ts.map +1 -1
- package/dist/setup-wizard-defaults.js +40 -9
- package/dist/setup-wizard-defaults.js.map +1 -1
- package/dist/setup-wizard-handshake.d.ts +5 -4
- package/dist/setup-wizard-handshake.d.ts.map +1 -1
- package/dist/setup-wizard-handshake.js +28 -24
- package/dist/setup-wizard-handshake.js.map +1 -1
- package/dist/setup-wizard-types.d.ts +48 -9
- package/dist/setup-wizard-types.d.ts.map +1 -1
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +62 -10
- package/dist/setup-wizard.js.map +1 -1
- package/dist/sites/env-bridge.d.ts +19 -0
- package/dist/sites/env-bridge.d.ts.map +1 -1
- package/dist/sites/env-bridge.js +29 -15
- package/dist/sites/env-bridge.js.map +1 -1
- package/dist/sites/store.d.ts +1 -1
- package/dist/sites/store.js +1 -1
- package/dist/skill-loader.d.ts +2 -2
- package/dist/skill-loader.js +2 -2
- package/dist/tools/elements/builders.d.ts.map +1 -1
- package/dist/tools/elements/builders.js +21 -2
- package/dist/tools/elements/builders.js.map +1 -1
- package/dist/tools/elements/handlers-write.d.ts +2 -0
- package/dist/tools/elements/handlers-write.d.ts.map +1 -1
- package/dist/tools/elements/handlers-write.js +16 -4
- package/dist/tools/elements/handlers-write.js.map +1 -1
- package/dist/tools/elements/handlers.d.ts +1 -0
- package/dist/tools/elements/handlers.d.ts.map +1 -1
- package/dist/tools/elements/handlers.js +14 -0
- package/dist/tools/elements/handlers.js.map +1 -1
- package/dist/tools/format/pages-format.d.ts.map +1 -1
- package/dist/tools/format/pages-format.js +21 -0
- package/dist/tools/format/pages-format.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/library.d.ts +279 -0
- package/dist/tools/library.d.ts.map +1 -0
- package/dist/tools/library.js +1060 -0
- package/dist/tools/library.js.map +1 -0
- package/dist/tools/local-content/builders.d.ts +19 -0
- package/dist/tools/local-content/builders.d.ts.map +1 -0
- package/dist/tools/local-content/builders.js +127 -0
- package/dist/tools/local-content/builders.js.map +1 -0
- package/dist/tools/local-content/handlers.d.ts +54 -0
- package/dist/tools/local-content/handlers.d.ts.map +1 -0
- package/dist/tools/local-content/handlers.js +287 -0
- package/dist/tools/local-content/handlers.js.map +1 -0
- package/dist/tools/local-content/index.d.ts +17 -0
- package/dist/tools/local-content/index.d.ts.map +1 -0
- package/dist/tools/local-content/index.js +16 -0
- package/dist/tools/local-content/index.js.map +1 -0
- package/dist/tools/local-content/schemas.d.ts +82 -0
- package/dist/tools/local-content/schemas.d.ts.map +1 -0
- package/dist/tools/local-content/schemas.js +89 -0
- package/dist/tools/local-content/schemas.js.map +1 -0
- package/dist/tools/pages/builders.d.ts.map +1 -1
- package/dist/tools/pages/builders.js +16 -10
- package/dist/tools/pages/builders.js.map +1 -1
- package/dist/tools/sources/handlers-bind.d.ts +2 -0
- package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
- package/dist/tools/sources/handlers-bind.js +18 -0
- package/dist/tools/sources/handlers-bind.js.map +1 -1
- package/manifest.json +4 -3
- package/package.json +4 -3
- 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
|
|
16
|
-
idempotent tools (
|
|
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
|
-
(
|
|
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
|
|
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
|
|
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 (
|
|
36
|
-
[
|
|
37
|
-
a Bearer key in **wp-admin → Tools → "YT Builder MCP" → Bearer Keys**.
|
|
38
|
-
- **Joomla 5/6**. Install the `yt-builder-mcp` package (
|
|
39
|
-
[
|
|
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 `.
|
|
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
|
|
57
|
-
The `yootheme_builder_advanced` gateway routes all
|
|
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
|
-
- **
|
|
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,
|
|
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
|
-
|
|
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`,
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
156
|
-
so hosts auto-allow it). Prefer the read gateway
|
|
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
|
-
`
|
|
160
|
-
|
|
161
|
-
(
|
|
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
|
|
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: {
|
|
383
|
-
append a new section at the template root (`parent_path: ""`).
|
|
384
|
-
|
|
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",
|
|
393
|
-
add the
|
|
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: {
|
|
567
|
+
props: { style: "primary" }, // section background = `style`, NOT `background`
|
|
434
568
|
etag: "abc123" // from yootheme_builder_get_etag
|
|
435
569
|
})
|
|
436
|
-
// Response: {
|
|
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: {
|
|
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: {
|
|
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,
|
|
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",
|
|
1222
|
+
props: { content: "Welcome", title_element: "h1" },
|
|
891
1223
|
etag: "abc123"
|
|
892
1224
|
})
|
|
893
|
-
// Response: {
|
|
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
|
|
968
|
-
|
|
969
|
-
path is `/0/children/1` (child 0 of the
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
`
|
|
984
|
-
|
|
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
|
-
**
|
|
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` |
|
|
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 (
|
|
1139
|
-
| `yootheme_builder_pages_resolve` | read+idempotent | `site_id`, `url` | Resolve a public page/article
|
|
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. |
|