@wootsup/yt-builder-mcp 1.9.0 → 1.11.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 +19 -10
- package/dist/gateway/advanced-read-tool.d.ts.map +1 -1
- package/dist/gateway/advanced-read-tool.js +14 -2
- package/dist/gateway/advanced-read-tool.js.map +1 -1
- package/dist/gateway/advanced-tool/domains.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/domains.js +5 -0
- package/dist/gateway/advanced-tool/domains.js.map +1 -1
- package/dist/gateway/advanced-tool/execute.d.ts +7 -1
- package/dist/gateway/advanced-tool/execute.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/execute.js +58 -4
- package/dist/gateway/advanced-tool/execute.js.map +1 -1
- package/dist/server.d.ts +14 -11
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +14 -11
- package/dist/server.js.map +1 -1
- package/dist/sites/tools/sites-list.d.ts.map +1 -1
- package/dist/sites/tools/sites-list.js +22 -2
- package/dist/sites/tools/sites-list.js.map +1 -1
- package/dist/tools/elements/builders.d.ts.map +1 -1
- package/dist/tools/elements/builders.js +69 -31
- 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 +132 -9
- package/dist/tools/elements/handlers-write.js.map +1 -1
- package/dist/tools/elements/handlers.d.ts +7 -4
- package/dist/tools/elements/handlers.d.ts.map +1 -1
- package/dist/tools/elements/handlers.js +49 -8
- package/dist/tools/elements/handlers.js.map +1 -1
- package/dist/tools/elements/schema-validation.d.ts +18 -0
- package/dist/tools/elements/schema-validation.d.ts.map +1 -1
- package/dist/tools/elements/schema-validation.js +49 -5
- package/dist/tools/elements/schema-validation.js.map +1 -1
- package/dist/tools/format/sources-format.d.ts +12 -5
- package/dist/tools/format/sources-format.d.ts.map +1 -1
- package/dist/tools/format/sources-format.js +31 -6
- package/dist/tools/format/sources-format.js.map +1 -1
- package/dist/tools/health.d.ts.map +1 -1
- package/dist/tools/health.js +18 -5
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/inspection.d.ts.map +1 -1
- package/dist/tools/inspection.js +81 -12
- package/dist/tools/inspection.js.map +1 -1
- package/dist/tools/layout-flatten.d.ts +7 -1
- package/dist/tools/layout-flatten.d.ts.map +1 -1
- package/dist/tools/layout-flatten.js +24 -13
- package/dist/tools/layout-flatten.js.map +1 -1
- package/dist/tools/library.d.ts +23 -0
- package/dist/tools/library.d.ts.map +1 -1
- package/dist/tools/library.js +213 -15
- package/dist/tools/library.js.map +1 -1
- package/dist/tools/local-content/builders.js +11 -11
- package/dist/tools/local-content/builders.js.map +1 -1
- package/dist/tools/local-content/handlers.d.ts.map +1 -1
- package/dist/tools/local-content/handlers.js +34 -5
- package/dist/tools/local-content/handlers.js.map +1 -1
- package/dist/tools/local-content/schemas.d.ts +9 -4
- package/dist/tools/local-content/schemas.d.ts.map +1 -1
- package/dist/tools/local-content/schemas.js +14 -6
- package/dist/tools/local-content/schemas.js.map +1 -1
- package/dist/tools/multi-items/builders.d.ts.map +1 -1
- package/dist/tools/multi-items/builders.js +11 -8
- package/dist/tools/multi-items/builders.js.map +1 -1
- package/dist/tools/pages/builders.d.ts.map +1 -1
- package/dist/tools/pages/builders.js +122 -46
- package/dist/tools/pages/builders.js.map +1 -1
- package/dist/tools/pages/handlers-audit.d.ts +129 -0
- package/dist/tools/pages/handlers-audit.d.ts.map +1 -0
- package/dist/tools/pages/handlers-audit.js +704 -0
- package/dist/tools/pages/handlers-audit.js.map +1 -0
- package/dist/tools/pages/handlers-read.d.ts +8 -3
- package/dist/tools/pages/handlers-read.d.ts.map +1 -1
- package/dist/tools/pages/handlers-read.js +172 -25
- package/dist/tools/pages/handlers-read.js.map +1 -1
- package/dist/tools/pages/handlers-write.d.ts +17 -3
- package/dist/tools/pages/handlers-write.d.ts.map +1 -1
- package/dist/tools/pages/handlers-write.js +79 -26
- package/dist/tools/pages/handlers-write.js.map +1 -1
- package/dist/tools/pages/index.d.ts +5 -1
- package/dist/tools/pages/index.d.ts.map +1 -1
- package/dist/tools/pages/index.js +5 -1
- package/dist/tools/pages/index.js.map +1 -1
- package/dist/tools/pages/schemas.d.ts +319 -1
- package/dist/tools/pages/schemas.d.ts.map +1 -1
- package/dist/tools/pages/schemas.js +340 -4
- package/dist/tools/pages/schemas.js.map +1 -1
- package/dist/tools/pages/target.d.ts +162 -0
- package/dist/tools/pages/target.d.ts.map +1 -0
- package/dist/tools/pages/target.js +239 -0
- package/dist/tools/pages/target.js.map +1 -0
- package/dist/tools/shared-schemas.d.ts +22 -0
- package/dist/tools/shared-schemas.d.ts.map +1 -1
- package/dist/tools/shared-schemas.js +22 -0
- package/dist/tools/shared-schemas.js.map +1 -1
- package/dist/tools/sources/builders.d.ts.map +1 -1
- package/dist/tools/sources/builders.js +46 -28
- package/dist/tools/sources/builders.js.map +1 -1
- package/dist/tools/sources/handlers-bind.d.ts +7 -8
- package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
- package/dist/tools/sources/handlers-bind.js +106 -7
- package/dist/tools/sources/handlers-bind.js.map +1 -1
- package/dist/tools/sparse-fields.d.ts +12 -0
- package/dist/tools/sparse-fields.d.ts.map +1 -1
- package/dist/tools/sparse-fields.js +34 -2
- package/dist/tools/sparse-fields.js.map +1 -1
- package/dist/tools/tool-builder/results.d.ts.map +1 -1
- package/dist/tools/tool-builder/results.js +114 -0
- package/dist/tools/tool-builder/results.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/skills/yt-builder-mcp/SKILL.md +735 -44
|
@@ -12,15 +12,15 @@ description: Drive the YOOtheme Pro Page Builder on WordPress or Joomla 5/6. Dis
|
|
|
12
12
|
> WordPress and Joomla 5/6.
|
|
13
13
|
|
|
14
14
|
This skill helps AI assistants drive the YOOtheme Pro Page Builder through the
|
|
15
|
-
`@wootsup/yt-builder-mcp` server. The server catalogues
|
|
16
|
-
idempotent tools (
|
|
15
|
+
`@wootsup/yt-builder-mcp` server. The server catalogues 49 typed, scoped,
|
|
16
|
+
idempotent tools (51 callable end-to-end with the two gateways). 41 of them
|
|
17
17
|
advertised as first-class entries in `tools/list`
|
|
18
|
-
(37 essential L1 forwards + 2 direct L3 entries + 2 gateways), and
|
|
18
|
+
(37 essential L1 forwards + 2 direct L3 entries + 2 gateways), and 10 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 10; the read-only
|
|
21
21
|
`yootheme_builder_advanced_read` gateway routes only the read-only subset
|
|
22
22
|
(page_get_schema, inspect_sublayout, layout_query, local_content_types,
|
|
23
|
-
local_content_read) and is itself
|
|
23
|
+
local_content_read, pages_audit) and is itself
|
|
24
24
|
read-only-annotated, so MCP hosts can auto-allow pure inspection without a
|
|
25
25
|
per-call write-approval prompt. This 3-lane split keeps `tools/list` well below
|
|
26
26
|
the ~40-tool Cursor cap while every catalogued tool stays fully reachable.
|
|
@@ -55,7 +55,7 @@ entries when the user types "YT Builder MCP" into the picker:
|
|
|
55
55
|
|
|
56
56
|
- **`YT Builder MCP for YOOtheme Pro (unofficial)`**: the MCP **server**.
|
|
57
57
|
Provides the 41 first-class tools (37 essential, 2 direct, 2 gateways).
|
|
58
|
-
The `yootheme_builder_advanced` gateway routes all
|
|
58
|
+
The `yootheme_builder_advanced` gateway routes all 10 advanced tools; the
|
|
59
59
|
read-only `yootheme_builder_advanced_read` gateway routes the read-only
|
|
60
60
|
subset without a write-approval prompt.
|
|
61
61
|
- **`Von YT Builder MCP for YOOtheme Pro`**: the bundled **skill** (this
|
|
@@ -125,10 +125,11 @@ The server exposes:
|
|
|
125
125
|
step that turns a placeholder-filled skeleton into a finished-looking
|
|
126
126
|
page (see "Fill image slots with real on-topic photos" under Layout
|
|
127
127
|
Library).
|
|
128
|
-
- **
|
|
128
|
+
- **10 advanced captured tools**: everything else (page_get_schema,
|
|
129
129
|
element_unbind_source, clean_implode_directives,
|
|
130
|
-
`inspect_sublayout`, `layout_query`, `library_save_as`,
|
|
131
|
-
Content Read tools `local_content_types` / `local_content_read
|
|
130
|
+
`inspect_sublayout`, `layout_query`, `library_save_as`, the two Local
|
|
131
|
+
Content Read tools `local_content_types` / `local_content_read`,
|
|
132
|
+
`pages_audit`, and `library_delete` — the INVERSE of `library_save_as`).
|
|
132
133
|
Reachable through one gateway tool:
|
|
133
134
|
`yootheme_builder_advanced({ tool: "<name>", arguments: { ... } })`. For
|
|
134
135
|
example, to inspect a panel/grid_item `< SUBLAYOUT`:
|
|
@@ -143,6 +144,18 @@ The server exposes:
|
|
|
143
144
|
It flags `invalid_source_possible: true` when the saved subtree carries
|
|
144
145
|
bound dynamic sources that may not exist wherever it is later reloaded —
|
|
145
146
|
re-bind after inserting it elsewhere.
|
|
147
|
+
And to clean up an experimental saved entry (PAGES_LIBRARY_MEDIA-16 — the
|
|
148
|
+
INVERSE of `library_save_as`; `source` defaults to `"ai"`, the ONLY deletable
|
|
149
|
+
store; the curated library is read-only):
|
|
150
|
+
`yootheme_builder_advanced({ tool: "yootheme_builder_library_delete", arguments: { id: "955bd3dc" } })`.
|
|
151
|
+
Pass the `id` from a `library_browse({ source: "ai" })` row; an unknown id
|
|
152
|
+
returns a 404, and `source: "catalog"` returns a 403 (read-only).
|
|
153
|
+
And to run a one-call site-wide SEO/structure/content/links audit
|
|
154
|
+
(read-only, severity-sorted findings + machine-executable fix_hints),
|
|
155
|
+
prefer the read gateway:
|
|
156
|
+
`yootheme_builder_advanced_read({ tool: "yootheme_builder_pages_audit", arguments: { checks: ["seo", "structure"] } })`
|
|
157
|
+
(the write gateway routes it too, if you prefer one entry point:
|
|
158
|
+
`yootheme_builder_advanced({ tool: "yootheme_builder_pages_audit" })`).
|
|
146
159
|
|
|
147
160
|
The core write/refactor + render-verify tools the Glöckle HANDOFF-4/5 ask for
|
|
148
161
|
are now **L1 — call them directly by name**, not through a gateway:
|
|
@@ -184,7 +197,8 @@ The server exposes:
|
|
|
184
197
|
- **2 gateway tools**: `yootheme_builder_advanced` (write-capable; can route
|
|
185
198
|
into delete/unbind, so hosts gate it) and `yootheme_builder_advanced_read`
|
|
186
199
|
(read-only; routes ONLY the read-only advanced subset — page_get_schema,
|
|
187
|
-
inspect_sublayout, layout_query, local_content_types, local_content_read
|
|
200
|
+
inspect_sublayout, layout_query, local_content_types, local_content_read,
|
|
201
|
+
pages_audit —
|
|
188
202
|
and is read-only-annotated so hosts auto-allow it). Prefer the read gateway
|
|
189
203
|
for pure inspection:
|
|
190
204
|
`yootheme_builder_advanced_read({ tool: "yootheme_builder_page_get_schema", arguments: { template_id } })`.
|
|
@@ -196,8 +210,8 @@ The server exposes:
|
|
|
196
210
|
`yootheme_builder_advanced({ tool: "yootheme_builder_local_content_read", arguments: { kind: "entities", type: "post" } })`.)
|
|
197
211
|
|
|
198
212
|
`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
|
|
200
|
-
(41 advertised +
|
|
213
|
+
essentials + 2 L3 direct + 2 gateways. The total callable surface is 51
|
|
214
|
+
(41 advertised + 10 advanced reachable through the gateways). If the AI
|
|
201
215
|
client reports "tool not found", you are almost certainly calling an
|
|
202
216
|
advanced tool by its raw name. Wrap it in
|
|
203
217
|
`yootheme_builder_advanced({ tool, arguments })` (or the read gateway for
|
|
@@ -266,6 +280,100 @@ serves_context_ids: [11] }`. Use `pages_resolve` when you just want the answer;
|
|
|
266
280
|
use the `serves_context_*` columns when you already hold `pages_list` and want to
|
|
267
281
|
match locally.
|
|
268
282
|
|
|
283
|
+
### Address a page WITHOUT its URL — the `target` object
|
|
284
|
+
|
|
285
|
+
`pages_resolve` and `pages_audit` take the **same** `target` object, so one
|
|
286
|
+
addressing vocabulary covers both "which template renders this?" and "audit just
|
|
287
|
+
this page". Supply **exactly ONE of** these five keys:
|
|
288
|
+
|
|
289
|
+
| Key | Value | Example |
|
|
290
|
+
|-----|-------|---------|
|
|
291
|
+
| `template_id` | An exact template id | `{ template_id: "page:4816" }` |
|
|
292
|
+
| `post_id` | The numeric WP post / Joomla article id | `{ post_id: 4816 }` |
|
|
293
|
+
| `url` | A public frontend URL (same resolver as `pages_resolve({ url })`) | `{ url: "https://example.com/schedule-demo/" }` |
|
|
294
|
+
| `special` | A semantic page ROLE (vocabulary below) | `{ special: "home" }` |
|
|
295
|
+
| `title` | Case-insensitive substring of the page NAME/label | `{ title: "SunWest" }` |
|
|
296
|
+
|
|
297
|
+
**On the wire it stays one object.** MCP-native transports pass `target`
|
|
298
|
+
through as the object above; the REST transport encodes it as **bracket params**
|
|
299
|
+
— `?target[special]=home`, `?target[template_id]=page:4816`,
|
|
300
|
+
`?target[url]=<url-encoded URL>` — which WordPress and Joomla both parse back
|
|
301
|
+
into an array natively. Same key names either way, so what you read in the table
|
|
302
|
+
is what goes over the wire on either CMS. You never build that querystring
|
|
303
|
+
yourself; it matters when you read a server log or reproduce a call with `curl`.
|
|
304
|
+
|
|
305
|
+
**The special vocabulary (all ten tokens):** `home` and `front-page` resolve
|
|
306
|
+
through the platform's front-page configuration (WordPress `show_on_front` /
|
|
307
|
+
`page_on_front`; Joomla's default menu item). `blog` resolves through
|
|
308
|
+
WordPress's `page_for_posts` setting, and on Joomla through the site's blog
|
|
309
|
+
**menu item** (see the caveat below). `search`, `404`, `category`, `tag`,
|
|
310
|
+
`author` and `archive` resolve forward through the template type index.
|
|
311
|
+
`date` is recognised but unsupported on
|
|
312
|
+
BOTH platforms and returns
|
|
313
|
+
`special_not_supported` — say so rather than guessing a date archive.
|
|
314
|
+
|
|
315
|
+
**Three platform caveats worth stating up front:**
|
|
316
|
+
|
|
317
|
+
- On **Joomla**, `author` maps to the **Contact** template
|
|
318
|
+
(`com_contact.contact`) — Joomla has no post-author archive, so the contact
|
|
319
|
+
view is the conventional analogue. Do not report it as a WordPress-style
|
|
320
|
+
author archive.
|
|
321
|
+
- On **Joomla**, `blog` is a **menu item**, not a site setting.
|
|
322
|
+
There is no Joomla equivalent of `page_for_posts`, so the resolver reads the
|
|
323
|
+
published site menu: a `view=featured` item wins outright (it is the
|
|
324
|
+
site-wide news front); otherwise the `view=category&layout=blog` items
|
|
325
|
+
decide. Those items are reduced to the DISTINCT TEMPLATES they resolve to
|
|
326
|
+
before anything is reported: several menu items governed by one template are
|
|
327
|
+
one page, and a blog category governed by no template is dropped rather than
|
|
328
|
+
offered as a dead address. One template left resolves outright — even when a
|
|
329
|
+
dozen menu items led to it. **Two or more return `blog_ambiguous`** (HTTP
|
|
330
|
+
409) with the same `candidates` shape as `title_ambiguous` — one row per
|
|
331
|
+
distinct blog page, `label` being the menu title the user recognises and `id`
|
|
332
|
+
the governing template, always present and always usable. Show the list and
|
|
333
|
+
let the user choose; then re-address the page with
|
|
334
|
+
`target.template_id`, `target.url` or `target.title`.
|
|
335
|
+
Sites with no blog menu item at all fall back to the post-archive template.
|
|
336
|
+
`special: "home"` is unaffected — it still resolves the default menu item.
|
|
337
|
+
- `date` is unsupported on both platforms (there is no date-archive vocabulary
|
|
338
|
+
yet), which is why it returns a precise `special_not_supported` instead of an
|
|
339
|
+
"unknown token" error.
|
|
340
|
+
|
|
341
|
+
**Typed failures, so you never have to guess what went wrong:** two or more keys
|
|
342
|
+
(or `url` alongside a `target`) → `too_many_targets`; a `title` matching nothing
|
|
343
|
+
→ `title_no_match`; a `title` matching two or more pages → `title_ambiguous`, with
|
|
344
|
+
a `candidates` list (max 10) to pick from — show that list to the user instead of
|
|
345
|
+
choosing for them; `special: "blog"` on a Joomla site with several blog menu
|
|
346
|
+
items → `blog_ambiguous`, carrying the same `candidates` list under the same
|
|
347
|
+
rule. A token that is resolvable in principle but matches no
|
|
348
|
+
template on this site → `special_not_resolvable`. Supplying NO address is not an
|
|
349
|
+
error — it falls back to each tool's default: `pages_audit` sweeps the whole
|
|
350
|
+
site, while `pages_resolve` needs one and answers `missing_url`.
|
|
351
|
+
|
|
352
|
+
**Every typed code this error envelope carries, and what to do about it.** Most
|
|
353
|
+
concern the ADDRESS; a couple concern the rest of the request shape. The code is
|
|
354
|
+
the contract; the HTTP status only groups them. A code you do not recognise is
|
|
355
|
+
never a transport failure — report it verbatim rather than retrying blindly.
|
|
356
|
+
|
|
357
|
+
| Code | Status | What happened | What to do |
|
|
358
|
+
|------|--------|---------------|------------|
|
|
359
|
+
| `no_target` | 400 | A `target` WAS supplied but none of its members is a usable address — every member must be a **non-empty scalar**. `target[special][x]=y` (a nested value) and a templated `target[title]=` that rendered empty both land here. | Re-send with exactly one non-empty scalar key. Never re-send without a `target` "to see what happens": that is a whole-site sweep whose findings you would attribute to one page. |
|
|
360
|
+
| `too_many_targets` | 400 | Two or more address keys, or `url` alongside `target`. | Pick one. Use `title` or `pages_list` when you only know the page by name. |
|
|
361
|
+
| `types_conflicts_with_target` | 400 | `types` was sent with a `target`. | Drop `types` — a target already names exactly one page, so there is nothing for a type filter to narrow. |
|
|
362
|
+
| `unknown_check_category` | 400 | A `checks` value that is not one of the four categories (`seo`, `structure`, `content`, `links`) — a typo, a finding CODE passed where a category belongs, or a case variant: the vocabulary is **case-sensitive**, so `SEO` is not `seo`. | Re-send with a real category (the message names both the unknown members and the valid set). This is refused rather than filtered because an unrecognised value selects ZERO checks, and zero checks over a normally-scanned site returns `findings: []` — a confident CLEAN verdict for a typo. **Never read such an answer as clean.** |
|
|
363
|
+
| `no_template_matched_types` | 400 | A site-sweep `types` filter matched NO template on this site — a typo or, most often, a case variant (`PAGE` is not `page`). Unlike `checks`, the `types` vocabulary is **open and site-dependent** (`page`, `article`, `single-post`, `com_content.category`, …), so it cannot be validated against a fixed list — only a zero match is knowable, and it is knowable only after the sweep. | Re-send with one of the types the message lists (it names what this site actually has), or omit `types` to sweep everything. Refused for the same reason as `unknown_check_category`: a filter that selected nothing answered HTTP 200 with `findings: []` and an all-zero summary, which reads as CLEAN. **Never read such an answer as clean.** |
|
|
364
|
+
| `template_not_found` | 404 | The `template_id` names no template carrying a YOOtheme layout on this site. | Re-read the id from `pages_list`; ids are site-specific and change when a page is recreated. |
|
|
365
|
+
| `post_not_found` | 404 | The `post_id` names no WP post / Joomla article with a per-page layout. | Same: confirm via `pages_list`. A post that exists but has no per-page layout is rendered by a THEME template — find that template instead. |
|
|
366
|
+
| `url_no_match` | 404 | The URL is well-formed but resolves to no per-page layout — usually because a theme template renders it. | Use `pages_list` to find the governing template, or `pages_resolve({ url })` for the fuller hint. |
|
|
367
|
+
| `title_no_match` | 404 | No page NAME contains that substring. | Try a shorter substring, or list with `pages_list`. |
|
|
368
|
+
| `special_not_resolvable` | 404 | The token is in the vocabulary and resolvable in principle, but matches no template on THIS site. | Report which role was requested; do not silently fall back to another page. |
|
|
369
|
+
| `title_ambiguous` | 409 | The substring matches two or more pages. | A `candidates` list (max 10) rides along — show it and let the user choose. |
|
|
370
|
+
| `blog_ambiguous` | 409 | Joomla only: several distinct blog pages. | Same `candidates` shape; show it (see the Joomla caveat above). |
|
|
371
|
+
| `special_not_supported` | 422 | The token is recognised but this platform has no such page role (today: `date`, on both). | Say so plainly instead of guessing an analogue. |
|
|
372
|
+
| `target_not_scannable` | 422 | **The target resolved to a real page, but this site does not enumerate it as an auditable page** — a post type or status the page enumerator does not cover, or a trashed item. | **This is NOT a clean result.** An audit that answered 200 with zero findings would read as "your page is fine"; this code exists precisely so it cannot. Audit it via `template_summary`, or pick a page `pages_list` returns. |
|
|
373
|
+
|
|
374
|
+
`server_too_old` is the one code the SERVER never sends — see
|
|
375
|
+
[Audit ONE page instead of the whole site](#audit-one-page-instead-of-the-whole-site).
|
|
376
|
+
|
|
269
377
|
## Scopes (Bearer key permissions)
|
|
270
378
|
|
|
271
379
|
Every Bearer key has a scope, set at key creation time:
|
|
@@ -474,6 +582,25 @@ templates are for), but confirm the intent — do not edit a `single-post` /
|
|
|
474
582
|
templates are defined in the theme itself and are not created via this MCP;
|
|
475
583
|
you edit the existing ones `pages_list` reports.
|
|
476
584
|
|
|
585
|
+
**Reaching a theme template by ROLE instead of by id.** You rarely know a theme
|
|
586
|
+
template's opaque id (`I99YS8Ii`). The `special` tokens of the shared `target`
|
|
587
|
+
object address these templates by what they SERVE, on both `pages_resolve` and
|
|
588
|
+
`pages_audit`:
|
|
589
|
+
|
|
590
|
+
| `target` | Lands on |
|
|
591
|
+
|----------|----------|
|
|
592
|
+
| `target: { special: "home" }` | The configured front page (a `page:<n>` when the site serves a static front page, otherwise the posts/archive template) |
|
|
593
|
+
| `target: { special: "blog" }` / `{ special: "archive" }` | The posts page / `archive-post` template |
|
|
594
|
+
| `target: { special: "404" }` | The `error-404` template |
|
|
595
|
+
| `target: { special: "search" }` | The `search` template (Joomla: `com_finder.search`) |
|
|
596
|
+
| `target: { special: "category" }` / `{ special: "tag" }` | The `taxonomy-category` / `taxonomy-post_tag` template (Joomla: `com_content.category` / `com_tags.tag`) |
|
|
597
|
+
| `target: { special: "author" }` | The author archive — on **Joomla** the Contact template |
|
|
598
|
+
|
|
599
|
+
So "audit the 404 page" is one call with `target: { special: "404" }`, and the
|
|
600
|
+
response's `resolved_target` tells you which template that actually was. The
|
|
601
|
+
same safety rule still applies: a finding on a theme template is a finding on
|
|
602
|
+
EVERY page that template renders.
|
|
603
|
+
|
|
477
604
|
## Workflow 1: Build a hero section
|
|
478
605
|
|
|
479
606
|
**Goal:** Add a fresh hero section (heading + sub-heading + CTA button)
|
|
@@ -736,6 +863,12 @@ must have **exactly one** `<h1>`). Set it explicitly on every headline:
|
|
|
736
863
|
for punch); a big stat number is `title_element: "div"` with
|
|
737
864
|
`title_style: "heading-medium"`.
|
|
738
865
|
|
|
866
|
+
**This `h1` default is specific to `headline`.** Container elements
|
|
867
|
+
(`grid`, `panel`, `slideshow`, and the rest of the item-carrier family)
|
|
868
|
+
default to `h3`, not `h1`, when `title_element` is left unset. See
|
|
869
|
+
"Workflow 6 (SEO audit)" below for the full per-type default table
|
|
870
|
+
`pages_audit` uses to find missing/multiple h1 issues across a whole site.
|
|
871
|
+
|
|
739
872
|
### Headline `content` is plain inline text — never wrapped in `<p>`
|
|
740
873
|
|
|
741
874
|
A `headline` renders its `content` **inside** the heading tag, so wrapping it
|
|
@@ -1238,7 +1371,563 @@ and the props payload you passed.
|
|
|
1238
1371
|
|
|
1239
1372
|
---
|
|
1240
1373
|
|
|
1241
|
-
##
|
|
1374
|
+
## Workflow 6: SEO audit (pages_audit)
|
|
1375
|
+
|
|
1376
|
+
**Goal:** Run one deterministic, site-wide sweep for structure, content,
|
|
1377
|
+
SEO, and link issues, drill into the pages a finding names, and fix them
|
|
1378
|
+
one page at a time through the existing write tools this server already
|
|
1379
|
+
exposes. Never analyze findings by hand and never bulk-apply a fix.
|
|
1380
|
+
|
|
1381
|
+
**Canonical tool-call sequence (real parameter names, snake_case):**
|
|
1382
|
+
|
|
1383
|
+
1. `yootheme_builder_advanced_read({ tool: "yootheme_builder_pages_audit", arguments: { checks: ["seo", "structure"] } })`
|
|
1384
|
+
sweeps the whole site in one call. `pages_audit` is a read-only
|
|
1385
|
+
advanced tool, so reach it through the read gateway (the write
|
|
1386
|
+
gateway routes it too, but the read gateway never triggers a
|
|
1387
|
+
write-approval prompt). Omit `checks`/`types` to run every category
|
|
1388
|
+
against every scanned template.
|
|
1389
|
+
2. Read the response's `findings` array. Each finding carries a `code`,
|
|
1390
|
+
`category`, `severity`, a `count`, up to 30 affected `pages`, and
|
|
1391
|
+
(where a safe automatic fix exists) a `fix_hint` naming the exact
|
|
1392
|
+
write tool and args template to run. `duplicate-layout` and
|
|
1393
|
+
`title-collision` never carry a `fix_hint` — they need a human
|
|
1394
|
+
decision (rename, merge, or intentionally keep as-is), not an
|
|
1395
|
+
automatic patch.
|
|
1396
|
+
3. `yootheme_builder_template_summary({ template_id })` drills into one
|
|
1397
|
+
page a finding names. It carries the same `heading_levels`,
|
|
1398
|
+
`images_missing_alt`, and `disabled_elements` counters the audit
|
|
1399
|
+
used, so you can see exactly which nodes are affected before
|
|
1400
|
+
writing.
|
|
1401
|
+
4. Fix that ONE page via the write tool named in `fix_hint.tool`, with
|
|
1402
|
+
`fix_hint.args_template` as a starting point (`element_update_settings`,
|
|
1403
|
+
`element_add`, etc), ETag-locked like every other write in this
|
|
1404
|
+
server.
|
|
1405
|
+
5. `yootheme_builder_page_publish({ template_id, etag })` republishes.
|
|
1406
|
+
The response's `quick_check` block (default on) re-checks THIS page
|
|
1407
|
+
with the same per-page codes, so you can confirm the fix landed
|
|
1408
|
+
without a second full sweep.
|
|
1409
|
+
|
|
1410
|
+
**Common pitfalls:**
|
|
1411
|
+
|
|
1412
|
+
- **Promising a full SEO audit.** `pages_audit` covers Builder structure
|
|
1413
|
+
and CMS basics only: heading hierarchy, empty/thin pages, missing
|
|
1414
|
+
image alt text, duplicate layouts, title collisions, non-pretty URL
|
|
1415
|
+
fallbacks, stale content, and dead in-page anchors. Every response
|
|
1416
|
+
carries a fixed disclaimer field:
|
|
1417
|
+
`"Meta title/description/OG live in your CMS SEO plugin — not covered here."`
|
|
1418
|
+
It does not read or evaluate Yoast/RankMath/any SEO-plugin meta
|
|
1419
|
+
title, meta description, or Open Graph tags. Say this scope boundary
|
|
1420
|
+
out loud and point the user at their SEO plugin for that layer.
|
|
1421
|
+
- **Confusing the CONTENT count with `pages_list.elements_count`.**
|
|
1422
|
+
`pages_list.elements_count` counts every node, including
|
|
1423
|
+
`section`/`row`/`column`/`fragment` layout wrappers. `pages_audit`
|
|
1424
|
+
instead uses a narrower CONTENT count (`elements_content` in the
|
|
1425
|
+
evidence) that excludes `section`/`row`/`column`/`fragment`/`layout`
|
|
1426
|
+
wrapper nodes: an empty section nested in three columns is correctly
|
|
1427
|
+
`0` content elements, not "not empty" just because the wrappers
|
|
1428
|
+
exist. Both numbers ride in the evidence (`elements_total` and
|
|
1429
|
+
`elements_content`). `thin-content` fires when `elements_content` is
|
|
1430
|
+
below `thin_content_threshold` (default 8); `empty-page` fires at
|
|
1431
|
+
`elements_content === 0`.
|
|
1432
|
+
- **The h1-default trap.** Only `headline` defaults to `h1` when
|
|
1433
|
+
`title_element` is unset. A container element (`grid`, `panel`,
|
|
1434
|
+
`slideshow`, and the rest) left unset does NOT become an `<h1>` — it
|
|
1435
|
+
becomes an `<h3>` (or stays a non-heading `div` for
|
|
1436
|
+
`description_list`). See the per-type default table below and
|
|
1437
|
+
"`title_style` vs `title_element`" above (that section's "defaults
|
|
1438
|
+
to `h1`" rule is specific to `headline` only). `meta_element` is a
|
|
1439
|
+
SECOND, independent heading channel on the same container types,
|
|
1440
|
+
defaulting to `div` and counting only when explicitly set to
|
|
1441
|
+
`h1`-`h6` with `show_meta` not `false`.
|
|
1442
|
+
- **Treating a bound-but-empty slot as missing.** Bound title/meta
|
|
1443
|
+
slots count as filled even when the static prop text is empty — a
|
|
1444
|
+
title bound to a dynamic source field is not "missing" just because
|
|
1445
|
+
the builder-side prop is blank. The same exemption applies to
|
|
1446
|
+
`image_alt`/`background_image_alt`: `images-missing-alt` never flags
|
|
1447
|
+
a bound image-alt slot, only an unbound, empty, or whitespace-only
|
|
1448
|
+
alt on a static image.
|
|
1449
|
+
- **Flagging every `#`-prefixed value as a dead anchor.** A
|
|
1450
|
+
`dead-anchor` finding fires only for a genuinely dangling `#name`
|
|
1451
|
+
link whose target element `id` does not exist anywhere on the same
|
|
1452
|
+
page. It explicitly excludes a bare `#` (a placeholder, never a real
|
|
1453
|
+
target), `mailto:`/`tel:` links, `http(s)://` external links, and
|
|
1454
|
+
cross-page links like `?page_id=…#…` (that anchor lives on a
|
|
1455
|
+
different page's render and cannot be verified from this page's own
|
|
1456
|
+
`props.id` set).
|
|
1457
|
+
- **Treating `missing-h1` as critical, or double-flagging it.**
|
|
1458
|
+
Severity is calibrated from live empirical review, not per-rule
|
|
1459
|
+
guessing: `no-headings` (zero headings anywhere on the page) is
|
|
1460
|
+
critical, but `missing-h1` (headings exist, but none is an effective
|
|
1461
|
+
h1) is only a warning, and it is suppressed entirely when
|
|
1462
|
+
`no-headings` already fired for the same page. Real sites
|
|
1463
|
+
legitimately have h2/h3-only pages with no h1.
|
|
1464
|
+
- **Treating `quick_check` findings as a publish failure.** `quick_check`
|
|
1465
|
+
is advisory only: publish and save always succeed regardless of what
|
|
1466
|
+
it reports. This is not the blocking render-precheck (which still
|
|
1467
|
+
gates on genuinely broken output) — SEO/structure findings are
|
|
1468
|
+
hints, not corruption. `quick_check` only ever includes checks whose
|
|
1469
|
+
`scope` is `"per-page"`; `duplicate-layout` and `title-collision`
|
|
1470
|
+
need the whole site swept to mean anything, so they never appear
|
|
1471
|
+
here — instead the block carries a fixed note:
|
|
1472
|
+
`"run pages_audit for site-wide checks"`. `page_publish` attaches
|
|
1473
|
+
`quick_check` by default (set `quick_check: false` to opt out, e.g.
|
|
1474
|
+
scripted bulk publishes); `page_save` carries it only when you opt
|
|
1475
|
+
in with `quick_check: true`.
|
|
1476
|
+
|
|
1477
|
+
> **Changed: the advisory block got bigger and stricter.** Three Tier-2 checks
|
|
1478
|
+
> — `anchor-text-quality` (warning), `generic-alt-text` (info) and
|
|
1479
|
+
> `long-heading` (info) — are `per-page`, so they joined `quick_check`
|
|
1480
|
+
> automatically, and every finding now carries per-hit **locators** (breadcrumb,
|
|
1481
|
+
> element name, JSON-Pointer path, snippet). Measured on a page that trips them:
|
|
1482
|
+
> ~7.5 KB of `quick_check`, roughly 3.3 KB of it locators, against ~1.5 KB
|
|
1483
|
+
> before. It is BOUNDED, not open-ended — the per-page hit cap holds the block at
|
|
1484
|
+
> roughly 11 KB even on a 500-element page — and a clean page is still just
|
|
1485
|
+
> `{ clean: true }`.
|
|
1486
|
+
>
|
|
1487
|
+
> **What this means for you, not for the customer.** `anchor-text-quality`
|
|
1488
|
+
> matches the generic-CTA list EXACTLY, and that list is bilingual: `mehr`,
|
|
1489
|
+
> `hier`, `mehr erfahren`, `weiterlesen` alongside `click here` / `read more`.
|
|
1490
|
+
> On a German site those are ordinary house-style buttons, so a perfectly good
|
|
1491
|
+
> publish can come back carrying a `warning`. **Do not report a warning-severity
|
|
1492
|
+
> `anchor-text-quality` as a failed publish, and do not "fix" the customer's
|
|
1493
|
+
> button copy unasked** — say the publish succeeded, mention the advice once,
|
|
1494
|
+
> and move on. There is no per-check switch on the publish path today: the only
|
|
1495
|
+
> lever is `quick_check: false`, which also silences `missing-h1`,
|
|
1496
|
+
> `empty-page`, `images-missing-alt` and `dead-anchor`. Prefer living with the
|
|
1497
|
+
> advisory over turning the block off.
|
|
1498
|
+
|
|
1499
|
+
**Per-type `title_element` default table (the correctness crux of every
|
|
1500
|
+
heading finding):**
|
|
1501
|
+
|
|
1502
|
+
| Element type(s) | `title_element` default | Counts as heading? |
|
|
1503
|
+
|---|---|---|
|
|
1504
|
+
| `headline` | `h1` | Yes, unless explicitly overridden. |
|
|
1505
|
+
| `comments` (WP source) | `h2` | Yes, always active. |
|
|
1506
|
+
| `alert`, `gallery`, `grid`, `map`, `overlay`, `overlay-slider`, `panel`, `panel-slider`, `popover`, `slideshow`, `switcher` | `h3` | Yes, when `show_title` is not `false` and the title is non-empty or bound. |
|
|
1507
|
+
| `description_list` | `div` | No. |
|
|
1508
|
+
| `section` (Section Title) | (no field; hardcoded `<div class="tm-section-title">`) | No, ever (counted only as an informational `section_titles` tally). |
|
|
1509
|
+
|
|
1510
|
+
`div` never counts as a heading whether it came from an explicit value
|
|
1511
|
+
or a type default.
|
|
1512
|
+
|
|
1513
|
+
### Audit ONE page instead of the whole site
|
|
1514
|
+
|
|
1515
|
+
Pass the shared `target` object (see "Address a page WITHOUT its URL" above) to
|
|
1516
|
+
scope the sweep to a single page — by `template_id`, `post_id`, `url`,
|
|
1517
|
+
`special` or `title`, exactly one of them:
|
|
1518
|
+
|
|
1519
|
+
```jsonc
|
|
1520
|
+
yootheme_builder_advanced_read({
|
|
1521
|
+
tool: "yootheme_builder_pages_audit",
|
|
1522
|
+
arguments: { target: { special: "home" } }
|
|
1523
|
+
})
|
|
1524
|
+
```
|
|
1525
|
+
|
|
1526
|
+
What changes in the response:
|
|
1527
|
+
|
|
1528
|
+
- **`scope: "single"`** — the positive proof the sweep was scoped. If you sent a
|
|
1529
|
+
`target` and the response is not scoped, the tool returns
|
|
1530
|
+
`server_too_old` instead of handing you a site sweep dressed up as a page
|
|
1531
|
+
audit: the site's host plugin predates single-target addressing and must be
|
|
1532
|
+
updated. This is the ONE code the server never sends — the client raises it
|
|
1533
|
+
after comparing what it asked for against what came back.
|
|
1534
|
+
- **`target_not_scannable` is a refusal, never a pass.** A `target` can resolve
|
|
1535
|
+
to a real page that this site does not enumerate as an auditable one — a post
|
|
1536
|
+
type or status the page enumerator does not cover, or a trashed item. Rather
|
|
1537
|
+
than answer `200` with zero findings, which reads as "your page is clean", the
|
|
1538
|
+
tool returns `target_not_scannable` (HTTP 422). Tell the user the page could
|
|
1539
|
+
not be audited and why; then audit it via `template_summary`, or pick a page
|
|
1540
|
+
`pages_list` returns. Never present it as a clean result.
|
|
1541
|
+
- **`resolved_target`** — `{ template_id, post_id, type, label, frontend_url,
|
|
1542
|
+
context, resolved_via, matched_by }`. This is the echo of WHICH page a fuzzy
|
|
1543
|
+
address landed on. Always report it: `special: "home"` and
|
|
1544
|
+
`title: "SunWest"` are guesses until the server says what they resolved to.
|
|
1545
|
+
- **`note`** — the shared, verbatim sentence `run pages_audit for site-wide
|
|
1546
|
+
checks`. A single page cannot be compared against other pages, so the two
|
|
1547
|
+
site-scope checks (`duplicate-layout`, `title-collision`) self-filter out.
|
|
1548
|
+
- **Richer per-hit evidence.** Single mode raises the per-page hit cap from 5 to
|
|
1549
|
+
**100 hits per check** and adds a `content_snippet` to every locator (the site
|
|
1550
|
+
sweep omits snippets to keep a whole-site payload small). The text leg also
|
|
1551
|
+
gains a `Where to fix:` block with the locator lines; the site sweep keeps its
|
|
1552
|
+
compact 5-column summary table, with the locators in `structuredContent` only.
|
|
1553
|
+
- **The summary table's count column counts HITS, and says so.** In a site sweep
|
|
1554
|
+
that column is headed `COUNT` and holds the number of affected **pages**. A
|
|
1555
|
+
single-page sweep scanned exactly one page, so that number would be `1` for
|
|
1556
|
+
every finding and tell you nothing — there the column is headed **`HITS`** and
|
|
1557
|
+
holds the number of individual occurrences **on that page** (19 alt-less
|
|
1558
|
+
images ⇒ `19`, not `1`). It is the uncapped total: `hits_truncated` shortens
|
|
1559
|
+
the listed locators, never this figure. Report the `HITS` number as the size
|
|
1560
|
+
of the problem. `structuredContent.findings[].count` is unaffected by the
|
|
1561
|
+
column — it stays the affected-PAGE count in every scope, so read the table
|
|
1562
|
+
(or the page entry's own `hits`) when you want the per-page hit total.
|
|
1563
|
+
- **`hits` is the unambiguous field; `count` means two different things.** On
|
|
1564
|
+
this wire `count` is level-dependent: on a FINDING it counts affected **pages**
|
|
1565
|
+
(so it is `1` for every finding of a single-page audit), and on a **page entry**
|
|
1566
|
+
it counts hits on that page. One name, two meanings — and that ambiguity has
|
|
1567
|
+
already produced a live misread, an agent reporting "one image is affected"
|
|
1568
|
+
when nineteen were. So the host emits **`hits`** next to a page entry's `count`
|
|
1569
|
+
whenever that count really is a hit tally: `findings[].pages[].hits`. Prefer it.
|
|
1570
|
+
It is an alias, never an invention — a page entry with no hit total (the PAGE
|
|
1571
|
+
ITSELF is the hit: `thin-content`, `empty-page`, `duplicate-layout`) carries no
|
|
1572
|
+
`hits` key at all, and a host plugin older than the alias sends only `count`.
|
|
1573
|
+
Fall back to `pages[].count` in both cases, never to `findings[].count`.
|
|
1574
|
+
- **On the plugin-served transport there is no summary table at all.** The
|
|
1575
|
+
`HITS`/`COUNT` column is rendered by the npm server. When the MCP server is the
|
|
1576
|
+
CMS plugin itself, the response is the JSON payload — so nothing labels the
|
|
1577
|
+
columns for you and the guidance above is the only thing standing between you
|
|
1578
|
+
and the misread. Read `findings[].pages[].hits` (falling back to
|
|
1579
|
+
`pages[].count`) and **never report `findings[].count` as the size of a
|
|
1580
|
+
single-page problem**: in single-target mode it is structurally `1`.
|
|
1581
|
+
|
|
1582
|
+
Two request shapes are refused up front, with a typed error rather than a
|
|
1583
|
+
round-trip: more than one address key → `too_many_targets` (pick one; use
|
|
1584
|
+
`title` or `pages_list` when you only know the page by name), and `target`
|
|
1585
|
+
together with `types` → `types_conflicts_with_target` (a target already names
|
|
1586
|
+
exactly one page, so there is nothing for a type filter to narrow). A `title`
|
|
1587
|
+
that matches several pages returns `title_ambiguous` with a candidate list —
|
|
1588
|
+
show it, let the user choose.
|
|
1589
|
+
|
|
1590
|
+
### Remediation catalog — what each finding means and how to fix it
|
|
1591
|
+
|
|
1592
|
+
The audit reports; YOU explain. Every finding below carries its fixed
|
|
1593
|
+
`category` and `severity` from the server (never re-rank them), and the entries
|
|
1594
|
+
whose fix is mechanical carry a `fix_hint` naming the write tool and an
|
|
1595
|
+
`args_template`. The rest need a human decision — say so instead of inventing a
|
|
1596
|
+
patch.
|
|
1597
|
+
|
|
1598
|
+
#### `empty-page` — structure · critical
|
|
1599
|
+
|
|
1600
|
+
**Why it matters.** A page with zero content elements is a dead end for readers
|
|
1601
|
+
and, once published, a soft-404 signal for crawlers: there is nothing to index,
|
|
1602
|
+
and internal links pointing at it leak crawl budget.
|
|
1603
|
+
**How to fix.** `element_add` at least one content element (headline, text,
|
|
1604
|
+
image). `fix_hint.tool = element_add`.
|
|
1605
|
+
**Severity.** Critical because the page fails at its only job. This is measured
|
|
1606
|
+
on the CONTENT count (`elements_content`), so wrapper-only pages — three nested
|
|
1607
|
+
columns around nothing — are correctly flagged rather than passing as "not
|
|
1608
|
+
empty".
|
|
1609
|
+
|
|
1610
|
+
#### `no-headings` — structure · critical
|
|
1611
|
+
|
|
1612
|
+
**Why it matters.** With no heading of any level the page has no machine-readable
|
|
1613
|
+
outline: search engines get no topical anchor, and screen-reader users lose the
|
|
1614
|
+
heading-jump navigation they rely on to skim a page.
|
|
1615
|
+
**How to fix.** `element_add` a `headline` with `props.title_element = "h1"`
|
|
1616
|
+
(that is the shipped `args_template`), then work down the hierarchy.
|
|
1617
|
+
**Severity.** Critical — it is an accessibility failure, not a style preference,
|
|
1618
|
+
and it is one element away from being fixed.
|
|
1619
|
+
|
|
1620
|
+
#### `missing-h1` — structure · warning
|
|
1621
|
+
|
|
1622
|
+
**Why it matters.** The h1 names what the page is about. Headings exist here but
|
|
1623
|
+
none resolves to an effective h1, so the page's main topic is never stated at the
|
|
1624
|
+
strongest available level.
|
|
1625
|
+
**How to fix.** `element_update_settings` with `props.title_element = "h1"` on
|
|
1626
|
+
the page's `first_headline_path` (the evidence carries it, and the locator points
|
|
1627
|
+
at it). When `first_headline_path` is `null` there is no `headline` element at
|
|
1628
|
+
all — the effective headings come from containers (`grid`, `panel`, …); add a
|
|
1629
|
+
headline rather than promoting a container title to h1 blindly.
|
|
1630
|
+
**Severity.** Only a warning, calibrated on live review: real, healthy sites run
|
|
1631
|
+
h2/h3-only pages. It is also suppressed entirely when `no-headings` already fired
|
|
1632
|
+
for the same page — never report both.
|
|
1633
|
+
|
|
1634
|
+
#### `multiple-h1` — structure · warning
|
|
1635
|
+
|
|
1636
|
+
**Why it matters.** Several h1s dilute the topical signal and flatten the outline
|
|
1637
|
+
a screen reader announces: everything looks equally important, so nothing is.
|
|
1638
|
+
**How to fix.** `element_update_settings` with `props.title_element = "h2"` on
|
|
1639
|
+
every hit except the one you keep. The locator's `heading_source` tells you WHICH
|
|
1640
|
+
channel produced each h1 (`panel-title` vs `panel-meta` — `meta_element` is a
|
|
1641
|
+
second, independent heading channel), which is exactly what the raw path cannot.
|
|
1642
|
+
**Severity.** Warning, not critical — the page still works. Read the evidence's
|
|
1643
|
+
`iterated` flag before you write: `iterated: true` means one authored h1 renders
|
|
1644
|
+
N times through an iterating container or a Multi-Items binding, so the fix is
|
|
1645
|
+
one edit on the item, not N edits; the locator's `×repeat` shows the multiplier.
|
|
1646
|
+
|
|
1647
|
+
#### `heading-skip` — structure · info
|
|
1648
|
+
|
|
1649
|
+
**Why it matters.** Jumping h1 → h3 breaks the document outline: assistive tech
|
|
1650
|
+
reports a missing level, and the hierarchy stops describing the real structure.
|
|
1651
|
+
**How to fix.** No `fix_hint` — the right level depends on intent. Either demote
|
|
1652
|
+
the jumped-to heading (`element_update_settings`, `props.title_element`) or add
|
|
1653
|
+
the missing intermediate level. The evidence lists each skip as
|
|
1654
|
+
`{ path, from, to }`.
|
|
1655
|
+
**Severity.** Info: it is a real hierarchy defect but the page remains usable,
|
|
1656
|
+
and the check is deliberately `headline`-only, so container-started hierarchies
|
|
1657
|
+
are invisible to it. Do not present it as complete heading-hierarchy coverage.
|
|
1658
|
+
|
|
1659
|
+
#### `images-missing-alt` — content · warning
|
|
1660
|
+
|
|
1661
|
+
**Why it matters.** An image with a set source and empty alt text is invisible to
|
|
1662
|
+
screen readers and to image search — the content of the image simply does not
|
|
1663
|
+
exist for anyone not looking at it.
|
|
1664
|
+
**How to fix.** `element_update_settings` with `props.image_alt` (or
|
|
1665
|
+
`props.background_image_alt` on a `popover` — the locator's `prop` field names
|
|
1666
|
+
which one). Describe what the image SHOWS, not that it is an image.
|
|
1667
|
+
**Severity.** Warning. Two exemptions keep it honest: a **bound** alt slot is
|
|
1668
|
+
never flagged (a dynamically-bound alt is not missing just because the builder
|
|
1669
|
+
prop is blank), and a genuinely decorative image may legitimately keep an empty
|
|
1670
|
+
alt — mention that possibility instead of mass-filling every slot.
|
|
1671
|
+
|
|
1672
|
+
#### `thin-content` — content · warning
|
|
1673
|
+
|
|
1674
|
+
**Why it matters.** A page with very few content elements rarely answers the
|
|
1675
|
+
query that brought a visitor to it, and a site full of them dilutes topical
|
|
1676
|
+
authority.
|
|
1677
|
+
**How to fix.** No `fix_hint` — no tool can write substance. Report the
|
|
1678
|
+
`elements_content` count against the threshold and propose merging the page into
|
|
1679
|
+
a stronger one or expanding it.
|
|
1680
|
+
**Severity.** Warning, and threshold-driven (`thin_content_threshold`, default
|
|
1681
|
+
8): a deliberately minimal landing page can be perfectly healthy. Pages at zero
|
|
1682
|
+
are excluded here — `empty-page` owns that case.
|
|
1683
|
+
|
|
1684
|
+
#### `duplicate-layout` — content · warning (site-wide)
|
|
1685
|
+
|
|
1686
|
+
**Why it matters.** Several pages sharing an identical structural layout is
|
|
1687
|
+
usually a sign of near-duplicate content competing for the same query, or of a
|
|
1688
|
+
template that should have been a theme template.
|
|
1689
|
+
**How to fix.** No `fix_hint` — this needs a human decision: consolidate,
|
|
1690
|
+
differentiate, or accept it (a product-page family SHOULD look alike). Group the
|
|
1691
|
+
report by the evidence's `cluster_id`.
|
|
1692
|
+
**Severity.** Warning, and it compares pages against EACH OTHER, so it only runs
|
|
1693
|
+
in a site sweep — it never appears in a single-page audit or in `quick_check`.
|
|
1694
|
+
|
|
1695
|
+
#### `title-collision` — seo · warning (site-wide)
|
|
1696
|
+
|
|
1697
|
+
**Why it matters.** Two pages with the same title are indistinguishable in search
|
|
1698
|
+
results, in the browser's tab bar and in your own `pages_list` — and they
|
|
1699
|
+
cannibalise each other's ranking for the same term.
|
|
1700
|
+
**How to fix.** No `fix_hint` — renaming is an editorial call. Note that
|
|
1701
|
+
comparison is normalised (lowercased, punctuation collapsed), so "A – B" and
|
|
1702
|
+
"A — B" collide by design; that is not a false positive.
|
|
1703
|
+
**Severity.** Warning, site-scope only — same exclusion as `duplicate-layout`.
|
|
1704
|
+
|
|
1705
|
+
#### `non-pretty-url` — seo · info
|
|
1706
|
+
|
|
1707
|
+
**Why it matters.** A `?page_id=…` / `?p=…` URL carries no keywords, is ugly to
|
|
1708
|
+
share, and signals a permalink configuration that was never finished.
|
|
1709
|
+
**How to fix.** No `fix_hint`, and deliberately so: this is fixed in the CMS
|
|
1710
|
+
(WordPress Settings → Permalinks, Joomla SEF settings / a menu item), NOT with a
|
|
1711
|
+
builder write tool. Say that plainly. The evidence's `reason` reads `non-pretty`
|
|
1712
|
+
on every hit today: publication status is not yet surfaced by the page
|
|
1713
|
+
enumerator, so a DRAFT — which often has no pretty URL yet and needs no action —
|
|
1714
|
+
is reported exactly like a published page. Mention that possibility rather than
|
|
1715
|
+
presenting every hit as actionable.
|
|
1716
|
+
**Severity.** Info: it is a hygiene issue, and the page still resolves.
|
|
1717
|
+
|
|
1718
|
+
#### `stale-content` — seo · info
|
|
1719
|
+
|
|
1720
|
+
**Why it matters.** Content untouched for years drifts out of date; freshness is
|
|
1721
|
+
a weak ranking factor but a strong trust factor for readers.
|
|
1722
|
+
**How to fix.** No `fix_hint` — review and update, or deliberately mark it as
|
|
1723
|
+
evergreen. Report the `modified_at` date, not just the flag.
|
|
1724
|
+
**Severity.** Info, threshold-driven (`stale_after_months`, default 24). A page
|
|
1725
|
+
with an empty `modified_at` is never flagged, so absence of the finding is not
|
|
1726
|
+
proof of freshness.
|
|
1727
|
+
|
|
1728
|
+
#### `dead-anchor` — links · warning
|
|
1729
|
+
|
|
1730
|
+
**Why it matters.** An in-page `#name` link whose target id does not exist on the
|
|
1731
|
+
page silently does nothing when clicked — the visitor concludes the site is
|
|
1732
|
+
broken, and a jump-navigation is often exactly the thing that was supposed to
|
|
1733
|
+
make a long page usable.
|
|
1734
|
+
**How to fix.** No `fix_hint`, because there are two equally valid fixes and only
|
|
1735
|
+
you know which was intended: correct the link, or give the intended target
|
|
1736
|
+
element `props.id = <name>` via `element_update_settings`. The locator carries
|
|
1737
|
+
the missing `name`.
|
|
1738
|
+
**Severity.** Warning. The check is deliberately narrow — a bare `#`,
|
|
1739
|
+
`mailto:`/`tel:`, external `http(s)://` links and cross-page `?page_id=…#…`
|
|
1740
|
+
links are all excluded — so a hit is a genuine dangling anchor, not a heuristic.
|
|
1741
|
+
|
|
1742
|
+
#### `anchor-text-quality` — links · warning
|
|
1743
|
+
|
|
1744
|
+
**Why it matters.** Anchor text is the strongest on-page relevance signal a link
|
|
1745
|
+
can send, and screen-reader users navigate by a flat list of link texts — a
|
|
1746
|
+
screen full of "Mehr" and "Read more" is unusable out of context.
|
|
1747
|
+
**How to fix.** `element_update_settings` with `props.content` naming the
|
|
1748
|
+
destination ("Preise ansehen" instead of "Mehr"); on `grid_item` and `panel` the
|
|
1749
|
+
prop is `title`. For an icon-only or image-only link, the right fix is
|
|
1750
|
+
`props.link_aria_label`, not invented visible text. Only link-bearing elements
|
|
1751
|
+
whose visible-text prop is known are checked — `button`, `button_item`,
|
|
1752
|
+
`grid_item`, `panel`, `headline`, `list_item`, `nav_item`, `subnav_item` — so
|
|
1753
|
+
this is an allowlist, not a sweep over every element carrying a `link`.
|
|
1754
|
+
**Severity.** Warning. The locator's `reason` says which rule fired: `empty` or
|
|
1755
|
+
`generic` (an **exact match** against the DE+EN generic list — `mehr`, `hier`,
|
|
1756
|
+
`weiterlesen`, `click here`, `read more`, `learn more`, …). Exact match only, so
|
|
1757
|
+
"Mehr über SunWest" stays clean.
|
|
1758
|
+
|
|
1759
|
+
`empty` states a precise rule, not "no accessible name at all": the type's
|
|
1760
|
+
**designated** text prop is empty AND the node carries no `icon`, `image`,
|
|
1761
|
+
`link_aria_label` or `link_text`, AND no sibling text that renders inside the
|
|
1762
|
+
same anchor — `content`/`meta` on `grid_item` and `panel` (both move the link
|
|
1763
|
+
onto the whole-card wrapper and render meta + content inside it), `meta` on
|
|
1764
|
+
`nav_item` (its `uk-nav-subtitle` sits inside the anchor). So a content-only
|
|
1765
|
+
linked card is NOT flagged. The remaining allowlisted types
|
|
1766
|
+
(`button`, `button_item`, `headline`, `list_item`, `subnav_item`) have no sibling
|
|
1767
|
+
text prop, so for them the designated prop is the only anchor text. A **bound**
|
|
1768
|
+
text prop is never flagged either, and `social_item` is icon-only by construction
|
|
1769
|
+
and is never checked at all.
|
|
1770
|
+
|
|
1771
|
+
#### `generic-alt-text` — content · info
|
|
1772
|
+
|
|
1773
|
+
**Why it matters.** Alt text like "Bild" or "Logo" names the MEDIUM, not the
|
|
1774
|
+
content: it passes an automated "has alt" check while telling a screen-reader
|
|
1775
|
+
user and an image crawler exactly nothing.
|
|
1776
|
+
**How to fix.** `element_update_settings` with `props.image_alt` describing what
|
|
1777
|
+
the image actually shows ("SunWest Büro in Manresa").
|
|
1778
|
+
**Severity.** Info, and strictly disjoint from `images-missing-alt` (which owns
|
|
1779
|
+
the EMPTY case). Only an **exact match** against the generic list (`image`,
|
|
1780
|
+
`photo`, `picture`, `logo`, `icon`, `bild`, `foto`, `grafik`) fires, so
|
|
1781
|
+
`"ACME Logo"` is already a good alt text and is never flagged. Bound alt/image
|
|
1782
|
+
props are exempt.
|
|
1783
|
+
|
|
1784
|
+
#### `long-heading` — structure · info
|
|
1785
|
+
|
|
1786
|
+
**Why it matters.** A heading is a label, not a sentence. Long ones dilute the
|
|
1787
|
+
keyword focus, get truncated in search results and wrap badly on mobile.
|
|
1788
|
+
**How to fix.** `element_update_settings` shortening `props.content` (on
|
|
1789
|
+
`headline`) or `props.title` (on `panel`, `alert`, `overlay`) and moving the
|
|
1790
|
+
detail into the body text below it. The locator carries the measured `length`.
|
|
1791
|
+
**Severity.** Info — a long heading is a quality signal, never a defect. The
|
|
1792
|
+
threshold is **70 characters**, measured multibyte-safely over the
|
|
1793
|
+
markup-stripped text, on unbound static text only. Scope is deliberately narrow:
|
|
1794
|
+
`headline` plus the standalone title carriers (`alert`, `overlay`, `panel`).
|
|
1795
|
+
Multi-Items containers keep their title text on the paired `*_item` children and
|
|
1796
|
+
are not measured — do not claim full heading-length coverage.
|
|
1797
|
+
|
|
1798
|
+
`anchor-text-quality`, `generic-alt-text` and `long-heading` are all `per-page`
|
|
1799
|
+
checks, so they also ride in the `quick_check` block on `page_publish` /
|
|
1800
|
+
`page_save` alongside the other per-page codes.
|
|
1801
|
+
|
|
1802
|
+
### The report contract — how to present an audit
|
|
1803
|
+
|
|
1804
|
+
The raw response is a machine payload. What you hand the user follows this
|
|
1805
|
+
shape every time, so two audits of two sites read the same way:
|
|
1806
|
+
|
|
1807
|
+
1. **Lead with the scope.** When `scope: "single"`, name the page the audit
|
|
1808
|
+
actually ran on from `resolved_target` — label, `template_id`, and
|
|
1809
|
+
`matched_by` when the address was fuzzy ("`special: "home"` resolved to
|
|
1810
|
+
*Startseite* (`page:4816`)"). When the response has no `scope`, it was a site
|
|
1811
|
+
sweep: lead with `scanned.templates` and the `summary` totals.
|
|
1812
|
+
2. **Order by severity: critical → warning → info.** The server already sorts
|
|
1813
|
+
that way; never re-rank findings by how easy they look to fix.
|
|
1814
|
+
3. **Group page → category → finding.** One block per affected page, its
|
|
1815
|
+
findings grouped by `category` (`structure`, `content`, `seo`, `links`), so
|
|
1816
|
+
the user can fix one page in one pass instead of hopping between pages per
|
|
1817
|
+
code.
|
|
1818
|
+
4. **Render every hit from its locator**, in this shape:
|
|
1819
|
+
|
|
1820
|
+
```
|
|
1821
|
+
element_name (breadcrumb) · heading_source · ×repeat · snippet → Fix: <tool + args>
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
Drop the parts a given finding does not carry (`heading_source` only exists on
|
|
1825
|
+
the heading checks; `×repeat` only when it is greater than 1; `snippet` only
|
|
1826
|
+
in single-page mode). Take `<tool + args>` from `fix_hint.tool` +
|
|
1827
|
+
`fix_hint.args_template` when the finding has one, and say "needs a decision"
|
|
1828
|
+
when it does not. The `path` is the write address — keep it for the tool call,
|
|
1829
|
+
but it is not the human-facing part of the line.
|
|
1830
|
+
5. **Name the element. Always.** Every reported finding names the element TYPE
|
|
1831
|
+
and its user-assigned name when one is set (`element_name`, falling back to
|
|
1832
|
+
`element_type`) plus the `breadcrumb` that locates it in the builder. A **bare
|
|
1833
|
+
path list is NOT acceptable output** — `/children/0/children/2` tells the user
|
|
1834
|
+
nothing about which box on their page to click. Add the page's `builder_url`
|
|
1835
|
+
as the "open in builder" link when the response carries one.
|
|
1836
|
+
6. **State what did and did not run.** On a site sweep, say the site-wide checks
|
|
1837
|
+
ran (`duplicate-layout`, `title-collision` compare pages against each other).
|
|
1838
|
+
On a single-page audit, say they did not — reuse the response's own `note`
|
|
1839
|
+
verbatim: `run pages_audit for site-wide checks`.
|
|
1840
|
+
7. **Be honest about truncation.** `hits_truncated: true` means the host's hit
|
|
1841
|
+
cap dropped hits, so the listed evidence for that page is incomplete — a
|
|
1842
|
+
single-page re-run returns up to 100 per check. `locators_stripped: true`
|
|
1843
|
+
means the locator prose was shed (or halved) to fit the response budget.
|
|
1844
|
+
They are not synonyms; do not report either as "that's all of them". A
|
|
1845
|
+
top-level `structured_truncated: true` says the WHOLE payload was over its
|
|
1846
|
+
char budget and the shed ladder ran, in this order: the DUPLICATE raw arrays
|
|
1847
|
+
first (every field of `alt_paths` / `anchors` / … also rides on the locator
|
|
1848
|
+
for the same hit, so nothing is lost), then the locator detail — halved
|
|
1849
|
+
before it is dropped — then the remaining raw evidence, then whole evidence
|
|
1850
|
+
pages (`pages_truncated`), so page ids and labels outlive the prose. Both
|
|
1851
|
+
transports shed in exactly that order; it is pinned by a contract test that
|
|
1852
|
+
derives the order from each implementation. Say so, and name the two ways
|
|
1853
|
+
back: raise `max_chars` (it bounds both legs on this tool), or
|
|
1854
|
+
re-run scoped to one page via `target`. `evidence_stripped: true` says the
|
|
1855
|
+
raw per-hit arrays (`paths`, `alt_paths`, `anchors`, …) were dropped from
|
|
1856
|
+
that entry while that page's id, label, hit count and `builder_url` were kept
|
|
1857
|
+
— so the answer stays actionable (name the page, say how many hits, offer the
|
|
1858
|
+
builder link) even when no per-hit detail survived. On a budget too small
|
|
1859
|
+
even for that floor, the plugin-served transport refuses with
|
|
1860
|
+
`max_chars_too_small` and tells you the minimum to ask for — it never returns
|
|
1861
|
+
a half-written payload.
|
|
1862
|
+
8. **Repeat the scope boundary.** Close with the response's `disclaimer` — meta
|
|
1863
|
+
title/description/OG live in the CMS SEO plugin and are not covered here — so
|
|
1864
|
+
nobody mistakes a clean audit for a clean SEO plugin.
|
|
1865
|
+
|
|
1866
|
+
Beyond that the prose is yours: group, summarise and prioritise however serves
|
|
1867
|
+
the user. Rules 2, 4, 5 and 6 are the parts that must not vary.
|
|
1868
|
+
|
|
1869
|
+
A single-page report that follows the contract reads like this:
|
|
1870
|
+
|
|
1871
|
+
```
|
|
1872
|
+
Audited: Startseite (page:4816) — matched by special:"home"
|
|
1873
|
+
2 findings — 0 critical / 2 warning / 0 info
|
|
1874
|
+
Site-wide checks (duplicate-layout, title-collision) did NOT run on a single
|
|
1875
|
+
page: run pages_audit for site-wide checks.
|
|
1876
|
+
|
|
1877
|
+
WARNING · structure · multiple-h1 — page renders 2 h1s [open in builder]
|
|
1878
|
+
• Willkommen bei SunWest (Hero ▸ Headline) · headline
|
|
1879
|
+
→ keep this one as the page's h1
|
|
1880
|
+
• Unsere Standorte (Karten ▸ Panel "Standorte") · panel-title · "Unsere Standorte"
|
|
1881
|
+
→ Fix: element_update_settings({ template_id: "page:4816",
|
|
1882
|
+
element_path: "/children/1/children/0", props: { title_element: "h2" } })
|
|
1883
|
+
Why: exactly one h1 anchors the page's topic; several dilute it.
|
|
1884
|
+
|
|
1885
|
+
WARNING · content · images-missing-alt — 1 image without alt text
|
|
1886
|
+
• Team photo (Über uns ▸ Grid ▸ Grid Item) · prop image_alt
|
|
1887
|
+
→ Fix: element_update_settings({ …, props: { image_alt: "<what it shows>" } })
|
|
1888
|
+
Why: an empty alt makes the image invisible to screen readers and image search.
|
|
1889
|
+
|
|
1890
|
+
Meta title/description/OG live in your CMS SEO plugin — not covered here.
|
|
1891
|
+
```
|
|
1892
|
+
|
|
1893
|
+
**Worked example (tool-call snippet):**
|
|
1894
|
+
|
|
1895
|
+
```jsonc
|
|
1896
|
+
// Step 1. Sweep the site for SEO + structure issues.
|
|
1897
|
+
yootheme_builder_advanced_read({
|
|
1898
|
+
tool: "yootheme_builder_pages_audit",
|
|
1899
|
+
arguments: { checks: ["seo", "structure"] }
|
|
1900
|
+
})
|
|
1901
|
+
// A finding: { code: "missing-h1", category: "structure", severity: "warning",
|
|
1902
|
+
// count: 3, pages: [{ id: "page:42", label: "About" }, ...],
|
|
1903
|
+
// fix_hint: { tool: "element_update_settings",
|
|
1904
|
+
// args_template: { props: { title_element: "h1" } }, note: "..." } }
|
|
1905
|
+
|
|
1906
|
+
// Step 3. Drill into the flagged page.
|
|
1907
|
+
yootheme_builder_template_summary({ template_id: "page:42" })
|
|
1908
|
+
// heading_levels: { h1: 0, h2: 2, h3: 5, div: 1 }, images_missing_alt: 2
|
|
1909
|
+
|
|
1910
|
+
// Step 4. Fix that ONE page via the write tool the fix_hint named.
|
|
1911
|
+
yootheme_builder_element_update_settings({
|
|
1912
|
+
template_id: "page:42",
|
|
1913
|
+
element_path: "/children/0/children/0",
|
|
1914
|
+
props: { title_element: "h1" },
|
|
1915
|
+
etag: "abc123"
|
|
1916
|
+
})
|
|
1917
|
+
|
|
1918
|
+
// Step 5. Republish; quick_check re-confirms this page is clean.
|
|
1919
|
+
yootheme_builder_page_publish({ template_id: "page:42", etag: "def456" })
|
|
1920
|
+
// Response: { ..., quick_check: { clean: true } }
|
|
1921
|
+
```
|
|
1922
|
+
|
|
1923
|
+
**Success criterion:** A re-run of `pages_audit` (or the `quick_check`
|
|
1924
|
+
block on the next publish) no longer lists the fixed finding for that
|
|
1925
|
+
page, and the response's `summary.critical`/`warning`/`info` totals go
|
|
1926
|
+
down.
|
|
1927
|
+
|
|
1928
|
+
---
|
|
1929
|
+
|
|
1930
|
+
## When something doesn't fit one of these 6 workflows
|
|
1242
1931
|
|
|
1243
1932
|
- **Move an element** (intra-template reorder/reparent): use
|
|
1244
1933
|
`yootheme_builder_element_move({ template_id, element_path,
|
|
@@ -2016,56 +2705,58 @@ perform.
|
|
|
2016
2705
|
|
|
2017
2706
|
<!-- TOOL-CATALOG:BEGIN -->
|
|
2018
2707
|
|
|
2019
|
-
**
|
|
2708
|
+
**49 catalogued tools** plus the `yootheme_builder_advanced` (write) + `yootheme_builder_advanced_read` (read-only) gateways = **51 reachable via `tools/list`** (37 L1 + 2 L3 + 2 gateways = 41 advertised; the gateways route to 10 additional advanced tools, bringing the total to 51 callable). Generated by `scripts/extract-tools.mjs` from the compiled `buildAllTools()` registry. Do not hand-edit this section; re-run `npm run build && node scripts/extract-tools.mjs` after changing tool definitions.
|
|
2020
2709
|
|
|
2021
2710
|
| Tool | Kind | Input keys | Description |
|
|
2022
2711
|
| --- | --- | --- | --- |
|
|
2023
|
-
| `yootheme_builder_clean_implode_directives` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Strips `props.source.props.*.implode` directives from an element binding. Returns audit log + new ETag. Idempotent
|
|
2712
|
+
| `yootheme_builder_clean_implode_directives` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Use when a Multi-Items element renders rows as ONE comma-joined string instead of N repeated items — typically stale `implode` directives left by a plugin upgrade (detect via yootheme_builder_inspect_multi_items_binding). Strips `props.source.props.*.implode` directives from an element binding. Returns audit log + new ETag. Idempotent (cleaned_count: 0 when nothing to remove) but destructive: the binding can't be restored without re-mapping. Ask the user to confirm, then call with `confirm: true`. Requires ETag. Operates on the default site unless site_id is provided. |
|
|
2024
2713
|
| `yootheme_builder_diagnose` | read+idempotent | `site_id` | Full diagnostic: /health + authenticated /etag probe. Returns site_url, home_url, plugin reachability, Bearer validity in one call. First call when you need to know where the site lives. For per-template URLs see pages_list. Operates on the default site unless site_id is provided. |
|
|
2025
|
-
| `yootheme_builder_element_add` | mutating | `children`, `element_type`, `etag`, `index`, `parent_path`, `position`, `props`, `site_id`, `template_id`, `type_name` | Add a new element to a template. Provide `parent_path` (or "" for root), `element_type` (e.g. "headline", "text", "grid"), and optional `props` / `children`. Appends by default; pass `index` (0 = first) to insert
|
|
2714
|
+
| `yootheme_builder_element_add` | mutating | `children`, `element_type`, `etag`, `index`, `name`, `parent_path`, `position`, `props`, `site_id`, `template_id`, `type_name` | Add a new element to a template. Provide `parent_path` (or "" for root), `element_type` (e.g. "headline", "text", "grid"), and optional `props` / `children`. Content/leaf elements may only be added inside a `column`; only `section` is valid at the root — call yootheme_builder_scaffold_section first for a valid column path. Appends by default; pass `index` (0 = first) to insert. Inserting renumbers later siblings — re-read paths via element_list after any structural write. Returns the new element's JSON-Pointer path. Requires ETag. Operates on the default site unless site_id is provided. |
|
|
2026
2715
|
| `yootheme_builder_element_bind_source` | idempotent | `arguments`, `bindingLevel`, `directives`, `element_path`, `etag`, `field`, `field_mappings`, `query_arguments`, `relation`, `site_id`, `source_id`, `source_name`, `template_id` | Binds a Builder source to an element (canonical shape: `source` top-level). Pass `field_mappings` in the SAME call; bindingLevel "item" binds a container's first *_item child. Leaf under an iterating *_item → CONSOLIDATE onto item slots (409 slot_collision if taken/not a slot); leaf under a #parent fragment → INHERIT (`__node_item__`); standalone renders empty. List on a grid_item with children → iterator_is_leaf_renderer. Map: bind `map_item`, map `location` to a "lat,lng" field or no markers. Requires ETag. Call directly. Operates on the default site unless site_id is provided. |
|
|
2027
|
-
| `yootheme_builder_element_clone` | mutating | `element_path`, `etag`, `site_id`, `template_id` | Clone an element as a sibling (same parent, immediately after the source). Returns the new element's path. Requires ETag. Operates on the default site unless site_id is provided. |
|
|
2028
|
-
| `yootheme_builder_element_delete` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | PERMANENTLY delete an element + children. Cannot be undone. Pass `confirm: true` (boolean) AND a fresh ETag. If `preview:true` returns despite confirm, check `details.received_confirm` — strings/numbers accepted, but boolean is canonical. Operates on the default site unless site_id is provided. |
|
|
2029
|
-
| `yootheme_builder_element_get` | read+idempotent | `element_path`, `site_id`, `template_id` | Get the full element object at a specific JSON-Pointer path, including props and children
|
|
2030
|
-
| `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Read the source binding attached to an element
|
|
2031
|
-
| `yootheme_builder_element_list` | read+idempotent | `cursor`, `depth`, `fields`, `limit`, `max_chars`, `root_path`, `site_id`, `template_id` | List elements in a template as a flat array with JSON-Pointer paths + types. Scope with `root_path`/`depth` for a subtree, paginate with `limit`/`cursor` for large templates. `fields[]` narrows each row; `max_chars` bounds the text output. Operates on the default site unless site_id is provided. |
|
|
2032
|
-
| `yootheme_builder_element_move` | idempotent | `element_path`, `etag`, `site_id`, `template_id`, `to_index`, `to_parent_path` | Move an element to a new parent + index in the tree. Useful for reordering or reparenting (e.g. moving a card from one grid column to another). Requires ETag. Operates on the default site unless site_id is provided. |
|
|
2033
|
-
| `yootheme_builder_element_set_source` | idempotent | `element_path`, `etag`, `site_id`, `source`, `source_extended`, `template_id` | Surgically SET a precomposed `source` (+ optional `source_extended.props`) on ONE element at a pointer, ETag-locked
|
|
2034
|
-
| `yootheme_builder_element_type_get_schema` | read+idempotent | `element_type`, `site_id`, `type_name` | Fetch a type's prop schema before
|
|
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. |
|
|
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. |
|
|
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. |
|
|
2038
|
-
| `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current ETag (state revision) for the YOOtheme builder. Returns sha256+revision string
|
|
2716
|
+
| `yootheme_builder_element_clone` | mutating | `element_path`, `etag`, `site_id`, `template_id` | Clone an element as a sibling (same parent, immediately after the source). Returns the new element's path. Cloning renumbers later siblings — re-read paths via element_list after any structural write. Requires ETag. Operates on the default site unless site_id is provided. |
|
|
2717
|
+
| `yootheme_builder_element_delete` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | PERMANENTLY delete an element + children. Cannot be undone. Pass `confirm: true` (boolean) AND a fresh ETag. Deleting renumbers later siblings — re-read paths via element_list after any structural write. If `preview:true` returns despite confirm, check `details.received_confirm` — strings/numbers accepted, but boolean is canonical. Operates on the default site unless site_id is provided. |
|
|
2718
|
+
| `yootheme_builder_element_get` | read+idempotent | `element_path`, `fields`, `max_chars`, `site_id`, `template_id` | Get the full element object at a specific JSON-Pointer path, including props and a `children_count` (NOT the children themselves — use yootheme_builder_element_list with root_path to enumerate children). `fields[]` narrows the returned object; `max_chars` bounds the text output. Address by template_id only — resolve a URL/post first via pages_resolve. Operates on the default site unless site_id is provided. |
|
|
2719
|
+
| `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Read the source binding attached to an element. Always returns {template_id, element_path, binding, has_binding, field_mapping_count, warning?}: `binding` is the raw source binding (bound source name, field-mappings, query args/directives); `has_binding` is false and `field_mapping_count` 0 when the element is not bound. Operates on the default site unless site_id is provided. |
|
|
2720
|
+
| `yootheme_builder_element_list` | read+idempotent | `cursor`, `depth`, `fields`, `limit`, `max_chars`, `root_path`, `site_id`, `template_id` | List elements in a template as a flat array with JSON-Pointer paths + types + `label` (the builder-assigned name; "" when unset — not that the type lacks one). Scope with `root_path`/`depth` for a subtree, paginate with `limit`/`cursor` for large templates. `fields[]` narrows each row; `max_chars` bounds the text output. template_id only — resolve a URL/post first via pages_resolve. Operates on the default site unless site_id is provided. |
|
|
2721
|
+
| `yootheme_builder_element_move` | idempotent | `element_path`, `etag`, `site_id`, `template_id`, `to_index`, `to_parent_path` | Move an element to a new parent + index in the tree. Useful for reordering or reparenting (e.g. moving a card from one grid column to another). Moving renumbers later siblings — re-read paths via element_list after any structural write. Requires ETag. Operates on the default site unless site_id is provided. |
|
|
2722
|
+
| `yootheme_builder_element_set_source` | idempotent | `element_path`, `etag`, `site_id`, `source`, `source_extended`, `template_id` | Surgically SET a precomposed `source` (+ optional `source_extended.props`) on ONE element at a pointer, ETag-locked (blast-radius 0). Use this — not element_bind_source — when you have the exact source shape OR need `source_extended.props`, the native Multi-Items item-mapping carrier bind_source cannot write. E.g. FAQ accordion scoped to term: query.name="categoryFaqs.taxonomyCategoryFaq", field={name:"faq"}, source_extended.props→#node. Element-prop keys the type does not render echo back as `unknown_prop_keys` (non-fatal). ETag req. Operates on the default site unless site_id is provided. |
|
|
2723
|
+
| `yootheme_builder_element_type_get_schema` | read+idempotent | `element_type`, `fields`, `max_chars`, `site_id`, `type_name` | Fetch a type's prop schema before element_add/element_update_settings: unknown prop keys are hard-rejected (error lists valid_prop_keys + did_you_mean). Returns each field as {name,type,label?,enum?,value_hint?,group?}, field_count, and a binding_contract for container/item types. Read value_hint+enum first — some defaults are SEO-fatal (headline.title_element→h1); checkbox = boolean true/false; group:"runtime-accepted" = honoured at render but hidden in the builder UI. Bound size with fields[]/max_chars. Operates on the default site unless site_id is provided. |
|
|
2724
|
+
| `yootheme_builder_element_types_list` | read+idempotent | `fields`, `site_id` | List element types registered on this site (built-ins + YOOessentials/uEssentials extras). Names feed `element_type` of element_add. Pass `fields[]` to narrow each row. Keywords: "slider"/"carousel" → `slideshow`/`panel-slider`/`overlay-slider`. Prop keys differ per element (grid_item `title` vs headline `content`) — use element_type_get_schema before element_add/bind. Container rows carry `requires_child_type` (grid→grid_item); `has_children`/`has_children_support` are aliases (same value). Operates on the default site unless site_id is provided. |
|
|
2725
|
+
| `yootheme_builder_element_unbind_source` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Remove the source binding from an element. Clears the element's canonical top-level `source` key AND the legacy `props.source`. Destructive in the sense that it may break dynamic-content rendering — always ask the user to confirm. Requires ETag. Operates on the default site unless site_id is provided. |
|
|
2726
|
+
| `yootheme_builder_element_update_settings` | idempotent | `element_path`, `etag`, `merge`, `name`, `props`, `site_id`, `template_id`, `unset` | Update `props` on an element. Default replaces all props; pass `merge:true` for server-side deep-merge (only request keys overwritten, others survive — avoids read-modify-write races). Requires ETag. Pass `unset:["key",…]` to reset props to their YOOtheme default by removing them. Operates on the default site unless site_id is provided. |
|
|
2727
|
+
| `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current ETag (state revision) for the YOOtheme builder. Returns a sha256+revision string for optimistic locking. This ETag is SITE-WIDE: it covers the ENTIRE builder state, so ANY write to ANY template (by any client) bumps it — a 412 means the site state advanced, not necessarily that YOUR template changed. It is the SAME token the read tools (page_get_layout, element_list) return in their `etag`. Pass it back as `etag` on any write tool; on 412, re-read and retry. Operates on the default site unless site_id is provided. |
|
|
2039
2728
|
| `yootheme_builder_health` | read+idempotent | `site_id` | Check plugin installed/reachable. Returns plugin version, YT Pro version, REST endpoints. Authenticated payload adds site_url + home_url for deep-linking. See yootheme_builder_diagnose for Bearer-validity + connectivity summary. Operates on the default site unless site_id is provided. |
|
|
2040
|
-
| `yootheme_builder_image_import` | idempotent | `site_id`, `url` | Sideload a
|
|
2729
|
+
| `yootheme_builder_image_import` | idempotent | `site_id`, `url` | Sideload a photo into the site's own Media Library. Accepts ANY public image URL — it does NOT require a Library API key, so it still works when image_search is unavailable (typically pass a `full_url` from yootheme_builder_image_search). Returns `{imported:true, local_url}` where local_url is a SITE-RELATIVE path (e.g. /wp-content/uploads/...). Bind it via element_update_settings({props:{image: local_url}, merge:true}). A caught import failure returns `{imported:false, reason}`, never a 500. Operates on the default site unless site_id is provided. |
|
|
2041
2730
|
| `yootheme_builder_image_search` | read+idempotent | `color`, `orientation`, `per_page`, `query`, `site_id`, `source` | Search real on-topic photos (Unsplash/Pexels) or YOOtheme Pro Images to fill layout image slots with REAL, on-brand photography instead of bare placeholders. Returns candidate rows with `full_url` (pass straight to yootheme_builder_image_import) plus dimensions/author/orientation. `total:0` usually means no Library API key is configured (the same theme apikey library_browse/insert use). Operates on the default site unless site_id is provided. |
|
|
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, …),
|
|
2731
|
+
| `yootheme_builder_inspect_multi_items_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Reports Multi-Items binding state: container/item pair (grid↔grid_item, slideshow↔slideshow_item, map↔map_item, …), the binding level (none\|container\|item), and a recommended_fix when it sits on the container. For map/map_item, a bound item without a `location` ("lat,lng") field is flagged as a blocking map_location_warning (renders 0 markers). A List on a grid_item with nested children → blocking iterator_is_leaf_renderer (children never render). If rows render as ONE comma-joined string, see clean_implode_directives. Operates on the default site unless site_id is provided. |
|
|
2043
2732
|
| `yootheme_builder_inspect_sublayout` | read+idempotent | `element_path`, `site_id`, `template_id` | Inspects a panel/grid_item `< SUBLAYOUT`: lists the `fragment` subtree bindable leaves (with JSON-Pointer paths to bind on — NOT the container) and the render-time inner grid. Flags `ungated_grid_prop` (blocking): grid props set but the gate (title_align=left / image_align=left\|right) unsatisfied → silent no-op. For container↔item binding see inspect_multi_items_binding. Operates on the default site unless site_id is provided. |
|
|
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
|
|
2733
|
+
| `yootheme_builder_layout_query` | read+idempotent | `cursor`, `fields`, `has_binding`, `limit`, `max_chars`, `max_depth`, `name_matches`, `site_id`, `template_id`, `type`, `under_section` | Find elements in a template by a structured filter (no expression language). AND-combine any of: `type` (one type or a list), `name_matches` (case-insensitive substring of the element name), `has_binding` (bound vs unbound), `max_depth` (cap descent — 0 = top-level), `under_section` (only nodes inside a named section). template_id only — resolve a URL/post first via pages_resolve. Returns matching rows {path, element_type, label, has_binding}; paginate with `limit`/`cursor`, narrow rows with `fields[]`, bound text with `max_chars`. Operates on the default site unless site_id is provided. |
|
|
2045
2734
|
| `yootheme_builder_library_browse` | read+idempotent | `cursor`, `element_type`, `has_effects`, `kind`, `limit`, `purpose`, `query`, `role`, `site_id`, `source`, `topic`, `type`, `website` | Browse the YOOtheme Pro Library catalog: pre-designed page LAYOUTS, element PRESETS, or purpose-classified SECTIONS (hero/features/cta/...). Filter with `kind` ("layout" default \| "element" \| "section"), `topic`, `type`/`role`, `element_type`, `purpose`/`has_effects` (sections only), `website`, `query`. Returns `{items, total, next_cursor}`; a "section" id is the composite "<layoutId>#<sectionIndex>" — feeds straight into library_get/library_insert. `total:0` usually means no Library API key configured. Operates on the default site unless site_id is provided. |
|
|
2735
|
+
| `yootheme_builder_library_delete` | destructive | `confirm`, `id`, `site_id`, `source` | Delete ONE saved entry from your "WootsUp AI Layouts" store (the isolated library yootheme_builder_library_save_as writes into) — clean up experimental sections/layouts the agent saved. Pass the `id` from a library_browse(source:"ai") row. `source` defaults to "ai", the ONLY deletable store; the curated library is read-only, so source:"catalog" returns a 403. IRREVERSIBLE — requires confirm:true. Returns `{deleted, id, deleted_count, source}`; an unknown id returns a 404. Advanced/gateway-only. Operates on the default site unless site_id is provided. |
|
|
2046
2736
|
| `yootheme_builder_library_get` | read+idempotent | `id`, `include_thumbnail`, `kind`, `site_id`, `source` | Fetch a SINGLE YOOtheme Pro Library layout or element preset by `id` — call after yootheme_builder_library_browse (items[].id feeds this). Returns `{id, kind, name, version, tree, sections, images_manifest_count}`: `tree` is the raw JSON; `sections` lists `tree.children` (layouts only, presets return `[]`), each `{index, name, path, element_count}`. `include_thumbnail:true` also returns an image block (costs tokens — see its own description). Read-only; does not insert/bind. Operates on the default site unless site_id is provided. |
|
|
2047
2737
|
| `yootheme_builder_library_insert` | mutating | `etag`, `id`, `images`, `kind`, `section_index`, `site_id`, `source`, `target` | Insert a YOOtheme Pro Library item (layout, section, or preset) onto a page — call after yootheme_builder_library_get/browse. `target`: new_page (creates a page), {template_id,element_path?} (splices onto the layout root, or a preset onto a Column), or sublayout (splices rows into a fragment). A preset (kind:"element") MUST target a Column — 422 otherwise; scaffold one first with yootheme_builder_scaffold_section. `images`: "placeholder" (default) or "curated" (downloads to Media Library). Optional `etag` locks (412 if stale). Operates on the default site unless site_id is provided. |
|
|
2048
2738
|
| `yootheme_builder_library_save_as` | mutating | `destination`, `name`, `site_id`, `source` | Save a page or subtree into a reusable library — the REVERSE of yootheme_builder_library_insert. `source`: {template_id, element_path?} (omit element_path to save the WHOLE page). `destination`: "ai" (default — your isolated "WootsUp AI Layouts", reusable via library_browse/insert source:"ai") or "native" (YOOtheme's built-in My Layouts). Returns `{library_id, name, storage, invalid_source_possible}` (the last flags bound sources that may need re-binding after reload). Advanced/gateway-only. Operates on the default site unless site_id is provided. |
|
|
2049
2739
|
| `yootheme_builder_local_content_read` | read+idempotent | `fields`, `include_children`, `kind`, `limit`, `max_chars`, `offset`, `parent_id`, `search`, `site_id`, `status`, `taxonomy`, `type` | Read native records for one `kind`: entities (posts/pages/CPT/articles), terms (TERMS — ids that archive-scope a Grid), users, fields (ACF/com_fields defs), menus, or media. Published-only + paginated: {items,total,next_offset}; pass next_offset back as `offset`. Archive-scoping flow: read {kind:"terms",taxonomy:"category"} → pick a term id → bind a source, pass query_arguments:{id:<term_id>}. CALL local_content_types FIRST for valid type/taxonomy. E.g. {kind:"entities",type:"product",search:"shoe"}. Slim with `fields[]`. Operates on the default site unless site_id is provided. |
|
|
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
|
|
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
|
|
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
|
|
2740
|
+
| `yootheme_builder_local_content_types` | read+idempotent | `fields`, `kind`, `max_chars`, `site_id` | Discover the site's NATIVE content landscape — post types/articles, taxonomies and custom-field groups (ACF/com_fields), native to WP/Joomla (no API Mapper). Returns {content_types, taxonomies, field_groups, capabilities}; rows carry `bindable` + `source_name` (null for native types — bind those with element_bind_source by the content-type `name`, e.g. "post"). CALL FIRST, then local_content_read for records. Narrow with `kind`, slim with `fields[]`. E.g. {kind:"content_types"}. Operates on the default site unless site_id is provided. |
|
|
2741
|
+
| `yootheme_builder_page_dynamize` | mutating | `conditions`, `dry_run`, `element_path`, `etag`, `leaf_map`, `list_source_name`, `page`, `remove_leaves`, `section_name`, `site_id`, `source_map`, `template_id` | One-call way to dynamize a designed section: copies the layout byte-identically and swaps only what you map. Use INSTEAD of hand-editing JSON. Give a REFERENCE (section_name from template_summary, or element_path) + list_source_name + a leaf_map (original leaf field-name → flat flow field; #parent + tuned props kept). A section with several source blocks dynamizes in ONE call via source_map ({match, source_name, leaf_map?}[]). Sub-query panels: key by "<subquery>.<leaf>". Returns page + `summary`. Preview with dry_run:true. Operates on the default site unless site_id is provided. |
|
|
2742
|
+
| `yootheme_builder_page_get_layout` | read+idempotent | `cursor`, `fields`, `flat`, `force`, `max_chars`, `mode`, `post_id`, `site_id`, `template_id`, `url` | Get the layout tree for one template — address it by `template_id` (from pages_list), `post_id`, or `url`. `mode:"bindings_only"` for dynamize/bind; `mode:"full"` (default) is nested `{layout, etag}`, `flat:true` a depth-first array. `max_chars` bounds the text; a clipped read returns `next_cursor` to page the rest. For a site-wide SEO/structure/content/links audit (h1, image alt, thin pages, dead links) do NOT loop this tool page-by-page — call yootheme_builder_pages_audit (one sweep via yootheme_builder_advanced_read). Operates on the default site unless site_id is provided. |
|
|
2053
2743
|
| `yootheme_builder_page_get_schema` | read+idempotent | `fields`, `max_chars`, `post_id`, `site_id`, `template_id`, `url` | Get the flat schema for a template — nodes with their JSON-Pointer paths and element types. Lighter than page_get_layout, enough to locate elements before editing. Address by `template_id` (pages_list), `post_id`, or `url`. `fields[]` slims each node; `max_chars` bounds output. Operates on the default site unless site_id is provided. |
|
|
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.
|
|
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
|
|
2744
|
+
| `yootheme_builder_page_publish` | idempotent | `etag`, `quick_check`, `site_id`, `template_id` | Publish a template — persist state, flush YT + WP caches, snapshot the published-state ETag. YOOtheme templates already publish on save, so this is a cache-flush + state-snapshot op. Neither publish nor save changes the CMS post status — a draft page stays invisible to visitors; set status at creation via pages_create/library_insert. ETag optional — when provided, 412 on conflict; when omitted, last-write-wins. Operates on the default site unless site_id is provided. |
|
|
2745
|
+
| `yootheme_builder_page_save` | idempotent | `dry_run`, `etag`, `layout`, `quick_check`, `site_id`, `template_id` | Re-run save-transforms + flush caches. Optionally REPLACE the template tree by passing the INNER {type:"layout",children:[...]} tree as `layout` — page_get_layout's `.layout` child, NOT the whole response it returns (rejected 422, would blank the page). Validated + ETag-locked; nothing persists on reject. Does NOT change CMS post status — a draft stays invisible to visitors; set status via pages_create. `dry_run:true` preflights; success carries `previous_layout` for rollback. ETag optional (412 on conflict; else last-write-wins). Operates on the default site unless site_id is provided. |
|
|
2746
|
+
| `yootheme_builder_pages_audit` | read+idempotent | `checks`, `max_chars`, `site_id`, `stale_after_months`, `target`, `thin_content_threshold`, `types` | One-call site-wide SEO/structure/content/links audit — severity-sorted findings with fix_hints (each a props FRAGMENT to merge, not a ready-made call — supply template_id + path from the finding locator). Covers h1/headings, thin pages, image alt, duplicate layouts, title collisions, non-pretty URLs, dead anchors. A SITE-wide sweep caps hits/page and drops per-hit content_snippet; scope to ONE page via `target` for the full set (cap 100). Filter with `types`/`checks`. Disclaimer: SEO-plugin meta (title/description/OG) NOT covered. Operates on the default site unless site_id is provided. |
|
|
2056
2747
|
| `yootheme_builder_pages_create` | idempotent | `dry_run`, `layout`, `menu_item`, `published`, `site_id`, `slug`, `status`, `title` | Create a page (WordPress) or article (Joomla 5/6) with a YOOtheme per-page layout. Required: title. Optional: slug, status (default draft), published (default false → live on true: WP publish / Joomla state 1), layout (JSON), menu_item (Joomla SEF route), dry_run (preflight, no persist). Returns page_id (WP) / article_id (Joomla), slug, template_id (page:{id} or article:{id}), etag, warnings. Joomla: a published article has NO SEF route until a menu item points at it — pass menu_item, else frontend_url may 404. Operates on the default site unless site_id is provided. |
|
|
2057
2748
|
| `yootheme_builder_pages_delete` | destructive | `confirm`, `force`, `post_id`, `site_id` | Delete a page (WordPress) or article (Joomla 5/6) with its YOOtheme per-page layout. Requires confirm:true (gate against accidental deletion). Default: trash (recoverable). force:true: permanent delete. Refuses non-page/article types with 403 (Layer 9 defense). Returns deleted:true, page_id (WP) / article_id (Joomla), force_deleted, bumped etag. Operates on the default site unless site_id is provided. |
|
|
2058
2749
|
| `yootheme_builder_pages_list` | read+idempotent | `cursor`, `fields`, `label_contains`, `limit`, `max_chars`, `site_id` | List pages, templates, and layouts in the YOOtheme Pro builder (id, label, type, elements_count, frontend_url, builder_url, serves_context_kind, serves_context_ids per row). CALL THIS FIRST to discover template IDs before page_get_layout / element_list / page_get_schema. Paginates: up to `limit` rows (default 50) plus `total` and a `next_cursor` (pass it back as `cursor`) when more remain. Filter with `label_contains`. Slim rows with `fields:["id","label"]`; bound output with `max_chars`. Operates on the default site unless site_id is provided. |
|
|
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. |
|
|
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
|
|
2061
|
-
| `yootheme_builder_render_preview` | read+idempotent | `expect`, `site_id`, `template_id`, `url` | Render-check a saved template
|
|
2062
|
-
| `yootheme_builder_scaffold_section` | mutating | `column_widths`, `columns`, `etag`, `site_id`, `template_id` | Create a VALID section → row → column hull in ONE
|
|
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.) |
|
|
2750
|
+
| `yootheme_builder_pages_resolve` | read+idempotent | `site_id`, `target`, `url` | Resolve a public page/article/archive URL to the template_id that renders it — paste a URL, get the layout id. Pretty/SEF permalinks resolve on WordPress AND Joomla (incl. deep sub-category paths), not only raw ?option= URLs. A singular page → its per-page layout; a category/tag/CPT/author archive → the governing THEME TEMPLATE, and for a category/term the SPECIFICALLY-ASSIGNED template (honoring include-child-categories), not a generic one. Or a `target`. Returns {template_id, post_id, type, frontend_url, context, resolved_via}. Operates on the default site unless site_id is provided. |
|
|
2751
|
+
| `yootheme_builder_rename_source` | idempotent | `dry_run`, `etag`, `field_map`, `from`, `site_id`, `source_map`, `template_id`, `to` | Template-wide RENAME of a source / type / field name across EVERY binding in a template, in ONE ETag-locked write — preserving each binding's arguments, props and field_mappings. Use after a taxonomy/CPT rename (faq→help) breaks every bound layout. Pass `from`/`to` for one source, and/or `source_map`/`field_map` to rename a relation + its field tokens together; the dotted "<source>.<relation>" form is rewritten segment-by-segment. Returns {replaced_count, changed_paths, new_etag}. Preview with dry_run:true. ETag req. Operates on the default site unless site_id is provided. |
|
|
2752
|
+
| `yootheme_builder_render_preview` | read+idempotent | `expect`, `post_id`, `site_id`, `template_id`, `url` | Render-check a saved template — does it actually render, with how many items, or is it blank? Address by `template_id` (from pages_list), `post_id`, or `url` (resolved for you) — exactly one. `item_count` = items yielded by the template's dynamic/Multi-Items bindings (rendered repeat-source rows); 0 with empty:true means a binding returned nothing. Returns {rendered, item_count, empty, error?, cache_purge_hint}; with `expect`, also {matched, snippet}. Use AFTER a save/bind to catch a silently-empty page. Read-only. Operates on the default site unless site_id is provided. |
|
|
2753
|
+
| `yootheme_builder_scaffold_section` | mutating | `column_widths`, `columns`, `etag`, `site_id`, `template_id` | Create a VALID section → row → column hull in ONE call; returns the column JSON-Pointer path(s) for content. The correct way to start a layout block: only `section` may sit at the layout root; a bare section accepts `row`, a bare row accepts `column`; leaf/content elements are rejected anywhere except inside a `column`. Pass `columns` (1-12, default 1); columns get width tokens so they render side-by-side (hand-built columns default to full width), or `column_widths` (one token per column) for an asymmetric row. Requires ETag. Operates on the default site unless site_id is provided. |
|
|
2754
|
+
| `yootheme_builder_sites_list` | read+idempotent | `site_id` | List all sites configured in this multi-site MCP installation. Returns site_id + URL + platform (wordpress\|joomla) + default flag per row. CALL THIS FIRST when working with a fresh MCP connection to discover available site_ids before targeting one with any other tool. Read-only, no REST calls. Keywords: list sites, list connections, list installations, discover site_id, available sites, configured sites, what sites exist, multi-site index. The `default_site_id` is the write target when a call omits site_id. (site_id is accepted for schema-uniformity but ignored by this tool.) |
|
|
2064
2755
|
| `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. |
|
|
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).
|
|
2756
|
+
| `yootheme_builder_source_get_schema` | read+idempotent | `max_chars`, `name`, `site_id` | Introspect ONE data source: its query methods + arguments + RELATIONS (e.g. Term→Posts: categoryFaqs.taxonomyCategoryFaq → field "faq") + a provides_archive_context flag (id:0 = current archive term, auto-scoped). SOME arguments carry `enum_values` (the allowed values); when an arg has none it is a free string — pick from enum_values when present, never invent values. CALL BEFORE binding on an archive/category template. Keywords: source schema, relations, archive context, enum values. Get `name` from yootheme_builder_sources_list. Operates on the default site unless site_id is provided. |
|
|
2066
2757
|
| `yootheme_builder_sources_list` | read+idempotent | `cursor`, `fields`, `group`, `kind`, `limit`, `max_chars`, `name_contains`, `site_id` | List all data sources, feeds, and dynamic content sources available in the YOOtheme Pro builder. Returns name + label + origin (apimapper / wordpress / joomla / essentials) per source. CALL THIS BEFORE binding any element to a data source. Then bind directly with yootheme_builder_element_bind_source. Keywords: list sources, list feeds, list bindings, dynamic content, what sources exist. Paginates (`limit`/`cursor`/`total`); scope with `group`/`kind`/`name_contains`; slim with `fields[]`; bound output with `max_chars`. Operates on the default site unless site_id is provided. |
|
|
2067
|
-
| `yootheme_builder_template_set_type` | idempotent | `etag`, `site_id`, `template_id`, `type` | Reassign which archive/single a YOOtheme THEME template applies to — set its `type` condition (the value pages_list shows) in ONE ETag-locked write. Use after a taxonomy/CPT rename orphans a template (e.g. re-type "taxonomy-category_faq" → "taxonomy-category_help"
|
|
2068
|
-
| `yootheme_builder_template_summary` | read+idempotent | `site_id`, `template_id` | Token-efficient template overview: element counts by type, binding count, max nesting depth, and named landmark sections — computed server-side in one call. Use this to grasp a large template before pulling element_list or page_get_layout. Example: yootheme_builder_template_summary({ template_id: "default" }). Operates on the default site unless site_id is provided. |
|
|
2758
|
+
| `yootheme_builder_template_set_type` | idempotent | `dry_run`, `etag`, `site_id`, `template_id`, `type` | Reassign which archive/single a YOOtheme THEME template applies to — set its `type` condition (the value pages_list shows) in ONE ETag-locked write. Use after a taxonomy/CPT rename orphans a template (e.g. re-type "taxonomy-category_faq" → "taxonomy-category_help"). Returns {type, previous_type, new_etag}. Site-wide + immediate — preview with dry_run:true, reversible via previous_type. THEME templates only: page:/article: is refused (422). ETag req. Operates on the default site unless site_id is provided. |
|
|
2759
|
+
| `yootheme_builder_template_summary` | read+idempotent | `site_id`, `template_id` | Token-efficient template overview: element counts by type, binding count, max nesting depth, and named landmark sections — computed server-side in one call. Use this to grasp a large template before pulling element_list or page_get_layout. template_id only — resolve a URL/post first via pages_resolve. Example: yootheme_builder_template_summary({ template_id: "default" }). Operates on the default site unless site_id is provided. |
|
|
2069
2760
|
| `yootheme_builder_use_site` | idempotent | `probe`, `site_id` | Switch the ACTIVE site for this multi-site session. After this call, every tool that omits `site_id` targets the site you picked here. NOTE: this default is PROCESS-WIDE — concurrent clients sharing this MCP server inherit it; pass `site_id` per call for isolation between parallel agents. Runs a /health + /etag probe by default; pass `probe: false` to switch offline. A degraded site is a soft warning (the switch still happens). `site_id` is REQUIRED. Keywords: switch/change/set active/target site, select installation, use profile. Use yootheme_builder_sites_list first to find IDs. |
|
|
2070
2761
|
|
|
2071
2762
|
<!-- TOOL-CATALOG:END -->
|