@wootsup/yt-builder-mcp 1.11.0 → 2.0.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 +131 -33
- package/bin/node-floor.d.ts +28 -0
- package/bin/node-floor.js +123 -0
- package/bin/yt-builder-mcp.js +52 -11
- package/dist/auth.d.ts +3 -121
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +40 -170
- package/dist/auth.js.map +1 -1
- package/dist/catalog/build-catalog.d.ts +4 -49
- package/dist/catalog/build-catalog.d.ts.map +1 -1
- package/dist/catalog/build-catalog.js +15 -126
- package/dist/catalog/build-catalog.js.map +1 -1
- package/dist/catalog/json-schema.d.ts +3 -0
- package/dist/catalog/json-schema.d.ts.map +1 -0
- package/dist/catalog/json-schema.js +68 -0
- package/dist/catalog/json-schema.js.map +1 -0
- package/dist/catalog/output-shapes.d.ts +6 -0
- package/dist/catalog/output-shapes.d.ts.map +1 -0
- package/dist/catalog/output-shapes.js +89 -0
- package/dist/catalog/output-shapes.js.map +1 -0
- package/dist/catalog/tool-catalog-markdown.d.ts +1 -37
- package/dist/catalog/tool-catalog-markdown.d.ts.map +1 -1
- package/dist/catalog/tool-catalog-markdown.js +9 -41
- package/dist/catalog/tool-catalog-markdown.js.map +1 -1
- package/dist/cli/doctor-command.d.ts +2 -7
- package/dist/cli/doctor-command.d.ts.map +1 -1
- package/dist/cli/doctor-command.js +33 -54
- package/dist/cli/doctor-command.js.map +1 -1
- package/dist/cli/invocation-decision.d.ts +4 -52
- package/dist/cli/invocation-decision.d.ts.map +1 -1
- package/dist/cli/invocation-decision.js +45 -54
- package/dist/cli/invocation-decision.js.map +1 -1
- package/dist/client.d.ts +3 -107
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +34 -115
- package/dist/client.js.map +1 -1
- package/dist/clients/claude-code.d.ts +0 -15
- package/dist/clients/claude-code.d.ts.map +1 -1
- package/dist/clients/claude-code.js +0 -22
- package/dist/clients/claude-code.js.map +1 -1
- package/dist/clients/claude-desktop.d.ts +0 -16
- package/dist/clients/claude-desktop.d.ts.map +1 -1
- package/dist/clients/claude-desktop.js +0 -18
- package/dist/clients/claude-desktop.js.map +1 -1
- package/dist/clients/cline.d.ts +0 -23
- package/dist/clients/cline.d.ts.map +1 -1
- package/dist/clients/cline.js +0 -33
- package/dist/clients/cline.js.map +1 -1
- package/dist/clients/codex-cli.d.ts +0 -39
- package/dist/clients/codex-cli.d.ts.map +1 -1
- package/dist/clients/codex-cli.js +0 -137
- package/dist/clients/codex-cli.js.map +1 -1
- package/dist/clients/continue.d.ts +0 -10
- package/dist/clients/continue.d.ts.map +1 -1
- package/dist/clients/continue.js +0 -26
- package/dist/clients/continue.js.map +1 -1
- package/dist/clients/cursor.d.ts +0 -9
- package/dist/clients/cursor.d.ts.map +1 -1
- package/dist/clients/cursor.js +0 -11
- package/dist/clients/cursor.js.map +1 -1
- package/dist/clients/gemini-cli.d.ts +0 -15
- package/dist/clients/gemini-cli.d.ts.map +1 -1
- package/dist/clients/gemini-cli.js +0 -17
- package/dist/clients/gemini-cli.js.map +1 -1
- package/dist/clients/home.d.ts +0 -12
- package/dist/clients/home.d.ts.map +1 -1
- package/dist/clients/home.js +0 -12
- package/dist/clients/home.js.map +1 -1
- package/dist/clients/index.d.ts +0 -150
- package/dist/clients/index.d.ts.map +1 -1
- package/dist/clients/index.js +0 -112
- package/dist/clients/index.js.map +1 -1
- package/dist/clients/roo-code.d.ts +0 -20
- package/dist/clients/roo-code.d.ts.map +1 -1
- package/dist/clients/roo-code.js +0 -22
- package/dist/clients/roo-code.js.map +1 -1
- package/dist/clients/zed.d.ts +0 -9
- package/dist/clients/zed.d.ts.map +1 -1
- package/dist/clients/zed.js +0 -18
- package/dist/clients/zed.js.map +1 -1
- package/dist/compat-floor.d.ts +35 -0
- package/dist/compat-floor.d.ts.map +1 -0
- package/dist/compat-floor.js +84 -0
- package/dist/compat-floor.js.map +1 -0
- package/dist/compat-probe.d.ts +6 -0
- package/dist/compat-probe.d.ts.map +1 -0
- package/dist/compat-probe.js +20 -0
- package/dist/compat-probe.js.map +1 -0
- package/dist/diagnostics/ca-reexec.d.ts +0 -30
- package/dist/diagnostics/ca-reexec.d.ts.map +1 -1
- package/dist/diagnostics/ca-reexec.js +0 -62
- package/dist/diagnostics/ca-reexec.js.map +1 -1
- package/dist/diagnostics/ca-trust.d.ts +0 -22
- package/dist/diagnostics/ca-trust.d.ts.map +1 -1
- package/dist/diagnostics/ca-trust.js +3 -42
- package/dist/diagnostics/ca-trust.js.map +1 -1
- package/dist/diagnostics/diagnose-network-error.d.ts +1 -38
- package/dist/diagnostics/diagnose-network-error.d.ts.map +1 -1
- package/dist/diagnostics/diagnose-network-error.js +26 -37
- package/dist/diagnostics/diagnose-network-error.js.map +1 -1
- package/dist/diagnostics/doctor.d.ts +13 -65
- package/dist/diagnostics/doctor.d.ts.map +1 -1
- package/dist/diagnostics/doctor.js +46 -70
- package/dist/diagnostics/doctor.js.map +1 -1
- package/dist/diagnostics/startup-self-probe.d.ts +3 -25
- package/dist/diagnostics/startup-self-probe.d.ts.map +1 -1
- package/dist/diagnostics/startup-self-probe.js +11 -48
- package/dist/diagnostics/startup-self-probe.js.map +1 -1
- package/dist/errors/hints.d.ts +0 -44
- package/dist/errors/hints.d.ts.map +1 -1
- package/dist/errors/hints.js +0 -32
- package/dist/errors/hints.js.map +1 -1
- package/dist/errors/mask.d.ts +0 -31
- package/dist/errors/mask.d.ts.map +1 -1
- package/dist/errors/mask.js +0 -32
- package/dist/errors/mask.js.map +1 -1
- package/dist/errors/redact-credentials.d.ts +2 -0
- package/dist/errors/redact-credentials.d.ts.map +1 -0
- package/dist/errors/redact-credentials.js +13 -0
- package/dist/errors/redact-credentials.js.map +1 -0
- package/dist/errors/sanitize.d.ts +0 -29
- package/dist/errors/sanitize.d.ts.map +1 -1
- package/dist/errors/sanitize.js +0 -40
- package/dist/errors/sanitize.js.map +1 -1
- package/dist/errors.d.ts +6 -17
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -21
- package/dist/errors.js.map +1 -1
- package/dist/gateway/advanced-read-tool.d.ts +0 -32
- package/dist/gateway/advanced-read-tool.d.ts.map +1 -1
- package/dist/gateway/advanced-read-tool.js +43 -111
- package/dist/gateway/advanced-read-tool.js.map +1 -1
- package/dist/gateway/advanced-tool/discovery.d.ts +1 -12
- package/dist/gateway/advanced-tool/discovery.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/discovery.js +18 -16
- package/dist/gateway/advanced-tool/discovery.js.map +1 -1
- package/dist/gateway/advanced-tool/domains.d.ts +7 -66
- package/dist/gateway/advanced-tool/domains.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/domains.js +36 -80
- package/dist/gateway/advanced-tool/domains.js.map +1 -1
- package/dist/gateway/advanced-tool/execute.d.ts +0 -25
- package/dist/gateway/advanced-tool/execute.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/execute.js +13 -49
- package/dist/gateway/advanced-tool/execute.js.map +1 -1
- package/dist/gateway/advanced-tool/index.d.ts +1 -32
- package/dist/gateway/advanced-tool/index.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/index.js +1 -35
- package/dist/gateway/advanced-tool/index.js.map +1 -1
- package/dist/gateway/advanced-tool/register.d.ts +0 -14
- package/dist/gateway/advanced-tool/register.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/register.js +22 -58
- package/dist/gateway/advanced-tool/register.js.map +1 -1
- package/dist/gateway/advanced-tool.d.ts +0 -11
- package/dist/gateway/advanced-tool.d.ts.map +1 -1
- package/dist/gateway/advanced-tool.js +0 -11
- package/dist/gateway/advanced-tool.js.map +1 -1
- package/dist/gateway/capturing-server.d.ts +3 -88
- package/dist/gateway/capturing-server.d.ts.map +1 -1
- package/dist/gateway/capturing-server.js +0 -77
- package/dist/gateway/capturing-server.js.map +1 -1
- package/dist/gateway/essentials.d.ts +1 -63
- package/dist/gateway/essentials.d.ts.map +1 -1
- package/dist/gateway/essentials.js +10 -174
- package/dist/gateway/essentials.js.map +1 -1
- package/dist/gateway/test-support.d.ts +0 -28
- package/dist/gateway/test-support.d.ts.map +1 -1
- package/dist/gateway/test-support.js +0 -29
- package/dist/gateway/test-support.js.map +1 -1
- package/dist/health-response.d.ts +2 -0
- package/dist/health-response.d.ts.map +1 -0
- package/dist/health-response.js +7 -0
- package/dist/health-response.js.map +1 -0
- package/dist/icon-data.js +0 -3
- package/dist/icon-data.js.map +1 -1
- package/dist/index.d.ts +7 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -130
- package/dist/index.js.map +1 -1
- package/dist/install-skill.d.ts +0 -44
- package/dist/install-skill.d.ts.map +1 -1
- package/dist/install-skill.js +5 -46
- package/dist/install-skill.js.map +1 -1
- package/dist/net/extra-headers.d.ts +0 -58
- package/dist/net/extra-headers.d.ts.map +1 -1
- package/dist/net/extra-headers.js +0 -67
- package/dist/net/extra-headers.js.map +1 -1
- package/dist/net/internal-host.d.ts +0 -31
- package/dist/net/internal-host.d.ts.map +1 -1
- package/dist/net/internal-host.js +7 -65
- package/dist/net/internal-host.js.map +1 -1
- package/dist/net/proxy-dispatcher.d.ts +0 -37
- package/dist/net/proxy-dispatcher.d.ts.map +1 -1
- package/dist/net/proxy-dispatcher.js +0 -40
- package/dist/net/proxy-dispatcher.js.map +1 -1
- package/dist/net/public-fetch.d.ts +53 -0
- package/dist/net/public-fetch.d.ts.map +1 -0
- package/dist/net/public-fetch.js +143 -0
- package/dist/net/public-fetch.js.map +1 -0
- package/dist/net/public-page-report.d.ts +26 -0
- package/dist/net/public-page-report.d.ts.map +1 -0
- package/dist/net/public-page-report.js +145 -0
- package/dist/net/public-page-report.js.map +1 -0
- package/dist/net/redirect-safe-fetch.d.ts +14 -0
- package/dist/net/redirect-safe-fetch.d.ts.map +1 -0
- package/dist/net/redirect-safe-fetch.js +61 -0
- package/dist/net/redirect-safe-fetch.js.map +1 -0
- package/dist/net/session-recovery-fetch.d.ts +7 -0
- package/dist/net/session-recovery-fetch.d.ts.map +1 -0
- package/dist/net/session-recovery-fetch.js +139 -0
- package/dist/net/session-recovery-fetch.js.map +1 -0
- package/dist/net/site-basic-auth.d.ts +1 -59
- package/dist/net/site-basic-auth.d.ts.map +1 -1
- package/dist/net/site-basic-auth.js +5 -94
- package/dist/net/site-basic-auth.js.map +1 -1
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +15 -0
- package/dist/package-version.js.map +1 -0
- package/dist/platform/detect.d.ts +0 -54
- package/dist/platform/detect.d.ts.map +1 -1
- package/dist/platform/detect.js +0 -53
- package/dist/platform/detect.js.map +1 -1
- package/dist/platform/index.d.ts +0 -100
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/index.js +0 -106
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/joomla.d.ts +0 -35
- package/dist/platform/joomla.d.ts.map +1 -1
- package/dist/platform/joomla.js +0 -35
- package/dist/platform/joomla.js.map +1 -1
- package/dist/proxy/bridge.d.ts +36 -76
- package/dist/proxy/bridge.d.ts.map +1 -1
- package/dist/proxy/bridge.js +443 -136
- package/dist/proxy/bridge.js.map +1 -1
- package/dist/proxy/confirm-elicitation.d.ts +22 -0
- package/dist/proxy/confirm-elicitation.d.ts.map +1 -0
- package/dist/proxy/confirm-elicitation.js +170 -0
- package/dist/proxy/confirm-elicitation.js.map +1 -0
- package/dist/proxy/confirm-question-contract.d.ts +6 -0
- package/dist/proxy/confirm-question-contract.d.ts.map +1 -0
- package/dist/proxy/confirm-question-contract.js +9 -0
- package/dist/proxy/confirm-question-contract.js.map +1 -0
- package/dist/proxy/confirm-question.d.ts +2 -0
- package/dist/proxy/confirm-question.d.ts.map +1 -0
- package/dist/proxy/confirm-question.js +125 -0
- package/dist/proxy/confirm-question.js.map +1 -0
- package/dist/proxy/instructions.d.ts +4 -0
- package/dist/proxy/instructions.d.ts.map +1 -0
- package/dist/proxy/instructions.js +12 -0
- package/dist/proxy/instructions.js.map +1 -0
- package/dist/proxy/mode.d.ts +14 -9
- package/dist/proxy/mode.d.ts.map +1 -1
- package/dist/proxy/mode.js +58 -34
- package/dist/proxy/mode.js.map +1 -1
- package/dist/proxy/multi-site.d.ts +42 -0
- package/dist/proxy/multi-site.d.ts.map +1 -0
- package/dist/proxy/multi-site.js +134 -0
- package/dist/proxy/multi-site.js.map +1 -0
- package/dist/proxy/request-headers.d.ts +9 -0
- package/dist/proxy/request-headers.d.ts.map +1 -0
- package/dist/proxy/request-headers.js +17 -0
- package/dist/proxy/request-headers.js.map +1 -0
- package/dist/proxy/shadow-tools.d.ts +9 -0
- package/dist/proxy/shadow-tools.d.ts.map +1 -0
- package/dist/proxy/shadow-tools.js +42 -0
- package/dist/proxy/shadow-tools.js.map +1 -0
- package/dist/proxy/tools-list-cap.d.ts +7 -0
- package/dist/proxy/tools-list-cap.d.ts.map +1 -0
- package/dist/proxy/tools-list-cap.js +14 -0
- package/dist/proxy/tools-list-cap.js.map +1 -0
- package/dist/proxy/transport-uncertainty.d.ts +2 -0
- package/dist/proxy/transport-uncertainty.d.ts.map +1 -0
- package/dist/proxy/transport-uncertainty.js +8 -0
- package/dist/proxy/transport-uncertainty.js.map +1 -0
- package/dist/proxy/upstream-error.d.ts +7 -0
- package/dist/proxy/upstream-error.d.ts.map +1 -0
- package/dist/proxy/upstream-error.js +35 -0
- package/dist/proxy/upstream-error.js.map +1 -0
- package/dist/proxy/wire-dialect.d.ts +5 -0
- package/dist/proxy/wire-dialect.d.ts.map +1 -0
- package/dist/proxy/wire-dialect.js +33 -0
- package/dist/proxy/wire-dialect.js.map +1 -0
- package/dist/server.d.ts +14 -82
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +107 -203
- package/dist/server.js.map +1 -1
- package/dist/setup/banner-color.d.ts +13 -0
- package/dist/setup/banner-color.d.ts.map +1 -0
- package/dist/setup/banner-color.js +93 -0
- package/dist/setup/banner-color.js.map +1 -0
- package/dist/setup/banner.d.ts +22 -0
- package/dist/setup/banner.d.ts.map +1 -0
- package/dist/setup/banner.js +119 -0
- package/dist/setup/banner.js.map +1 -0
- package/dist/setup/ca-recovery.d.ts +20 -0
- package/dist/setup/ca-recovery.d.ts.map +1 -0
- package/dist/setup/ca-recovery.js +92 -0
- package/dist/setup/ca-recovery.js.map +1 -0
- package/dist/setup/closing.d.ts +15 -0
- package/dist/setup/closing.d.ts.map +1 -0
- package/dist/setup/closing.js +43 -0
- package/dist/setup/closing.js.map +1 -0
- package/dist/setup/corporate-ca.d.ts +16 -0
- package/dist/setup/corporate-ca.d.ts.map +1 -0
- package/dist/setup/corporate-ca.js +32 -0
- package/dist/setup/corporate-ca.js.map +1 -0
- package/dist/setup/host-platform.d.ts +8 -0
- package/dist/setup/host-platform.d.ts.map +1 -0
- package/dist/setup/host-platform.js +21 -0
- package/dist/setup/host-platform.js.map +1 -0
- package/dist/setup/install-configs.d.ts +18 -0
- package/dist/setup/install-configs.d.ts.map +1 -0
- package/dist/setup/install-configs.js +70 -0
- package/dist/setup/install-configs.js.map +1 -0
- package/dist/setup/multi-site-loop.d.ts +14 -0
- package/dist/setup/multi-site-loop.d.ts.map +1 -0
- package/dist/setup/multi-site-loop.js +20 -0
- package/dist/setup/multi-site-loop.js.map +1 -0
- package/dist/setup/phases.d.ts +18 -0
- package/dist/setup/phases.d.ts.map +1 -0
- package/dist/setup/phases.js +55 -0
- package/dist/setup/phases.js.map +1 -0
- package/dist/setup/registry-remedy.d.ts +12 -0
- package/dist/setup/registry-remedy.d.ts.map +1 -0
- package/dist/setup/registry-remedy.js +37 -0
- package/dist/setup/registry-remedy.js.map +1 -0
- package/dist/setup/resume.d.ts +28 -0
- package/dist/setup/resume.d.ts.map +1 -0
- package/dist/setup/resume.js +99 -0
- package/dist/setup/resume.js.map +1 -0
- package/dist/setup-ca-env.d.ts +0 -30
- package/dist/setup-ca-env.d.ts.map +1 -1
- package/dist/setup-ca-env.js +0 -28
- package/dist/setup-ca-env.js.map +1 -1
- package/dist/setup-ca-reprobe.d.ts +0 -25
- package/dist/setup-ca-reprobe.d.ts.map +1 -1
- package/dist/setup-ca-reprobe.js +0 -34
- package/dist/setup-ca-reprobe.js.map +1 -1
- package/dist/setup-cli-confirm.d.ts +0 -34
- package/dist/setup-cli-confirm.d.ts.map +1 -1
- package/dist/setup-cli-confirm.js +0 -36
- package/dist/setup-cli-confirm.js.map +1 -1
- package/dist/setup-cli.d.ts +6 -167
- package/dist/setup-cli.d.ts.map +1 -1
- package/dist/setup-cli.js +161 -173
- package/dist/setup-cli.js.map +1 -1
- package/dist/setup-npx-spec.d.ts +0 -36
- package/dist/setup-npx-spec.d.ts.map +1 -1
- package/dist/setup-npx-spec.js +0 -38
- package/dist/setup-npx-spec.js.map +1 -1
- package/dist/setup-prompts.d.ts +5 -70
- package/dist/setup-prompts.d.ts.map +1 -1
- package/dist/setup-prompts.js +67 -107
- package/dist/setup-prompts.js.map +1 -1
- package/dist/setup-token.d.ts +0 -34
- package/dist/setup-token.d.ts.map +1 -1
- package/dist/setup-token.js +0 -40
- package/dist/setup-token.js.map +1 -1
- package/dist/setup-wizard-defaults.d.ts +0 -36
- package/dist/setup-wizard-defaults.d.ts.map +1 -1
- package/dist/setup-wizard-defaults.js +0 -75
- package/dist/setup-wizard-defaults.js.map +1 -1
- package/dist/setup-wizard-handshake.d.ts +1 -19
- package/dist/setup-wizard-handshake.d.ts.map +1 -1
- package/dist/setup-wizard-handshake.js +8 -45
- package/dist/setup-wizard-handshake.js.map +1 -1
- package/dist/setup-wizard-types.d.ts +16 -209
- package/dist/setup-wizard-types.d.ts.map +1 -1
- package/dist/setup-wizard-types.js +0 -9
- package/dist/setup-wizard-types.js.map +1 -1
- package/dist/setup-wizard.d.ts +0 -28
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +167 -412
- package/dist/setup-wizard.js.map +1 -1
- package/dist/setup.d.ts +0 -15
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +0 -15
- package/dist/setup.js.map +1 -1
- package/dist/sites/bootstrap.d.ts +0 -38
- package/dist/sites/bootstrap.d.ts.map +1 -1
- package/dist/sites/bootstrap.js +0 -44
- package/dist/sites/bootstrap.js.map +1 -1
- package/dist/sites/cli/add-site.d.ts +0 -76
- package/dist/sites/cli/add-site.d.ts.map +1 -1
- package/dist/sites/cli/add-site.js +0 -71
- package/dist/sites/cli/add-site.js.map +1 -1
- package/dist/sites/cli/list-sites.d.ts +0 -31
- package/dist/sites/cli/list-sites.d.ts.map +1 -1
- package/dist/sites/cli/list-sites.js +0 -45
- package/dist/sites/cli/list-sites.js.map +1 -1
- package/dist/sites/cli/remove-site.d.ts +0 -37
- package/dist/sites/cli/remove-site.d.ts.map +1 -1
- package/dist/sites/cli/remove-site.js +0 -37
- package/dist/sites/cli/remove-site.js.map +1 -1
- package/dist/sites/cli/set-default.d.ts +0 -25
- package/dist/sites/cli/set-default.d.ts.map +1 -1
- package/dist/sites/cli/set-default.js +0 -22
- package/dist/sites/cli/set-default.js.map +1 -1
- package/dist/sites/cli/test-site.d.ts +0 -44
- package/dist/sites/cli/test-site.d.ts.map +1 -1
- package/dist/sites/cli/test-site.js +0 -39
- package/dist/sites/cli/test-site.js.map +1 -1
- package/dist/sites/client-pool.d.ts +11 -164
- package/dist/sites/client-pool.d.ts.map +1 -1
- package/dist/sites/client-pool.js +73 -194
- package/dist/sites/client-pool.js.map +1 -1
- package/dist/sites/config-problem.d.ts +2 -0
- package/dist/sites/config-problem.d.ts.map +1 -0
- package/dist/sites/config-problem.js +13 -0
- package/dist/sites/config-problem.js.map +1 -0
- package/dist/sites/env-bridge.d.ts +0 -69
- package/dist/sites/env-bridge.d.ts.map +1 -1
- package/dist/sites/env-bridge.js +2 -71
- package/dist/sites/env-bridge.js.map +1 -1
- package/dist/sites/op-inject.d.ts +23 -0
- package/dist/sites/op-inject.d.ts.map +1 -0
- package/dist/sites/op-inject.js +131 -0
- package/dist/sites/op-inject.js.map +1 -0
- package/dist/sites/paths.d.ts +0 -19
- package/dist/sites/paths.d.ts.map +1 -1
- package/dist/sites/paths.js +0 -19
- package/dist/sites/paths.js.map +1 -1
- package/dist/sites/probe.d.ts +1 -47
- package/dist/sites/probe.d.ts.map +1 -1
- package/dist/sites/probe.js +4 -46
- package/dist/sites/probe.js.map +1 -1
- package/dist/sites/registry.d.ts +4 -135
- package/dist/sites/registry.d.ts.map +1 -1
- package/dist/sites/registry.js +4 -98
- package/dist/sites/registry.js.map +1 -1
- package/dist/sites/schema.d.ts +3 -51
- package/dist/sites/schema.d.ts.map +1 -1
- package/dist/sites/schema.js +6 -52
- package/dist/sites/schema.js.map +1 -1
- package/dist/sites/secret-resolver.d.ts +6 -76
- package/dist/sites/secret-resolver.d.ts.map +1 -1
- package/dist/sites/secret-resolver.js +39 -104
- package/dist/sites/secret-resolver.js.map +1 -1
- package/dist/sites/store.d.ts +12 -69
- package/dist/sites/store.d.ts.map +1 -1
- package/dist/sites/store.js +37 -86
- package/dist/sites/store.js.map +1 -1
- package/dist/sites/substituted-registry.d.ts +11 -0
- package/dist/sites/substituted-registry.d.ts.map +1 -0
- package/dist/sites/substituted-registry.js +13 -0
- package/dist/sites/substituted-registry.js.map +1 -0
- package/dist/sites/tools/fetch-public-url.d.ts +4 -0
- package/dist/sites/tools/fetch-public-url.d.ts.map +1 -0
- package/dist/sites/tools/fetch-public-url.js +231 -0
- package/dist/sites/tools/fetch-public-url.js.map +1 -0
- package/dist/sites/tools/index.d.ts +2 -26
- package/dist/sites/tools/index.d.ts.map +1 -1
- package/dist/sites/tools/index.js +5 -28
- package/dist/sites/tools/index.js.map +1 -1
- package/dist/sites/tools/sites-list.d.ts +3 -47
- package/dist/sites/tools/sites-list.d.ts.map +1 -1
- package/dist/sites/tools/sites-list.js +68 -80
- package/dist/sites/tools/sites-list.js.map +1 -1
- package/dist/sites/tools/sites-overview.d.ts +4 -0
- package/dist/sites/tools/sites-overview.d.ts.map +1 -0
- package/dist/sites/tools/sites-overview.js +209 -0
- package/dist/sites/tools/sites-overview.js.map +1 -0
- package/dist/sites/tools/sites-test.d.ts +2 -45
- package/dist/sites/tools/sites-test.d.ts.map +1 -1
- package/dist/sites/tools/sites-test.js +1 -56
- package/dist/sites/tools/sites-test.js.map +1 -1
- package/dist/sites/tools/use-site.d.ts +2 -45
- package/dist/sites/tools/use-site.d.ts.map +1 -1
- package/dist/sites/tools/use-site.js +31 -80
- package/dist/sites/tools/use-site.js.map +1 -1
- package/dist/sites/unknown-name-result.d.ts +10 -0
- package/dist/sites/unknown-name-result.d.ts.map +1 -0
- package/dist/sites/unknown-name-result.js +13 -0
- package/dist/sites/unknown-name-result.js.map +1 -0
- package/dist/skill-instructions.d.ts +7 -0
- package/dist/skill-instructions.d.ts.map +1 -0
- package/dist/skill-instructions.js +84 -0
- package/dist/skill-instructions.js.map +1 -0
- package/dist/skill-loader.d.ts +0 -102
- package/dist/skill-loader.d.ts.map +1 -1
- package/dist/skill-loader.js +4 -142
- package/dist/skill-loader.js.map +1 -1
- package/dist/skill-manifest.d.ts +0 -20
- package/dist/skill-manifest.d.ts.map +1 -1
- package/dist/skill-manifest.js +1 -21
- package/dist/skill-manifest.js.map +1 -1
- package/dist/skill-topics.d.ts +19 -0
- package/dist/skill-topics.d.ts.map +1 -0
- package/dist/skill-topics.js +181 -0
- package/dist/skill-topics.js.map +1 -0
- package/dist/tools/active-site.d.ts +16 -0
- package/dist/tools/active-site.d.ts.map +1 -0
- package/dist/tools/active-site.js +55 -0
- package/dist/tools/active-site.js.map +1 -0
- package/dist/tools/bind-guidance.d.ts +2 -72
- package/dist/tools/bind-guidance.d.ts.map +1 -1
- package/dist/tools/bind-guidance.js +2 -72
- package/dist/tools/bind-guidance.js.map +1 -1
- package/dist/tools/budgeted-table.d.ts +16 -0
- package/dist/tools/budgeted-table.d.ts.map +1 -0
- package/dist/tools/budgeted-table.js +203 -0
- package/dist/tools/budgeted-table.js.map +1 -0
- package/dist/tools/elements/bind-output-schema.d.ts +69 -0
- package/dist/tools/elements/bind-output-schema.d.ts.map +1 -0
- package/dist/tools/elements/bind-output-schema.js +64 -0
- package/dist/tools/elements/bind-output-schema.js.map +1 -0
- package/dist/tools/elements/builders.d.ts +1 -22
- package/dist/tools/elements/builders.d.ts.map +1 -1
- package/dist/tools/elements/builders.js +171 -133
- package/dist/tools/elements/builders.js.map +1 -1
- package/dist/tools/elements/index.d.ts +0 -26
- package/dist/tools/elements/index.d.ts.map +1 -1
- package/dist/tools/elements/index.js +0 -25
- package/dist/tools/elements/index.js.map +1 -1
- package/dist/tools/elements/read-output-schemas.d.ts +121 -0
- package/dist/tools/elements/read-output-schemas.d.ts.map +1 -0
- package/dist/tools/elements/read-output-schemas.js +225 -0
- package/dist/tools/elements/read-output-schemas.js.map +1 -0
- package/dist/tools/elements/write-output-schemas.d.ts +185 -0
- package/dist/tools/elements/write-output-schemas.d.ts.map +1 -0
- package/dist/tools/elements/write-output-schemas.js +98 -0
- package/dist/tools/elements/write-output-schemas.js.map +1 -0
- package/dist/tools/elements.d.ts +0 -11
- package/dist/tools/elements.d.ts.map +1 -1
- package/dist/tools/elements.js +0 -11
- package/dist/tools/elements.js.map +1 -1
- package/dist/tools/format/health-format.d.ts +42 -43
- package/dist/tools/format/health-format.d.ts.map +1 -1
- package/dist/tools/format/health-format.js +32 -55
- package/dist/tools/format/health-format.js.map +1 -1
- package/dist/tools/format/inspection-format.d.ts +4 -49
- package/dist/tools/format/inspection-format.d.ts.map +1 -1
- package/dist/tools/format/inspection-format.js +46 -114
- package/dist/tools/format/inspection-format.js.map +1 -1
- package/dist/tools/format/pages-format.d.ts +32 -69
- package/dist/tools/format/pages-format.d.ts.map +1 -1
- package/dist/tools/format/pages-format.js +115 -133
- package/dist/tools/format/pages-format.js.map +1 -1
- package/dist/tools/format/sources-format.d.ts +5 -39
- package/dist/tools/format/sources-format.d.ts.map +1 -1
- package/dist/tools/format/sources-format.js +57 -87
- package/dist/tools/format/sources-format.js.map +1 -1
- package/dist/tools/health-output-schemas.d.ts +82 -0
- package/dist/tools/health-output-schemas.d.ts.map +1 -0
- package/dist/tools/health-output-schemas.js +130 -0
- package/dist/tools/health-output-schemas.js.map +1 -0
- package/dist/tools/health.d.ts +2 -20
- package/dist/tools/health.d.ts.map +1 -1
- package/dist/tools/health.js +59 -184
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/index.d.ts +1 -26
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +13 -33
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inspection.d.ts +21 -17
- package/dist/tools/inspection.d.ts.map +1 -1
- package/dist/tools/inspection.js +431 -268
- package/dist/tools/inspection.js.map +1 -1
- package/dist/tools/library.d.ts +118 -126
- package/dist/tools/library.d.ts.map +1 -1
- package/dist/tools/library.js +723 -456
- package/dist/tools/library.js.map +1 -1
- package/dist/tools/local-content/builders.d.ts +1 -17
- package/dist/tools/local-content/builders.d.ts.map +1 -1
- package/dist/tools/local-content/builders.js +105 -48
- package/dist/tools/local-content/builders.js.map +1 -1
- package/dist/tools/local-content/index.d.ts +0 -15
- package/dist/tools/local-content/index.d.ts.map +1 -1
- package/dist/tools/local-content/index.js +0 -14
- package/dist/tools/local-content/index.js.map +1 -1
- package/dist/tools/local-content/schemas.d.ts +31 -47
- package/dist/tools/local-content/schemas.d.ts.map +1 -1
- package/dist/tools/local-content/schemas.js +33 -62
- package/dist/tools/local-content/schemas.js.map +1 -1
- package/dist/tools/modules/builders.d.ts +4 -0
- package/dist/tools/modules/builders.d.ts.map +1 -0
- package/dist/tools/modules/builders.js +58 -0
- package/dist/tools/modules/builders.js.map +1 -0
- package/dist/tools/modules/index.d.ts +2 -0
- package/dist/tools/modules/index.d.ts.map +1 -0
- package/dist/tools/modules/index.js +2 -0
- package/dist/tools/modules/index.js.map +1 -0
- package/dist/tools/modules/module-id-schema.d.ts +4 -0
- package/dist/tools/modules/module-id-schema.d.ts.map +1 -0
- package/dist/tools/modules/module-id-schema.js +14 -0
- package/dist/tools/modules/module-id-schema.js.map +1 -0
- package/dist/tools/modules/module-type-fields.d.ts +10 -0
- package/dist/tools/modules/module-type-fields.d.ts.map +1 -0
- package/dist/tools/modules/module-type-fields.js +34 -0
- package/dist/tools/modules/module-type-fields.js.map +1 -0
- package/dist/tools/modules/modules-create.d.ts +59 -0
- package/dist/tools/modules/modules-create.d.ts.map +1 -0
- package/dist/tools/modules/modules-create.js +154 -0
- package/dist/tools/modules/modules-create.js.map +1 -0
- package/dist/tools/modules/modules-delete.d.ts +51 -0
- package/dist/tools/modules/modules-delete.d.ts.map +1 -0
- package/dist/tools/modules/modules-delete.js +180 -0
- package/dist/tools/modules/modules-delete.js.map +1 -0
- package/dist/tools/modules/modules-update.d.ts +43 -0
- package/dist/tools/modules/modules-update.d.ts.map +1 -0
- package/dist/tools/modules/modules-update.js +177 -0
- package/dist/tools/modules/modules-update.js.map +1 -0
- package/dist/tools/modules/placement-hints.d.ts +5 -0
- package/dist/tools/modules/placement-hints.d.ts.map +1 -0
- package/dist/tools/modules/placement-hints.js +90 -0
- package/dist/tools/modules/placement-hints.js.map +1 -0
- package/dist/tools/modules/placement-ways.d.ts +13 -0
- package/dist/tools/modules/placement-ways.d.ts.map +1 -0
- package/dist/tools/modules/placement-ways.js +61 -0
- package/dist/tools/modules/placement-ways.js.map +1 -0
- package/dist/tools/multi-items/builders.d.ts +1 -21
- package/dist/tools/multi-items/builders.d.ts.map +1 -1
- package/dist/tools/multi-items/builders.js +22 -67
- package/dist/tools/multi-items/builders.js.map +1 -1
- package/dist/tools/multi-items/index.d.ts +0 -15
- package/dist/tools/multi-items/index.d.ts.map +1 -1
- package/dist/tools/multi-items/index.js +0 -15
- package/dist/tools/multi-items/index.js.map +1 -1
- package/dist/tools/multi-items/item-container-map.d.ts +0 -35
- package/dist/tools/multi-items/item-container-map.d.ts.map +1 -1
- package/dist/tools/multi-items/item-container-map.js +0 -35
- package/dist/tools/multi-items/item-container-map.js.map +1 -1
- package/dist/tools/multi-items/output-schemas.d.ts +101 -0
- package/dist/tools/multi-items/output-schemas.d.ts.map +1 -0
- package/dist/tools/multi-items/output-schemas.js +74 -0
- package/dist/tools/multi-items/output-schemas.js.map +1 -0
- package/dist/tools/navigation/builders.d.ts +3 -0
- package/dist/tools/navigation/builders.d.ts.map +1 -0
- package/dist/tools/navigation/builders.js +25 -0
- package/dist/tools/navigation/builders.js.map +1 -0
- package/dist/tools/navigation/index.d.ts +2 -0
- package/dist/tools/navigation/index.d.ts.map +1 -0
- package/dist/tools/navigation/index.js +2 -0
- package/dist/tools/navigation/index.js.map +1 -0
- package/dist/tools/navigation/schemas.d.ts +16 -0
- package/dist/tools/navigation/schemas.d.ts.map +1 -0
- package/dist/tools/navigation/schemas.js +11 -0
- package/dist/tools/navigation/schemas.js.map +1 -0
- package/dist/tools/output-schema-metadata.d.ts +5 -0
- package/dist/tools/output-schema-metadata.d.ts.map +1 -0
- package/dist/tools/output-schema-metadata.js +83 -0
- package/dist/tools/output-schema-metadata.js.map +1 -0
- package/dist/tools/pages/builders.d.ts +1 -41
- package/dist/tools/pages/builders.d.ts.map +1 -1
- package/dist/tools/pages/builders.js +373 -359
- package/dist/tools/pages/builders.js.map +1 -1
- package/dist/tools/pages/header-transparency.d.ts +34 -0
- package/dist/tools/pages/header-transparency.d.ts.map +1 -0
- package/dist/tools/pages/header-transparency.js +85 -0
- package/dist/tools/pages/header-transparency.js.map +1 -0
- package/dist/tools/pages/index.d.ts +1 -19
- package/dist/tools/pages/index.d.ts.map +1 -1
- package/dist/tools/pages/index.js +1 -19
- package/dist/tools/pages/index.js.map +1 -1
- package/dist/tools/pages/schemas.d.ts +392 -224
- package/dist/tools/pages/schemas.d.ts.map +1 -1
- package/dist/tools/pages/schemas.js +605 -458
- package/dist/tools/pages/schemas.js.map +1 -1
- package/dist/tools/pages/write-output-schemas.d.ts +139 -0
- package/dist/tools/pages/write-output-schemas.d.ts.map +1 -0
- package/dist/tools/pages/write-output-schemas.js +138 -0
- package/dist/tools/pages/write-output-schemas.js.map +1 -0
- package/dist/tools/pages.d.ts +0 -11
- package/dist/tools/pages.d.ts.map +1 -1
- package/dist/tools/pages.js +0 -11
- package/dist/tools/pages.js.map +1 -1
- package/dist/tools/pool-resolve-helper.d.ts +0 -52
- package/dist/tools/pool-resolve-helper.d.ts.map +1 -1
- package/dist/tools/pool-resolve-helper.js +29 -104
- package/dist/tools/pool-resolve-helper.js.map +1 -1
- package/dist/tools/progress-phases.d.ts +0 -42
- package/dist/tools/progress-phases.d.ts.map +1 -1
- package/dist/tools/progress-phases.js +0 -44
- package/dist/tools/progress-phases.js.map +1 -1
- package/dist/tools/response-budget.d.ts +8 -0
- package/dist/tools/response-budget.d.ts.map +1 -0
- package/dist/tools/response-budget.js +7 -0
- package/dist/tools/response-budget.js.map +1 -0
- package/dist/tools/shared-schemas.d.ts +12 -43
- package/dist/tools/shared-schemas.d.ts.map +1 -1
- package/dist/tools/shared-schemas.js +42 -97
- package/dist/tools/shared-schemas.js.map +1 -1
- package/dist/tools/skill/builders.d.ts +3 -0
- package/dist/tools/skill/builders.d.ts.map +1 -0
- package/dist/tools/skill/builders.js +81 -0
- package/dist/tools/skill/builders.js.map +1 -0
- package/dist/tools/sources/builders.d.ts +1 -11
- package/dist/tools/sources/builders.d.ts.map +1 -1
- package/dist/tools/sources/builders.js +195 -173
- package/dist/tools/sources/builders.js.map +1 -1
- package/dist/tools/sources/index.d.ts +0 -17
- package/dist/tools/sources/index.d.ts.map +1 -1
- package/dist/tools/sources/index.js +0 -16
- package/dist/tools/sources/index.js.map +1 -1
- package/dist/tools/sources/read-output-schemas.d.ts +101 -0
- package/dist/tools/sources/read-output-schemas.d.ts.map +1 -0
- package/dist/tools/sources/read-output-schemas.js +105 -0
- package/dist/tools/sources/read-output-schemas.js.map +1 -0
- package/dist/tools/sources/source-name-argument.d.ts +17 -0
- package/dist/tools/sources/source-name-argument.d.ts.map +1 -0
- package/dist/tools/sources/source-name-argument.js +64 -0
- package/dist/tools/sources/source-name-argument.js.map +1 -0
- package/dist/tools/sources/write-output-schemas.d.ts +100 -0
- package/dist/tools/sources/write-output-schemas.d.ts.map +1 -0
- package/dist/tools/sources/write-output-schemas.js +61 -0
- package/dist/tools/sources/write-output-schemas.js.map +1 -0
- package/dist/tools/sources.d.ts +0 -12
- package/dist/tools/sources.d.ts.map +1 -1
- package/dist/tools/sources.js +0 -12
- package/dist/tools/sources.js.map +1 -1
- package/dist/tools/sparse-fields.d.ts +19 -194
- package/dist/tools/sparse-fields.d.ts.map +1 -1
- package/dist/tools/sparse-fields.js +212 -196
- package/dist/tools/sparse-fields.js.map +1 -1
- package/dist/tools/strict-input.d.ts +5 -0
- package/dist/tools/strict-input.d.ts.map +1 -0
- package/dist/tools/strict-input.js +55 -0
- package/dist/tools/strict-input.js.map +1 -0
- package/dist/tools/sublayout/builders.d.ts +1 -21
- package/dist/tools/sublayout/builders.d.ts.map +1 -1
- package/dist/tools/sublayout/builders.js +10 -40
- package/dist/tools/sublayout/builders.js.map +1 -1
- package/dist/tools/sublayout/index.d.ts +1 -11
- package/dist/tools/sublayout/index.d.ts.map +1 -1
- package/dist/tools/sublayout/index.js +1 -11
- package/dist/tools/sublayout/index.js.map +1 -1
- package/dist/tools/sublayout/output-schemas.d.ts +57 -0
- package/dist/tools/sublayout/output-schemas.d.ts.map +1 -0
- package/dist/tools/sublayout/output-schemas.js +51 -0
- package/dist/tools/sublayout/output-schemas.js.map +1 -0
- package/dist/tools/tool-builder/annotations.d.ts +1 -49
- package/dist/tools/tool-builder/annotations.d.ts.map +1 -1
- package/dist/tools/tool-builder/annotations.js +9 -49
- package/dist/tools/tool-builder/annotations.js.map +1 -1
- package/dist/tools/tool-builder/confirm-preview.d.ts +24 -0
- package/dist/tools/tool-builder/confirm-preview.d.ts.map +1 -0
- package/dist/tools/tool-builder/confirm-preview.js +37 -0
- package/dist/tools/tool-builder/confirm-preview.js.map +1 -0
- package/dist/tools/tool-builder/define.d.ts +6 -26
- package/dist/tools/tool-builder/define.d.ts.map +1 -1
- package/dist/tools/tool-builder/define.js +0 -25
- package/dist/tools/tool-builder/define.js.map +1 -1
- package/dist/tools/tool-builder/index.d.ts +3 -26
- package/dist/tools/tool-builder/index.d.ts.map +1 -1
- package/dist/tools/tool-builder/index.js +2 -25
- package/dist/tools/tool-builder/index.js.map +1 -1
- package/dist/tools/tool-builder/results.d.ts +4 -111
- package/dist/tools/tool-builder/results.d.ts.map +1 -1
- package/dist/tools/tool-builder/results.js +258 -250
- package/dist/tools/tool-builder/results.js.map +1 -1
- package/dist/tools/tool-builder/types.d.ts +5 -59
- package/dist/tools/tool-builder/types.d.ts.map +1 -1
- package/dist/tools/tool-builder/types.js +0 -7
- package/dist/tools/tool-builder/types.js.map +1 -1
- package/dist/tools/tool-builder.d.ts +0 -14
- package/dist/tools/tool-builder.d.ts.map +1 -1
- package/dist/tools/tool-builder.js +0 -14
- package/dist/tools/tool-builder.js.map +1 -1
- package/dist/uninstall-skill.d.ts +0 -41
- package/dist/uninstall-skill.d.ts.map +1 -1
- package/dist/uninstall-skill.js +0 -49
- package/dist/uninstall-skill.js.map +1 -1
- package/dist/uninstall.d.ts +0 -41
- package/dist/uninstall.d.ts.map +1 -1
- package/dist/uninstall.js +1 -29
- package/dist/uninstall.js.map +1 -1
- package/dist/update/update-check.d.ts +0 -26
- package/dist/update/update-check.d.ts.map +1 -1
- package/dist/update/update-check.js +0 -25
- package/dist/update/update-check.js.map +1 -1
- package/manifest.json +4 -5
- package/package.json +5 -5
- package/skills/yt-builder-mcp/SKILL.md +2958 -382
- package/dist/tools/elements/handlers-write.d.ts +0 -70
- package/dist/tools/elements/handlers-write.d.ts.map +0 -1
- package/dist/tools/elements/handlers-write.js +0 -700
- package/dist/tools/elements/handlers-write.js.map +0 -1
- package/dist/tools/elements/handlers.d.ts +0 -125
- package/dist/tools/elements/handlers.d.ts.map +0 -1
- package/dist/tools/elements/handlers.js +0 -571
- package/dist/tools/elements/handlers.js.map +0 -1
- package/dist/tools/elements/layout-hierarchy-rules.d.ts +0 -62
- package/dist/tools/elements/layout-hierarchy-rules.d.ts.map +0 -1
- package/dist/tools/elements/layout-hierarchy-rules.js +0 -154
- package/dist/tools/elements/layout-hierarchy-rules.js.map +0 -1
- package/dist/tools/elements/schema-validation.d.ts +0 -96
- package/dist/tools/elements/schema-validation.d.ts.map +0 -1
- package/dist/tools/elements/schema-validation.js +0 -249
- package/dist/tools/elements/schema-validation.js.map +0 -1
- package/dist/tools/elicitation.d.ts +0 -87
- package/dist/tools/elicitation.d.ts.map +0 -1
- package/dist/tools/elicitation.js +0 -100
- package/dist/tools/elicitation.js.map +0 -1
- package/dist/tools/format/elements-format.d.ts +0 -75
- package/dist/tools/format/elements-format.d.ts.map +0 -1
- package/dist/tools/format/elements-format.js +0 -324
- package/dist/tools/format/elements-format.js.map +0 -1
- package/dist/tools/format/source-schema-format.d.ts +0 -74
- package/dist/tools/format/source-schema-format.d.ts.map +0 -1
- package/dist/tools/format/source-schema-format.js +0 -208
- package/dist/tools/format/source-schema-format.js.map +0 -1
- package/dist/tools/layout-flatten.d.ts +0 -84
- package/dist/tools/layout-flatten.d.ts.map +0 -1
- package/dist/tools/layout-flatten.js +0 -129
- package/dist/tools/layout-flatten.js.map +0 -1
- package/dist/tools/local-content/handlers.d.ts +0 -54
- package/dist/tools/local-content/handlers.d.ts.map +0 -1
- package/dist/tools/local-content/handlers.js +0 -316
- package/dist/tools/local-content/handlers.js.map +0 -1
- package/dist/tools/multi-items/clean-implode-handler.d.ts +0 -37
- package/dist/tools/multi-items/clean-implode-handler.d.ts.map +0 -1
- package/dist/tools/multi-items/clean-implode-handler.js +0 -65
- package/dist/tools/multi-items/clean-implode-handler.js.map +0 -1
- package/dist/tools/multi-items/format/inspection-format.d.ts +0 -41
- package/dist/tools/multi-items/format/inspection-format.d.ts.map +0 -1
- package/dist/tools/multi-items/format/inspection-format.js +0 -64
- package/dist/tools/multi-items/format/inspection-format.js.map +0 -1
- package/dist/tools/multi-items/inspect-handler.d.ts +0 -97
- package/dist/tools/multi-items/inspect-handler.d.ts.map +0 -1
- package/dist/tools/multi-items/inspect-handler.js +0 -323
- package/dist/tools/multi-items/inspect-handler.js.map +0 -1
- package/dist/tools/multi-items/inspection-cache.d.ts +0 -85
- package/dist/tools/multi-items/inspection-cache.d.ts.map +0 -1
- package/dist/tools/multi-items/inspection-cache.js +0 -126
- package/dist/tools/multi-items/inspection-cache.js.map +0 -1
- package/dist/tools/pages/handlers-audit.d.ts +0 -129
- package/dist/tools/pages/handlers-audit.d.ts.map +0 -1
- package/dist/tools/pages/handlers-audit.js +0 -704
- package/dist/tools/pages/handlers-audit.js.map +0 -1
- package/dist/tools/pages/handlers-read.d.ts +0 -95
- package/dist/tools/pages/handlers-read.d.ts.map +0 -1
- package/dist/tools/pages/handlers-read.js +0 -929
- package/dist/tools/pages/handlers-read.js.map +0 -1
- package/dist/tools/pages/handlers-write.d.ts +0 -212
- package/dist/tools/pages/handlers-write.d.ts.map +0 -1
- package/dist/tools/pages/handlers-write.js +0 -418
- package/dist/tools/pages/handlers-write.js.map +0 -1
- package/dist/tools/pages/target.d.ts +0 -162
- package/dist/tools/pages/target.d.ts.map +0 -1
- package/dist/tools/pages/target.js +0 -239
- package/dist/tools/pages/target.js.map +0 -1
- package/dist/tools/sources/handlers-bind.d.ts +0 -148
- package/dist/tools/sources/handlers-bind.d.ts.map +0 -1
- package/dist/tools/sources/handlers-bind.js +0 -655
- package/dist/tools/sources/handlers-bind.js.map +0 -1
- package/dist/tools/sources/handlers.d.ts +0 -114
- package/dist/tools/sources/handlers.d.ts.map +0 -1
- package/dist/tools/sources/handlers.js +0 -285
- package/dist/tools/sources/handlers.js.map +0 -1
- package/dist/tools/sublayout/inspect-sublayout-handler.d.ts +0 -95
- package/dist/tools/sublayout/inspect-sublayout-handler.d.ts.map +0 -1
- package/dist/tools/sublayout/inspect-sublayout-handler.js +0 -181
- package/dist/tools/sublayout/inspect-sublayout-handler.js.map +0 -1
|
@@ -12,18 +12,20 @@ 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
|
-
|
|
15
|
+
`@wootsup/yt-builder-mcp` server. The server catalogues 60 typed, scoped
|
|
16
|
+
tools (62 callable end-to-end with the two gateways). 49 of them
|
|
17
17
|
advertised as first-class entries in `tools/list`
|
|
18
|
-
(
|
|
18
|
+
(45 essential L1 forwards + 2 direct L3 entries + 2 gateways), and 13 advanced
|
|
19
19
|
tools reachable through the gateways. The write-capable
|
|
20
|
-
`yootheme_builder_advanced` gateway routes all
|
|
20
|
+
`yootheme_builder_advanced` gateway routes all 13; the read-only
|
|
21
21
|
`yootheme_builder_advanced_read` gateway routes only the read-only subset
|
|
22
|
-
(
|
|
23
|
-
local_content_read, pages_audit) and is itself
|
|
22
|
+
(inspect_sublayout, layout_query, local_content_types,
|
|
23
|
+
local_content_read, pages_audit, heading_outline, navigation) and is itself
|
|
24
24
|
read-only-annotated, so MCP hosts can auto-allow pure inspection without a
|
|
25
|
-
per-call write-approval prompt. This 3-lane split
|
|
26
|
-
|
|
25
|
+
per-call write-approval prompt. This 3-lane split exists because Cursor (and
|
|
26
|
+
similar hosts) cap a server at roughly 40 first-class `tools/list` entries —
|
|
27
|
+
it keeps the advertised surface small while every catalogued tool stays
|
|
28
|
+
fully reachable through the gateways.
|
|
27
29
|
|
|
28
30
|
## How to use this MCP server
|
|
29
31
|
|
|
@@ -54,8 +56,8 @@ Some clients (notably Claude Desktop with the `.mcpb` bundle) expose **two**
|
|
|
54
56
|
entries when the user types "YT Builder MCP" into the picker:
|
|
55
57
|
|
|
56
58
|
- **`YT Builder MCP for YOOtheme Pro (unofficial)`**: the MCP **server**.
|
|
57
|
-
Provides the
|
|
58
|
-
The `yootheme_builder_advanced` gateway routes all
|
|
59
|
+
Provides the 49 first-class tools (45 essential, 2 direct, 2 gateways).
|
|
60
|
+
The `yootheme_builder_advanced` gateway routes all 13 advanced tools; the
|
|
59
61
|
read-only `yootheme_builder_advanced_read` gateway routes the read-only
|
|
60
62
|
subset without a write-approval prompt.
|
|
61
63
|
- **`Von YT Builder MCP for YOOtheme Pro`**: the bundled **skill** (this
|
|
@@ -82,39 +84,40 @@ The server exposes:
|
|
|
82
84
|
- **2 direct top-level tools**, always callable, always in `tools/list`:
|
|
83
85
|
`yootheme_builder_health` and `yootheme_builder_diagnose`. These are
|
|
84
86
|
the "the gateway itself might be broken" escape hatch.
|
|
85
|
-
- **
|
|
86
|
-
(pages_list, pages_resolve, pages_create, pages_delete,
|
|
87
|
-
get_etag, element_list / add / update_settings / get / move / clone / delete,
|
|
87
|
+
- **45 essential forwarded tools**: common reads + the most-used writes
|
|
88
|
+
(pages_list, pages_resolve, pages_create, pages_delete, pages_set_status, pages_restore, page_generate_dynamic, scaffold_section,
|
|
89
|
+
get_etag, get_skill, element_list / add / update_settings / set_status / get / move / clone / delete,
|
|
88
90
|
page_get_layout, sources_list, source_get_schema, element_types_list, element_type_get_schema,
|
|
89
91
|
template_summary, inspect_multi_items_binding, library_browse, library_get, library_insert,
|
|
90
|
-
image_search, image_import,
|
|
92
|
+
image_search, image_import, modules_delete,
|
|
91
93
|
sites_list, sites_test,
|
|
92
|
-
use_site, the bind→verify→save→publish chain element_bind_source /
|
|
93
|
-
element_get_binding / page_save / page_publish, and the archive-rebind /
|
|
94
|
-
refactor surface render_preview / element_set_source /
|
|
94
|
+
use_site, the bind→verify→save→publish→unbind chain element_bind_source /
|
|
95
|
+
element_get_binding / page_save / page_publish / element_unbind_source, and the archive-rebind /
|
|
96
|
+
refactor surface render_preview / element_set_source / repair_source_references /
|
|
95
97
|
template_set_type). Always advertised in
|
|
96
98
|
`tools/list` so AI clients see them first-class. `element_get_binding`
|
|
97
99
|
(B4) is L1 so the verify half of a bind is a direct call, not a
|
|
98
100
|
gateway hop. `source_get_schema` (U4-L1) is L1 so the archive/category
|
|
99
101
|
binding entry-point — introspect a source's query methods + relations +
|
|
100
102
|
the current-term archive-context convention before binding — is a direct
|
|
101
|
-
call too. `render_preview`, `element_set_source`, `
|
|
102
|
-
`template_set_type` (
|
|
103
|
+
call too. `render_preview`, `element_set_source`, `repair_source_references`
|
|
104
|
+
and `template_set_type` (customer HANDOFF-4/5) are L1 so the core write/refactor +
|
|
103
105
|
render-verify tools an agent reaches for after a taxonomy/CPT rename — set a
|
|
104
106
|
precomposed source at a pointer, render-verify the saved page is not silently
|
|
105
|
-
blank,
|
|
107
|
+
blank, repair a source/field binding REFERENCE template-wide (the registered
|
|
108
|
+
source itself is untouched), re-type an orphaned archive/single
|
|
106
109
|
template — are direct calls, not undiscoverable gateway hops. `scaffold_section`
|
|
107
110
|
creates a valid
|
|
108
111
|
section → row → column hull in one call — the easiest correct way to
|
|
109
112
|
start a layout block (content placed anywhere else is rejected by the
|
|
110
|
-
structural-hierarchy guard). `library_browse` (
|
|
113
|
+
structural-hierarchy guard). `library_browse` (Layout Library) is L1
|
|
111
114
|
so browsing the YOOtheme Pro Library catalog — pre-designed page layouts and
|
|
112
115
|
element presets, filterable by topic/type/role/website/element_type/query —
|
|
113
116
|
is a direct call before hand-building a section from scratch. `library_get`
|
|
114
|
-
(
|
|
117
|
+
(Layout Library) is L1 so fetching a SINGLE browsed item's full tree
|
|
115
118
|
+ derived section list (`{id, name, path, element_count}` per top-level
|
|
116
119
|
section) — the natural next call once `library_browse` returns an `id` —
|
|
117
|
-
is a direct call too. `library_insert` (
|
|
120
|
+
is a direct call too. `library_insert` (Layout Library) is L1 so
|
|
118
121
|
composing + persisting a browsed/fetched item (layout, section, or preset)
|
|
119
122
|
onto a page or existing template — target `new_page` / `{template_id,
|
|
120
123
|
element_path}` / `sublayout` — through the SAME hardened write chain as
|
|
@@ -124,21 +127,98 @@ The server exposes:
|
|
|
124
127
|
the chosen photo into the site's Media Library, are direct calls — the
|
|
125
128
|
step that turns a placeholder-filled skeleton into a finished-looking
|
|
126
129
|
page (see "Fill image slots with real on-topic photos" under Layout
|
|
127
|
-
Library).
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
Library). `get_skill` (Design E2, 2026-08-10) is L1 because it is the
|
|
131
|
+
Rettungsanker back to this document: the connect-time `instructions`
|
|
132
|
+
field now carries only a short curated preamble instead of the full
|
|
133
|
+
SKILL.md, so without this tool being directly callable a cold agent
|
|
134
|
+
would have no way to reach the rest of the guide. Call it without
|
|
135
|
+
`topic` for the list of chapters, or with `topic: "<slug>"` for one
|
|
136
|
+
chapter verbatim. `element_unbind_source` is L1 so the write half of
|
|
137
|
+
the bind pair is discoverable the same way its siblings
|
|
138
|
+
(`element_bind_source`, `element_get_binding`, `element_set_source`)
|
|
139
|
+
already are — a customer looking for "how do I remove a binding" could
|
|
140
|
+
otherwise not find it by name in `tools/list`, nor from any of the
|
|
141
|
+
other three tools' own descriptions. `pages_restore` is L1 for the same
|
|
142
|
+
discoverability reason: two cold-customer runs each measured the same gap
|
|
143
|
+
from opposite ends — a trashed page/article could not be found, and even
|
|
144
|
+
once found there was no correct way back — and one caller searched
|
|
145
|
+
explicitly for "restore"/"untrash"/"recover" and found nothing, because a
|
|
146
|
+
gateway-only placement is discoverable only to a caller who already knows
|
|
147
|
+
the tool exists.
|
|
148
|
+
- **13 advanced captured tools**: everything else (
|
|
149
|
+
clean_implode_directives,
|
|
130
150
|
`inspect_sublayout`, `layout_query`, `library_save_as`, the two Local
|
|
131
151
|
Content Read tools `local_content_types` / `local_content_read`,
|
|
132
|
-
`pages_audit`,
|
|
152
|
+
`pages_audit`, `library_delete` — the INVERSE of `library_save_as` —
|
|
153
|
+
`heading_outline`, a real doc-order H1-H6 outline of one page,
|
|
154
|
+
`navigation`, the site's menus as a parent -> children tree,
|
|
155
|
+
`header_transparency_set`, the transparent-header inspect/repair,
|
|
156
|
+
`modules_update`, the widget/module write surface, and
|
|
157
|
+
`modules_create`, the widget/module CREATE surface).
|
|
158
|
+
`modules_delete`, the widget/module DELETE surface, is first-class (L1) —
|
|
159
|
+
not one of these 14 (live-test-befunde, 2026-08-11): the one
|
|
160
|
+
gateway-only destructive tool that removes data OUTSIDE the layout tree (a
|
|
161
|
+
Joomla module row + its `#__modules_menu` assignments), so a host can gate
|
|
162
|
+
it individually instead of only via the all-or-nothing "Advanced Tool
|
|
163
|
+
Gateway" permission.
|
|
164
|
+
To remove a widget/module — the clean-up verb create/update lacked, so a
|
|
165
|
+
probe no longer leaves permanent litter on a customer site — pass the widget
|
|
166
|
+
`id` and PREVIEW first (`confirm` defaults FALSE and the call deletes
|
|
167
|
+
NOTHING, answering with the row plus `usage.element_refs` /
|
|
168
|
+
`usage.assigned_pages`):
|
|
169
|
+
`yootheme_builder_modules_delete({ id: "custom_html-2" })`,
|
|
170
|
+
then repeat with `confirm: true` AND an `intent` — a call missing or
|
|
171
|
+
misnaming it is refused with a 400 naming exactly which intents are
|
|
172
|
+
possible for the module's CURRENT state, each with its own
|
|
173
|
+
`effect`/`reversible`/`native_name`, so calling with neither `confirm` nor
|
|
174
|
+
`intent` doubles as that discovery step.
|
|
175
|
+
WHICH INTENTS ARE OFFERED DEPENDS ON THE PLATFORM AND STATE, because each
|
|
176
|
+
arm mirrors its own CMS rather than inventing a common one. `intent:
|
|
177
|
+
"permanent"` is offered on BOTH platforms and deletes the row for good —
|
|
178
|
+
irreversible, and `modules_create` makes a NEW id with default fields
|
|
179
|
+
rather than restoring this one. `intent: "trash"` exists ONLY on JOOMLA
|
|
180
|
+
and is REVERSIBLE: it sets `published = -2` — the module manager offers
|
|
181
|
+
`Trash` and no direct `Delete` at all, and its `Delete` button exists only
|
|
182
|
+
in the *Trashed* view. The row and its `#__modules_menu` assignments
|
|
183
|
+
survive and Joomla restores both; it does stop rendering at once (Joomla
|
|
184
|
+
loads modules with `published = 1`), so an element bound to it shows
|
|
185
|
+
nothing meanwhile. Joomla offers `"permanent"` directly too — trashing
|
|
186
|
+
first is no longer required to remove a module for good — but once a
|
|
187
|
+
module is ALREADY trashed, `"trash"` stops being offered (only
|
|
188
|
+
`"permanent"` empties it from there). On WORDPRESS widgets have no trash
|
|
189
|
+
step at all (`widgets.php` offers deactivate-by-drag and an immediate
|
|
190
|
+
`Delete` link, not a recoverable one), so only `"permanent"` is ever
|
|
191
|
+
offered there and the confirmed call is always the irreversible one. Read
|
|
192
|
+
the MEASURED `trashed` / `deleted` flags in the response rather than
|
|
193
|
+
assuming which happened.
|
|
194
|
+
Only wanted it off the page, not gone? That is not a removal at all, and
|
|
195
|
+
`modules_delete` is the wrong tool: `modules_update({ id, active: false })`
|
|
196
|
+
parks the widget in WordPress's **Inactive Widgets** area (settings stay
|
|
197
|
+
intact, dragging it back into a widget area restores it) or unpublishes the
|
|
198
|
+
module on Joomla (`published = 0`, position kept) — the same wording the
|
|
199
|
+
400 response's `notes` gives when you call `modules_delete` without an
|
|
200
|
+
`intent`.
|
|
201
|
+
A layout element that references the id DIRECTLY
|
|
202
|
+
(`usage.element_refs` with `via:"element"`) BLOCKS the IRREVERSIBLE step with
|
|
203
|
+
a 409 naming every stranded location, until you either remove those elements
|
|
204
|
+
with `element_delete` or pass `acknowledge_element_refs`: true. Joomla's
|
|
205
|
+
reversible `"trash"` step is never refused — a restore puts the reference
|
|
206
|
+
back in working order — so on that platform the 409 can only come from
|
|
207
|
+
`intent: "permanent"`. An INDIRECT
|
|
208
|
+
reference (`via:"position"`, a `module_position` element naming the POSITION
|
|
209
|
+
this module sits in) never blocks, because it points at a position rather
|
|
210
|
+
than at this module and stays valid for whatever else occupies it.
|
|
211
|
+
`assigned_pages` never blocks either: a Joomla module's `#__modules_menu`
|
|
212
|
+
rows are its OWN rows and go with it.
|
|
133
213
|
Reachable through one gateway tool:
|
|
134
214
|
`yootheme_builder_advanced({ tool: "<name>", arguments: { ... } })`. For
|
|
135
215
|
example, to inspect a panel/grid_item `< SUBLAYOUT`:
|
|
136
|
-
`yootheme_builder_advanced({ tool: "yootheme_builder_inspect_sublayout", arguments: {
|
|
216
|
+
`yootheme_builder_advanced({ tool: "yootheme_builder_inspect_sublayout", arguments: { layout_id, element_path } })`.
|
|
137
217
|
And to find elements by a structured filter (G5 — no expression language,
|
|
138
|
-
AND-combined `
|
|
139
|
-
`yootheme_builder_advanced({ tool: "yootheme_builder_layout_query", arguments: {
|
|
218
|
+
AND-combined `element_type`/`name_matches`/`has_binding`/`max_depth`/`under_section`):
|
|
219
|
+
`yootheme_builder_advanced({ tool: "yootheme_builder_layout_query", arguments: { layout_id, element_type: "headline", has_binding: false, under_section: "Hero" } })`
|
|
140
220
|
(it's read-only, so the read gateway `yootheme_builder_advanced_read({ tool: "yootheme_builder_layout_query", … })` works too).
|
|
141
|
-
And to save a page/subtree INTO "My Layouts" (
|
|
221
|
+
And to save a page/subtree INTO "My Layouts" (the reverse of
|
|
142
222
|
`library_insert`; omit `element_path` to save the whole page):
|
|
143
223
|
`yootheme_builder_advanced({ tool: "yootheme_builder_library_save_as", arguments: { source: { template_id, element_path }, name: "Hero Section" } })`.
|
|
144
224
|
It flags `invalid_source_possible: true` when the saved subtree carries
|
|
@@ -156,21 +236,75 @@ The server exposes:
|
|
|
156
236
|
`yootheme_builder_advanced_read({ tool: "yootheme_builder_pages_audit", arguments: { checks: ["seo", "structure"] } })`
|
|
157
237
|
(the write gateway routes it too, if you prefer one entry point:
|
|
158
238
|
`yootheme_builder_advanced({ tool: "yootheme_builder_pages_audit" })`).
|
|
159
|
-
|
|
160
|
-
|
|
239
|
+
And to get a real, doc-order H1-H6 outline of ONE page (each heading with
|
|
240
|
+
its source element and TEXT, plus the alt-status of every image) —
|
|
241
|
+
read-only STRUCTURE inspection, not findings:
|
|
242
|
+
`yootheme_builder_advanced_read({ tool: "yootheme_builder_heading_outline", arguments: { layout_id } })`
|
|
243
|
+
(or via the write gateway: `yootheme_builder_advanced({ tool: "yootheme_builder_heading_outline" })`).
|
|
244
|
+
And to see the site's Haupt- + Sub-Menüstruktur as a parent -> children
|
|
245
|
+
TREE (not a flat list) — for "which page sits under which menu item"
|
|
246
|
+
(product pages usually live in the main menu), each item carrying a
|
|
247
|
+
`page_role` facet (home/landing/product/legal/thankyou/blog/mockup/
|
|
248
|
+
template/generic, the SAME vocabulary `pages_list` uses) and, when
|
|
249
|
+
derivable at zero extra cost, a `target_page` (`page:<id>` /
|
|
250
|
+
`article:<id>`) ready for `page_get_layout`:
|
|
251
|
+
`yootheme_builder_advanced_read({ tool: "yootheme_builder_navigation" })`
|
|
252
|
+
(or via the write gateway: `yootheme_builder_advanced({ tool: "yootheme_builder_navigation" })`).
|
|
253
|
+
Pass `menu: "Footer"` to narrow to one menu by name.
|
|
254
|
+
And to inspect or repair the transparent header — the navigation pulled over
|
|
255
|
+
the first section — a WRITE, so the write gateway only. `dry_run` defaults to
|
|
256
|
+
TRUE, so this call reports the current state, the theme-declared
|
|
257
|
+
`--uk-inverse`, `risk_class`, `confidence` and `would_change` without
|
|
258
|
+
touching anything:
|
|
259
|
+
`yootheme_builder_advanced({ tool: "yootheme_builder_header_transparency_set", arguments: { layout_id } })`
|
|
260
|
+
Repeat with `mode: "auto", dry_run: false, etag` to write the theme-declared
|
|
261
|
+
colour. See "Transparent header (the exception — never a default)".
|
|
262
|
+
And to update a widget's (WordPress) or module's (Joomla) title,
|
|
263
|
+
position, or theme_overlay — a WRITE, so the write gateway only.
|
|
264
|
+
`dry_run` defaults to TRUE, so this call previews the diff without
|
|
265
|
+
touching anything:
|
|
266
|
+
`yootheme_builder_advanced({ tool: "yootheme_builder_modules_update", arguments: { id: "custom_html-2", title: "New Title" } })`
|
|
267
|
+
Repeat with `dry_run: false` to persist. See "Now change what you found —
|
|
268
|
+
`modules_update`" above for the platform-specific field split.
|
|
269
|
+
And to CREATE a new widget/module — one of exactly five known types
|
|
270
|
+
(WordPress custom_html/text/block; Joomla mod_custom/mod_menu) — a WRITE,
|
|
271
|
+
so the write gateway only. `placement` is REQUIRED (`position` /
|
|
272
|
+
`module_element` / `position_element` — how the module reaches a page); a
|
|
273
|
+
call without it creates NOTHING and returns BOTH halves of the answer:
|
|
274
|
+
`ways`, every way for this platform with its reach and a summary, and
|
|
275
|
+
`notes`, what `modules_create` can build and when you need no module at all.
|
|
276
|
+
Which ways additionally need a `position` is a PLATFORM fact: all three on
|
|
277
|
+
WordPress, two on Joomla. It is never defaulted on either. `dry_run` defaults to TRUE, so once `placement` is chosen this call
|
|
278
|
+
previews (type, position, fields) without creating anything:
|
|
279
|
+
`yootheme_builder_advanced({ tool: "yootheme_builder_modules_create", arguments: { type: "custom_html", title: "New Widget", content: "<p>Hi</p>", placement: "module_element", position: "builder-1" } })`
|
|
280
|
+
Repeat with `dry_run: false` to persist — the response's `row` is the full
|
|
281
|
+
`local_content_read(kind:'modules')` shape and `next_step` is the exact
|
|
282
|
+
follow-up call **for the placement you chose**, which is an `element_add`
|
|
283
|
+
for only two of the three: `module_element` and `position_element` get one,
|
|
284
|
+
`position` gets `modules_update{assignment}` on Joomla and a read-only
|
|
285
|
+
`local_content_read{kind:"modules", include_usage:true}` on WordPress — the
|
|
286
|
+
widget is already live site wide (no write to narrow it with), but add
|
|
287
|
+
`verify_on_urls` to that same call to PROVE guest-visible rendering rather
|
|
288
|
+
than trust `active`/`builder_visible` alone. See "Create → wire in (the
|
|
289
|
+
two-step recipe)" below.
|
|
290
|
+
|
|
291
|
+
The core write/refactor + render-verify tools the customer HANDOFF-4/5 ask for
|
|
161
292
|
are now **L1 — call them directly by name**, not through a gateway:
|
|
162
293
|
- To render-check a saved page over the authenticated channel (F5+N5 — does
|
|
163
294
|
it render, with how many items, or is it silently blank? where an external
|
|
164
295
|
cURL 401s behind staging Basic-Auth), call the now-L1
|
|
165
|
-
`yootheme_builder_render_preview({
|
|
296
|
+
`yootheme_builder_render_preview({ layout_id })` directly — or address it by
|
|
166
297
|
a public URL with `yootheme_builder_render_preview({ url })` (the URL is
|
|
167
298
|
resolved to its template for you, exactly like `page_get_layout`), so if you
|
|
168
299
|
only hold a resolved category/archive URL you can render-check it in ONE call
|
|
169
|
-
without first converting it to a
|
|
300
|
+
without first converting it to a layout_id. It takes no post/term target: it
|
|
170
301
|
renders YOOtheme's implicit fallback context, so a source-bound template comes
|
|
171
302
|
back non-empty. Treat a non-empty render as the success signal, not "blank
|
|
172
|
-
despite a binding". `
|
|
173
|
-
(a static grid/list nested in a repeated
|
|
303
|
+
despite a binding". `rendered_item_wrapper_count` counts the TOP-LEVEL repeat-item
|
|
304
|
+
wrappers the render emitted, BOUND OR NOT (a static grid/list nested in a repeated
|
|
305
|
+
card does not inflate it); `source_item_counts` is the per-source ROW count,
|
|
306
|
+
and `unresolved_sources` names any bound source the registry does not recognise
|
|
307
|
+
(the render still succeeds, only that source contributes nothing). Pass
|
|
174
308
|
`expect: "<substring>"` to confirm a specific edit reached the HTML in one call
|
|
175
309
|
(the response adds `matched` + `snippet`).
|
|
176
310
|
- To surgically SET a precomposed `source` + the native item-level
|
|
@@ -178,30 +312,52 @@ The server exposes:
|
|
|
178
312
|
leaving the rest of the layout byte-unchanged (F2 — the one-call source edit
|
|
179
313
|
bind_source can't do because it cannot write `source_extended`), call the
|
|
180
314
|
now-L1
|
|
181
|
-
`yootheme_builder_element_set_source({
|
|
182
|
-
- To template-wide
|
|
183
|
-
one ETag-locked write — after
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
315
|
+
`yootheme_builder_element_set_source({ layout_id, element_path, source: { query: { name: "categoryFaqs.taxonomyCategoryFaq", field: { name: "faq" } } }, source_extended: { props: { title: { query: { name: "#node" }, name: "title" } } }, etag })` directly.
|
|
316
|
+
- To template-wide REPAIR binding REFERENCES to a source/type/field name
|
|
317
|
+
across EVERY binding in one template, in one ETag-locked write — after
|
|
318
|
+
that name changed OUTSIDE the builder (a taxonomy/CPT rename such as
|
|
319
|
+
faq→help, a re-published flow), preserving each binding's arguments +
|
|
320
|
+
props + field_mappings — this does NOT rename the source itself,
|
|
321
|
+
`sources_list` stays unchanged; call the now-L1
|
|
322
|
+
`yootheme_builder_repair_source_references({ layout_id, from: "categoryFaqs", to: "categoryHelps", source_map: { taxonomyCategoryFaq: "taxonomyCategoryHelp", faq: "help" }, field_map: { faq_title: "help_title" }, etag })` directly.
|
|
323
|
+
`to` (and each `source_map` value landing at the top-level source
|
|
324
|
+
position) must be a currently registered source name — an unregistered
|
|
325
|
+
target is refused with the nearest matches, unless you pass
|
|
326
|
+
`allow_unregistered:true` for the case where the target only exists
|
|
327
|
+
after a flow republish.
|
|
328
|
+
That write is per-template, so find WHICH templates carry the binding FIRST —
|
|
329
|
+
`yootheme_builder_layout_query({ source_name: "categoryFaqs" })` with **no**
|
|
330
|
+
`layout_id` sweeps the whole site and answers per-source counts plus the
|
|
331
|
+
templates each one lives on (add `max_paths` for the individual element
|
|
332
|
+
paths). One call instead of one per template.
|
|
333
|
+
The sweep recognises a binding by the SAME rule `has_binding` uses per
|
|
334
|
+
template, so an unfiltered call splits its `total_bindings` three ways and
|
|
335
|
+
only the first is a source: `sources` (real source names — the ones
|
|
336
|
+
`sources_list` also knows), `inherited_context_bindings` (a `#`-prefixed
|
|
337
|
+
YOOtheme Pro self-reference such as `#parent`, i.e. a binding that INHERITS
|
|
338
|
+
the row context of an iterating ancestor — on real sites this is often the
|
|
339
|
+
majority of the count and it is NOT a source), and `unnamed_bindings`
|
|
340
|
+
(bound via field mappings or a query-field selector only, so there is no
|
|
341
|
+
name for `repair_source_references` to match). Repair covers the first
|
|
342
|
+
bucket; the third names the templates you still have to open by hand.
|
|
187
343
|
- To reassign which archive/single a THEME template applies to — set its
|
|
188
344
|
`type` condition (the value pages_list shows) in one ETag-locked write, the
|
|
189
345
|
step a taxonomy rename leaves orphaned (N3 — re-type
|
|
190
346
|
`taxonomy-category_faq` → `taxonomy-category_help`), call the now-L1
|
|
191
|
-
`yootheme_builder_template_set_type({
|
|
347
|
+
`yootheme_builder_template_set_type({ layout_id, type: "taxonomy-category_help", etag })` directly.
|
|
192
348
|
|
|
193
349
|
To introspect a source's query methods + relations + the current-term
|
|
194
350
|
archive-context convention (U4-L1 — turns a ~25-call YOOtheme code-dive into
|
|
195
351
|
one call before binding on an archive/category template), call the now-L1
|
|
196
352
|
`yootheme_builder_source_get_schema({ name: "categoryFaqs" })` directly.
|
|
197
353
|
- **2 gateway tools**: `yootheme_builder_advanced` (write-capable; can route
|
|
198
|
-
into delete
|
|
199
|
-
(read-only; routes ONLY the read-only advanced subset —
|
|
354
|
+
into delete, so hosts gate it) and `yootheme_builder_advanced_read`
|
|
355
|
+
(read-only; routes ONLY the read-only advanced subset —
|
|
200
356
|
inspect_sublayout, layout_query, local_content_types, local_content_read,
|
|
201
|
-
pages_audit —
|
|
357
|
+
pages_audit, heading_outline, navigation —
|
|
202
358
|
and is read-only-annotated so hosts auto-allow it). Prefer the read gateway
|
|
203
359
|
for pure inspection:
|
|
204
|
-
`yootheme_builder_advanced_read({ tool: "
|
|
360
|
+
`yootheme_builder_advanced_read({ tool: "yootheme_builder_pages_audit", arguments: { site_id } })`.
|
|
205
361
|
Discover the site's NATIVE content the same way — the landscape then the records:
|
|
206
362
|
`yootheme_builder_advanced_read({ tool: "yootheme_builder_local_content_types" })`
|
|
207
363
|
then `yootheme_builder_advanced_read({ tool: "yootheme_builder_local_content_read", arguments: { kind: "terms", taxonomy: "category" } })`.
|
|
@@ -209,9 +365,9 @@ The server exposes:
|
|
|
209
365
|
`yootheme_builder_advanced({ tool: "yootheme_builder_local_content_types" })` and
|
|
210
366
|
`yootheme_builder_advanced({ tool: "yootheme_builder_local_content_read", arguments: { kind: "entities", type: "post" } })`.)
|
|
211
367
|
|
|
212
|
-
`tools/list` therefore advertises
|
|
213
|
-
essentials + 2 L3 direct + 2 gateways. The total callable surface is
|
|
214
|
-
(
|
|
368
|
+
`tools/list` therefore advertises 49 names (45 + 2 + 2). That's 45 L1
|
|
369
|
+
essentials + 2 L3 direct + 2 gateways. The total callable surface is 62
|
|
370
|
+
(49 advertised + 13 advanced reachable through the gateways). If the AI
|
|
215
371
|
client reports "tool not found", you are almost certainly calling an
|
|
216
372
|
advanced tool by its raw name. Wrap it in
|
|
217
373
|
`yootheme_builder_advanced({ tool, arguments })` (or the read gateway for
|
|
@@ -222,10 +378,14 @@ read-only tools) instead.
|
|
|
222
378
|
The server surfaces the connected site's URLs in two places so you never have to
|
|
223
379
|
guess where the agent is pointing:
|
|
224
380
|
|
|
225
|
-
- **`yootheme_builder_health` (Bearer-authenticated)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
381
|
+
- **`yootheme_builder_health` (Bearer-authenticated)** returns `site_url` for
|
|
382
|
+
the connected install, and on WordPress `home_url` as well, because WordPress
|
|
383
|
+
distinguishes the two; Joomla has a single site URL and sends only
|
|
384
|
+
`site_url`. Call it when the user asks "which site are you on?" or before
|
|
385
|
+
deep-linking the user back into wp-admin / Joomla administrator.
|
|
386
|
+
`yootheme_builder_diagnose` carries `site_url` too, so read it there when you
|
|
387
|
+
are already triaging an auth failure — but **only** `site_url`. `home_url` is
|
|
388
|
+
on health alone.
|
|
229
389
|
- **`yootheme_builder_pages_list`** returns per-template `frontend_url`,
|
|
230
390
|
`frontend_url_template`, and `frontend_url_description` columns when the
|
|
231
391
|
host plugin can resolve them. Use these when the user asks for a verification
|
|
@@ -233,9 +393,54 @@ guess where the agent is pointing:
|
|
|
233
393
|
template"): find the matching row, return `frontend_url` (resolved) or
|
|
234
394
|
`frontend_url_template` (with placeholders the user fills in).
|
|
235
395
|
|
|
396
|
+
### `available_endpoints` is request-ready, on both platforms
|
|
397
|
+
|
|
398
|
+
`yootheme_builder_health` also returns `available_endpoints`: every REST path the
|
|
399
|
+
host plugin actually serves. On **both** WordPress and Joomla this list is
|
|
400
|
+
derived from the **live route registration** — WordPress enumerates
|
|
401
|
+
`rest_get_server()->get_routes()`, Joomla reads the ApiRouter that matched the
|
|
402
|
+
request — so it can never drift from what the server accepts.
|
|
403
|
+
|
|
404
|
+
Use it as-is when documenting or probing a site's surface: **every path is
|
|
405
|
+
request-ready as printed**, relative to the platform's REST root
|
|
406
|
+
(`/wp-json` on WordPress, `/api/index.php` on Joomla). Captures appear as
|
|
407
|
+
readable placeholders (`<templateId>`, `<path>`) that you substitute.
|
|
408
|
+
|
|
409
|
+
Do not hand-write a Joomla endpoint list from memory: the Joomla paths carry a
|
|
410
|
+
`yt-builder-mcp` segment (`/v1/yt-builder-mcp/pages/audit`, not `/v1/pages/audit`)
|
|
411
|
+
and the router rejects the short form.
|
|
412
|
+
|
|
413
|
+
If the field is **missing** from a health answer, the host could not read its own
|
|
414
|
+
route table for this request — read that as "not knowable here", never as "this
|
|
415
|
+
install serves nothing". It is omitted rather than sent as an empty list for
|
|
416
|
+
exactly that reason.
|
|
417
|
+
|
|
236
418
|
Treat `frontend_url: null` as "host plugin could not resolve a public URL for
|
|
237
419
|
this template". Surface that honestly rather than fabricating one.
|
|
238
420
|
|
|
421
|
+
### What a write clears, and what it does not
|
|
422
|
+
|
|
423
|
+
`page_save` and `page_publish` say they flush caches. They flush **the caches
|
|
424
|
+
this plugin owns**, and that is a narrower set than "the caches on this site":
|
|
425
|
+
|
|
426
|
+
| layer | who clears it after a write |
|
|
427
|
+
|---|---|
|
|
428
|
+
| YOOtheme's own render / schema cache | this plugin |
|
|
429
|
+
| the option entries this plugin writes | this plugin |
|
|
430
|
+
| the CMS object-cache entry for the page itself | **the CMS core**, on its own post/article save — not this plugin |
|
|
431
|
+
| a full-page cache or CDN (WP Rocket, LiteSpeed, Cloudflare, a reverse proxy) | **nobody, from here** |
|
|
432
|
+
|
|
433
|
+
The last row is the one that reaches visitors. **No full-page-cache or CDN
|
|
434
|
+
plugin was measured**; what was measured is that the CMS core hooks such
|
|
435
|
+
plugins register on do fire during a write. That is an indication, not a
|
|
436
|
+
guarantee — and on Joomla there is no equivalent link at all.
|
|
437
|
+
|
|
438
|
+
So after a publish that must be visible immediately: purge the page cache / CDN
|
|
439
|
+
yourself, then prove it with `yootheme_builder_fetch_public_url`, which fetches
|
|
440
|
+
the page as an anonymous visitor and reports whether a cache served the answer.
|
|
441
|
+
`render_preview` cannot settle this — it renders the stored layout server-side
|
|
442
|
+
and never touches the visitor's path.
|
|
443
|
+
|
|
239
444
|
## Resolve a URL back to the template that renders it
|
|
240
445
|
|
|
241
446
|
The two sections above go template → URL (forward). To go the OTHER way — the
|
|
@@ -244,18 +449,35 @@ complementary tools.
|
|
|
244
449
|
|
|
245
450
|
**`yootheme_builder_pages_resolve({ url })` — the direct reverse lookup.** Paste
|
|
246
451
|
any public frontend URL and it returns
|
|
247
|
-
`{
|
|
452
|
+
`{ layout_id, post_id, type, frontend_url, context, resolved_via }` (or a
|
|
248
453
|
`url_not_resolvable` when it genuinely maps to no template — it degrades to an
|
|
249
454
|
honest null rather than guessing a wrong one). It handles **SEF / pretty
|
|
250
455
|
permalinks on BOTH WordPress and Joomla** (not just raw `?option=…&id=…` query
|
|
251
|
-
URLs), including deep sub-category paths like `/matches/first-men-s-team
|
|
456
|
+
URLs), including deep sub-category paths like `/matches/first-men-s-team` and
|
|
457
|
+
the deep ARTICLE paths a Joomla site prints for an article routed below a
|
|
458
|
+
category menu item (`/company/blog/9-news/36-my-article`) — the id and the alias
|
|
459
|
+
in that last segment are both checked against the article, so an address whose
|
|
460
|
+
alias belongs to something else is refused rather than guessed. For a
|
|
252
461
|
**category or term URL it returns the SPECIFICALLY-ASSIGNED template** — the one
|
|
253
462
|
whose YOOtheme `query.catid` (Joomla) / `query.terms` (WordPress) binds THAT
|
|
254
463
|
category/term, honoring **include-child-categories** (a template assigned to a
|
|
255
464
|
parent category with child-inclusion covers its subcategories; the nearest
|
|
256
465
|
specific assignment wins). Only when nothing is specifically assigned does it
|
|
257
466
|
fall back to the governing generic theme template. Then feed the returned
|
|
258
|
-
`
|
|
467
|
+
`layout_id` straight into `page_get_layout` / `element_list` / the write tools.
|
|
468
|
+
|
|
469
|
+
**The site's own front page resolves too, in every spelling** — bare host,
|
|
470
|
+
trailing slash, `…/index.php`, and the subdirectory forms of an install served
|
|
471
|
+
from one (`https://example.com/joomla/index.php`). It resolves to whatever
|
|
472
|
+
renders it, which on Joomla is usually the single article the default menu item
|
|
473
|
+
points at. `pages_list` hands out one of those spellings as the front page's
|
|
474
|
+
`frontend_url`, so an address this server issues is one it accepts.
|
|
475
|
+
|
|
476
|
+
A front page that resolves to NOTHING is answered by name, not as
|
|
477
|
+
`url_not_resolvable`: the code is **`url_resolves_but_no_layout`** and the
|
|
478
|
+
message says which page was identified and why nothing renders it. Re-spelling
|
|
479
|
+
the URL will not help there — give that page a layout, or address the governing
|
|
480
|
+
template by `layout_id`.
|
|
259
481
|
|
|
260
482
|
**`serves_context_kind` + `serves_context_ids` on `pages_list` rows — resolve it
|
|
261
483
|
yourself, zero extra calls.** Every `pages_list` row also carries these two Layer-C
|
|
@@ -280,6 +502,26 @@ serves_context_ids: [11] }`. Use `pages_resolve` when you just want the answer;
|
|
|
280
502
|
use the `serves_context_*` columns when you already hold `pages_list` and want to
|
|
281
503
|
match locally.
|
|
282
504
|
|
|
505
|
+
**`page_role` on `pages_list` rows — a coarse content-role facet for filtering.**
|
|
506
|
+
Every row also carries `page_role`, one of `home`, `landing`, `product`, `legal`,
|
|
507
|
+
`thankyou`, `blog`, `mockup`, `template`, `generic`. It answers "which of these 80
|
|
508
|
+
rows are actually PRODUCT pages?" without you guessing from names — filter the
|
|
509
|
+
rows you already have by `page_role === "product"` instead of pattern-matching
|
|
510
|
+
labels yourself. It is derived HEURISTICALLY from fields already on the row
|
|
511
|
+
(URL slug, the raw YOOtheme type condition, the row's name) — a legal/thank-you/
|
|
512
|
+
product/landing match comes from the URL path (`/impressum`, `/datenschutz`,
|
|
513
|
+
`/danke`, `/produkt/…`); `home` comes from the site's authoritative front-page
|
|
514
|
+
resolution OR the row's own URL being the bare site root; `blog` from a native
|
|
515
|
+
WP post / Joomla article type; `mockup` from a designer's own "demo"/"mockup"/
|
|
516
|
+
"placeholder" wording in the row's name; `template` from a bare internal
|
|
517
|
+
condition. **`generic` is the honest default, not a bug** — exactly like an
|
|
518
|
+
empty `serves_context_ids`, it means no signal fired, not that classification
|
|
519
|
+
failed. Treat `page_role` as a filter/identification AID, not a guarantee: it
|
|
520
|
+
never re-reads anything (zero extra calls), so it can occasionally miss a page
|
|
521
|
+
whose role isn't visible from its URL/type/name — cross-check with
|
|
522
|
+
`page_get_layout` when precision genuinely matters (e.g. before a destructive
|
|
523
|
+
bulk action).
|
|
524
|
+
|
|
283
525
|
### Address a page WITHOUT its URL — the `target` object
|
|
284
526
|
|
|
285
527
|
`pages_resolve` and `pages_audit` take the **same** `target` object, so one
|
|
@@ -288,7 +530,7 @@ this page". Supply **exactly ONE of** these five keys:
|
|
|
288
530
|
|
|
289
531
|
| Key | Value | Example |
|
|
290
532
|
|-----|-------|---------|
|
|
291
|
-
| `template_id` | An exact
|
|
533
|
+
| `template_id` | An exact layout id | `{ template_id: "page:4816" }` |
|
|
292
534
|
| `post_id` | The numeric WP post / Joomla article id | `{ post_id: 4816 }` |
|
|
293
535
|
| `url` | A public frontend URL (same resolver as `pages_resolve({ url })`) | `{ url: "https://example.com/schedule-demo/" }` |
|
|
294
536
|
| `special` | A semantic page ROLE (vocabulary below) | `{ special: "home" }` |
|
|
@@ -359,17 +601,22 @@ never a transport failure — report it verbatim rather than retrying blindly.
|
|
|
359
601
|
| `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
602
|
| `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
603
|
| `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
|
|
604
|
+
| `unknown_check_category` | 400 | A `checks` value that is not one of the five categories (`seo`, `structure`, `content`, `links`, `design`) — a typo, a finding CODE passed where a category belongs, or a case variant: the vocabulary is **case-sensitive**, so `SEO` is not `seo`. | Re-send with a real category (the message names both the unknown members and the valid set). This is refused rather than filtered because an unrecognised value selects ZERO checks, and zero checks over a normally-scanned site returns `findings: []` — a confident CLEAN verdict for a typo. **Never read such an answer as clean.** |
|
|
605
|
+
| `unknown_audit_mode` | 400 | A `mode` value that is not `builder` or `rendered` (a typo, e.g. `render`). | Re-send with one of the two valid values. Refused rather than silently falling back to `builder`, for the same reason as `unknown_check_category`: nothing was scanned, so the previous answer is NOT a clean result. |
|
|
606
|
+
| `unknown_heading_outline_mode` | 400 | The SAME `mode` vocabulary (`builder`/`rendered`), on `yootheme_builder_heading_outline` instead of `pages_audit` — a dedicated code so the two are never confused. | Re-send with `builder` or `rendered`. |
|
|
363
607
|
| `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
|
-
| `
|
|
608
|
+
| `insufficient_capability` | 403 | The object EXISTS and this key may not read it — a permission answer, not a missing page. The message names the object (`page:5040` / `article:42`). **These tools answer this way**, the ones that read ONE layout: `page_get_layout`, `layout_query` (single template), `element_list`, `element_get`, `element_get_binding`, `inspect_multi_items_binding`, `inspect_sublayout`, `render_preview`, `template_summary`, `heading_outline`. **The others do NOT**, on purpose: `pages_list`, `pages_resolve`, `pages_audit`, `get_etag` and a site-wide `layout_query` sweep report a forbidden object as absent, because a half-sharpened sweep is more misleading than an honestly lenient one. **Never read their silence about an object as proof that it does not exist.** | Do not repeat the call: it gives the same answer while the key stays the same. Ask an administrator to open the object to this key, or use a key whose subject can read it. See [Scopes](#scopes-bearer-key-permissions). |
|
|
609
|
+
| `template_not_found` | 404 | The `layout_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
610
|
| `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
|
|
611
|
+
| `url_no_match` | 404 | The URL is well-formed but this server could not map it to any page at all — neither a per-page layout nor an archive/term/author theme template matched. Note this is NOT the "page exists, has no layout" case: that one has its own code in the next row. | Check the address is this site's and names a public, singular page/article. A LIST view is usually reached with `target: { special: … }` instead. Failing both, browse `pages_list`. |
|
|
612
|
+
| `url_resolves_but_no_layout` | 404 | The URL DID resolve to a page — WordPress mapped it to a post, this server read the Joomla article the address names, or the address is this site's front page — but that page carries no YOOtheme layout of its own; something else renders it. The message names the page. | Nothing here is fixed by re-spelling the address. Give that page a layout in the builder, or find the governing theme template in `pages_list` (its `serves_context_kind` column says which context each one serves) and address it by `layout_id`. |
|
|
367
613
|
| `title_no_match` | 404 | No page NAME contains that substring. | Try a shorter substring, or list with `pages_list`. |
|
|
368
614
|
| `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
615
|
| `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
616
|
| `blog_ambiguous` | 409 | Joomla only: several distinct blog pages. | Same `candidates` shape; show it (see the Joomla caveat above). |
|
|
371
617
|
| `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
618
|
| `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. |
|
|
619
|
+
| `render_failed` | 422 | `yootheme_builder_heading_outline` in `mode:"rendered"` — the target resolved to a real template, but rendering it genuinely failed (a broken binding or other structural problem — NOT a Joomla `module`/`module_position` gap, which the server tolerates transparently; see `render_fallback`/`render_fallback_used` below). | The page is real but cannot currently be rendered. Read the message for the underlying cause; fix the layout, or fall back to `mode:"builder"` (structural inspection over the layout tree, no render needed). |
|
|
373
620
|
|
|
374
621
|
`server_too_old` is the one code the SERVER never sends — see
|
|
375
622
|
[Audit ONE page instead of the whole site](#audit-one-page-instead-of-the-whole-site).
|
|
@@ -395,11 +642,35 @@ Do not loop on auth errors.
|
|
|
395
642
|
> capabilities like `manage_options` gate the admin settings page only; the
|
|
396
643
|
> REST API surface is Bearer-gated.
|
|
397
644
|
|
|
398
|
-
> **Joomla L2 articles surface.** The Joomla plugin ships
|
|
399
|
-
> REST endpoints
|
|
400
|
-
>
|
|
401
|
-
>
|
|
402
|
-
>
|
|
645
|
+
> **Joomla L2 articles surface.** The Joomla plugin ships READ-ONLY
|
|
646
|
+
> `/v1/articles*` REST endpoints (`GET /articles`, `GET /articles/<id>/page-layout`,
|
|
647
|
+
> `GET /articles/<id>/elements/<path>`) for inspecting an article's Builder
|
|
648
|
+
> state, but they are NOT exposed via MCP tools in v1.x. To read that
|
|
649
|
+
> surface, call the REST endpoints directly with the Bearer key. To WRITE
|
|
650
|
+
> an article's layout, address it the same way as any other page — target
|
|
651
|
+
> `article:<id>` with the normal page-editing tools (`page_save`,
|
|
652
|
+
> `element_add`, `element_update_settings`, …); there is no separate write
|
|
653
|
+
> route for articles.
|
|
654
|
+
|
|
655
|
+
### The write budget is attached to the KEY, not to you
|
|
656
|
+
|
|
657
|
+
Writes are rate-limited **per bearer key**. Several clients — several agents,
|
|
658
|
+
several editors, a script running alongside you — sharing one key share **one**
|
|
659
|
+
budget, so a burst that is well within the cap for you alone can still be
|
|
660
|
+
refused. This is reproducible: six agents on one key hit it.
|
|
661
|
+
|
|
662
|
+
`yootheme_builder_health` publishes the cap as `write_rate_limit`
|
|
663
|
+
(`{limit, window_seconds, scope: "kid", applies_to: "writes"}`), read from the
|
|
664
|
+
limiter the site actually enforces. **Read it before a burst instead of
|
|
665
|
+
discovering it as a refusal.** Two bounds worth knowing:
|
|
666
|
+
|
|
667
|
+
- `limit` is the CAP, never the headroom you have left — no counter is read.
|
|
668
|
+
- Reads are not capped, so there is no reason to slow a read loop down for it.
|
|
669
|
+
|
|
670
|
+
Exceeding it answers HTTP 429 with `retry_after_seconds`, and **the write did
|
|
671
|
+
not happen** — wait out the window and retry rather than treating it as a
|
|
672
|
+
failed write to investigate. Anything throttling in FRONT of the site (a CDN,
|
|
673
|
+
a WAF, the host) is invisible here and answers with its own status.
|
|
403
674
|
|
|
404
675
|
---
|
|
405
676
|
|
|
@@ -442,6 +713,16 @@ To verify a specific site before doing work on it, call
|
|
|
442
713
|
and `/etag` in parallel and returns `plugin_reachable` + `bearer_valid`
|
|
443
714
|
without mutating anything.
|
|
444
715
|
|
|
716
|
+
For the whole fleet at once — "which of my sites are up, and how much is on
|
|
717
|
+
them" — call `yootheme_builder_sites_overview()`. It answers in ONE call what
|
|
718
|
+
`sites_list` plus one `sites_test` and one `pages_list` per site answered
|
|
719
|
+
before, probing every site in parallel: one row per site with `reachable`,
|
|
720
|
+
`bearer_valid` and the page counts. A site that is down becomes a row carrying
|
|
721
|
+
`error`, never a failed call — which is the point: you want to see WHICH site
|
|
722
|
+
is broken, not lose the answer for the others. Pass `include_pages: false` to
|
|
723
|
+
skip the per-site page read. The content/theme-template split on a row is
|
|
724
|
+
ABSENT on a host that does not report it; it is never reported as zero.
|
|
725
|
+
|
|
445
726
|
### Default-site mechanics
|
|
446
727
|
|
|
447
728
|
The default site is set automatically on first add:
|
|
@@ -462,7 +743,9 @@ You can store the Bearer key two ways per site:
|
|
|
462
743
|
- **1Password reference**: `bearer_ref: "op://Vault/Item/credential"` in
|
|
463
744
|
`sites.json`. The plaintext token never touches disk. The MCP server shells
|
|
464
745
|
out to the `op` CLI at first use per site to fetch the live token, then
|
|
465
|
-
caches it in memory for that process lifetime.
|
|
746
|
+
caches it in memory for that process lifetime. `op` itself does not cache
|
|
747
|
+
— restart the MCP client after rotating a key so the server re-resolves
|
|
748
|
+
the new value.
|
|
466
749
|
|
|
467
750
|
**Recommendation for production sites**: use `bearer_ref`. You get rotation
|
|
468
751
|
without editing `sites.json`, and your 1Password audit log captures every
|
|
@@ -470,6 +753,24 @@ fetch. The `op` CLI must be installed and signed in on the machine running
|
|
|
470
753
|
the MCP server. If `op` is missing, the resolver returns a structured error
|
|
471
754
|
(`op CLI not found in PATH`) pointing at the install docs.
|
|
472
755
|
|
|
756
|
+
### Prompt friction depends on how the client was launched
|
|
757
|
+
|
|
758
|
+
Per 1Password's own documentation, an app-integration session binds to a
|
|
759
|
+
**TTY** (macOS/Linux) or PID + start time (Windows) — a GUI-launched process
|
|
760
|
+
(Claude Desktop, Cursor) has neither, so the confirmation-free window a
|
|
761
|
+
terminal session gets does not carry over there. Three environment classes,
|
|
762
|
+
worst-first:
|
|
763
|
+
|
|
764
|
+
| Class | Confirmation dialogs | Who |
|
|
765
|
+
|---|---|---|
|
|
766
|
+
| Service account (`OP_SERVICE_ACCOUNT_TOKEN`) | none | agencies, servers, CI |
|
|
767
|
+
| Terminal + app integration | one per window, valid ~10 min | developers running the CLI |
|
|
768
|
+
| **GUI-launched + app integration** | **per call** | **Claude Desktop, Cursor — the customer default** |
|
|
769
|
+
|
|
770
|
+
**For multi-site customers a service account is the frictionless path** —
|
|
771
|
+
worth setting up explicitly rather than discovering the per-call prompts one
|
|
772
|
+
site at a time.
|
|
773
|
+
|
|
473
774
|
### `sites.json` location
|
|
474
775
|
|
|
475
776
|
The registry lives at `~/.config/yt-builder-mcp/sites.json` (XDG-conform).
|
|
@@ -542,9 +843,9 @@ one conversation.
|
|
|
542
843
|
2. For each site, the agent runs the same sequence with `site_id` set:
|
|
543
844
|
- `yootheme_builder_pages_list({ site_id: "wp-acme", fields: ["id", "label"] })`
|
|
544
845
|
- `yootheme_builder_get_etag({ site_id: "wp-acme" })`
|
|
545
|
-
- `yootheme_builder_element_update_settings({ site_id: "wp-acme",
|
|
546
|
-
- `yootheme_builder_page_save({ site_id: "wp-acme",
|
|
547
|
-
- `yootheme_builder_page_publish({ site_id: "wp-acme",
|
|
846
|
+
- `yootheme_builder_element_update_settings({ site_id: "wp-acme", layout_id: "home", element_path: "/children/0/children/0/children/0/children/0", props: { content: "New headline" }, merge: true, etag: "<etag>" })`
|
|
847
|
+
- `yootheme_builder_page_save({ site_id: "wp-acme", layout_id: "home", etag: "<fresh>" })`
|
|
848
|
+
- `yootheme_builder_page_publish({ site_id: "wp-acme", layout_id: "home", etag: "<fresh>" })`
|
|
548
849
|
3. The customer sees a stream of replies, each prefixed with the matching
|
|
549
850
|
`[label @ host]`, so it is obvious which site is at which step.
|
|
550
851
|
|
|
@@ -558,7 +859,7 @@ for a focused diagnosis without touching the others.
|
|
|
558
859
|
## Pages vs theme templates (know what you are editing)
|
|
559
860
|
|
|
560
861
|
`pages_list` returns TWO different kinds of editable layout, and every tool
|
|
561
|
-
takes a `
|
|
862
|
+
takes a `layout_id` that addresses either. Know which one you have before
|
|
562
863
|
you write — they have very different blast radius.
|
|
563
864
|
|
|
564
865
|
| Kind | `id` shape | `type` value(s) | What it is |
|
|
@@ -569,7 +870,7 @@ you write — they have very different blast radius.
|
|
|
569
870
|
**The safety rule: editing a theme template changes the layout for ALL
|
|
570
871
|
matching content, not one page.** If the user says "restyle the blog post
|
|
571
872
|
template", `pages_list` shows it as e.g. `{ id: "I99YS8Ii", label: "Post",
|
|
572
|
-
type: "single-post" }` — editing that `
|
|
873
|
+
type: "single-post" }` — editing that `layout_id` restyles EVERY blog post
|
|
573
874
|
on the site at once. That is often exactly what's wanted (that's what theme
|
|
574
875
|
templates are for), but confirm the intent — do not edit a `single-post` /
|
|
575
876
|
`taxonomy-*` / `archive-*` template when the user meant one specific page.
|
|
@@ -581,6 +882,33 @@ templates are for), but confirm the intent — do not edit a `single-post` /
|
|
|
581
882
|
- `pages_create` creates a **page/article** (an individual layout). Theme
|
|
582
883
|
templates are defined in the theme itself and are not created via this MCP;
|
|
583
884
|
you edit the existing ones `pages_list` reports.
|
|
885
|
+
- `yootheme_builder_pages_delete` removes one — an L1 tool, call it directly
|
|
886
|
+
(no gateway). `confirm: true` is REQUIRED on every call (a bare call is
|
|
887
|
+
refused, `pages.confirm_required`); there is no preview-by-omitting-confirm
|
|
888
|
+
the way `modules_delete` has. Preview instead with `dry_run: true`
|
|
889
|
+
(reports `element_count`, `has_dynamic_binding`, and whether the real call
|
|
890
|
+
would be refused for lacking an `intent`; nothing is written and no
|
|
891
|
+
request is sent). The real call additionally needs `intent`: call
|
|
892
|
+
`confirm: true` without it once and the server refuses with a 400 naming
|
|
893
|
+
which intents are possible for THIS page's/article's CURRENT status, each
|
|
894
|
+
with its own `effect`/`reversible`/`native_name` — the same
|
|
895
|
+
`intent_not_chosen` shape `modules_delete` uses. Unlike modules,
|
|
896
|
+
`intent: "trash"` is REVERSIBLE on BOTH platforms: it sets the WordPress
|
|
897
|
+
`post_status` to `trash` or the Joomla article `state` to `-2`;
|
|
898
|
+
`intent: "permanent"` removes the row and its layout for good on either
|
|
899
|
+
platform, and neither CMS can recreate them. There is a way back from the
|
|
900
|
+
trash step, but it is NOT this tool and NOT `pages_set_status` (see "Finding
|
|
901
|
+
and un-trashing a page" below).
|
|
902
|
+
`yootheme_builder_pages_delete({ layout_id: 42, confirm: true, intent: "trash" })`.
|
|
903
|
+
A page/article ALREADY in the trash short-circuits BEFORE the intent gate:
|
|
904
|
+
passing `intent: "permanent"` still deletes it for good (the only intent
|
|
905
|
+
offered there); passing anything else — no `intent`, `intent: "trash"`, or
|
|
906
|
+
an unrecognised one — instead answers `200 {deleted:false,
|
|
907
|
+
already_trashed:true}`, a MEASUREMENT, not an error, that names
|
|
908
|
+
`intent:"permanent"` or the way back (below) as the two ways forward. The
|
|
909
|
+
EXECUTED trash response (`confirm:true, intent:"trash"`) carries the same
|
|
910
|
+
pointer in its own `note` field — the caller who just trashed something and
|
|
911
|
+
regrets it never has to re-read the preview to find it.
|
|
584
912
|
|
|
585
913
|
**Reaching a theme template by ROLE instead of by id.** You rarely know a theme
|
|
586
914
|
template's opaque id (`I99YS8Ii`). The `special` tokens of the shared `target`
|
|
@@ -601,6 +929,40 @@ response's `resolved_target` tells you which template that actually was. The
|
|
|
601
929
|
same safety rule still applies: a finding on a theme template is a finding on
|
|
602
930
|
EVERY page that template renders.
|
|
603
931
|
|
|
932
|
+
**Finding and un-trashing a page you cannot see in `pages_list`.** A trashed
|
|
933
|
+
page/article never appears there, and by default `local_content_read`
|
|
934
|
+
answers the same `total: 0` for "genuinely nothing" and "it's in the trash" —
|
|
935
|
+
pass `kind: "entities", status: "trash"` for the EXCLUSIVE trashed view (never
|
|
936
|
+
mixed into the default list), with the identifier in `id`. Even without
|
|
937
|
+
asking: a plain `local_content_read({kind:"entities"})` call carries
|
|
938
|
+
`trashed_total` in the response whenever a match exists in the trash, so the
|
|
939
|
+
gap surfaces unasked. Then `yootheme_builder_pages_restore({ layout_id: <id> })`
|
|
940
|
+
brings it back — the ONE correct way, do NOT try `pages_set_status`, which
|
|
941
|
+
refuses a trashed page with a 409 pointing at this tool. **The two platforms
|
|
942
|
+
genuinely diverge here, and the tool says so rather than hiding it**:
|
|
943
|
+
WordPress' `wp_untrash_post()` reinstates BOTH the exact pre-trash status AND
|
|
944
|
+
the pre-trash URL slug (a hand-rolled status write would leave the trash's
|
|
945
|
+
`__trashed` slug suffix in place, silently changing the address) — Joomla
|
|
946
|
+
stores no pre-trash status at all, so a restored article can only become
|
|
947
|
+
`unpublished`; call `pages_set_status` with `status: "published"` afterwards
|
|
948
|
+
to make it visible again.
|
|
949
|
+
|
|
950
|
+
**A trashed Joomla MODULE has no restore tool of its own, and the way out is
|
|
951
|
+
NOT the equivalent of the above — it lands on the opposite side.** Measured:
|
|
952
|
+
`yootheme_builder_modules_update({active:true})` writes `published = 1`, so a
|
|
953
|
+
module comes out of the trash **PUBLISHED** whatever it was before. Joomla
|
|
954
|
+
keeps no pre-trash state for modules either (`#__modules` has one state column
|
|
955
|
+
and the trash writes `-2` over it), but here the single reachable value is the
|
|
956
|
+
VISIBLE one — an unpublished module you trash and bring back is live on the
|
|
957
|
+
site. Its counterpart `{active:false}` is not a second way out: a trashed
|
|
958
|
+
module already reads `active:false`, so that call writes nothing and leaves it
|
|
959
|
+
in the trash, reporting only `changed: []`. Coming back unpublished is
|
|
960
|
+
therefore two calls in order — `{active:true}` then `{active:false}` — and the
|
|
961
|
+
tool says so in `restore_state_note` on the trash row, in the delete preview's
|
|
962
|
+
notes, and in the `notes` of the restoring call itself.
|
|
963
|
+
`local_content_read({kind:"modules"})` lists a trashed module unasked, marked
|
|
964
|
+
`publish_state:"trashed"`.
|
|
965
|
+
|
|
604
966
|
## Workflow 1: Build a hero section
|
|
605
967
|
|
|
606
968
|
**Goal:** Add a fresh hero section (heading + sub-heading + CTA button)
|
|
@@ -616,34 +978,124 @@ to an existing page.
|
|
|
616
978
|
named a specific page, match on `label` (exact then fuzzy).
|
|
617
979
|
3. `yootheme_builder_get_etag()`: fetch the current top-level
|
|
618
980
|
optimistic-lock ETag. Every write tool requires it via `etag`.
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
981
|
+
**Chaining writes: every write answers with `etag` AND `new_etag`, and both
|
|
982
|
+
carry the value read AFTER that write.** They are interchangeable; pass
|
|
983
|
+
either into the next write. You only need `get_etag` again after a 412, or
|
|
984
|
+
when you did not just write.
|
|
985
|
+
4. `yootheme_builder_scaffold_section({ layout_id: "<id>", columns: 1, etag: "<etag>" })`:
|
|
986
|
+
build a VALID `section → row → column` hull at the template root in ONE
|
|
987
|
+
atomic call — the correct way to start a layout block. Only `section` may
|
|
988
|
+
sit at the layout root; a bare section accepts only `row`; a bare row
|
|
989
|
+
accepts only `column`; content/leaf elements (headline, text, button, …)
|
|
990
|
+
are rejected anywhere except inside a `column`. `columns: 1` (the
|
|
991
|
+
default) is a single full-width column — the shape a hero needs.
|
|
992
|
+
Returns `{ section_path: "/children/0/children/N", row_path: "…/children/0",
|
|
993
|
+
column_paths: ["…/children/0/children/0"], etag: "<fresh>" }`. Drop every
|
|
994
|
+
content element below into `column_paths[0]`.
|
|
995
|
+
5. (Optional) `yootheme_builder_element_update_settings({ layout_id, element_path: "<section-path>", props: { style: "primary" }, etag: "<fresh-etag>" })`:
|
|
996
|
+
`scaffold_section` does not take section `props`, so set the background
|
|
997
|
+
as a follow-up call when the hero needs one. `style` (enum: `default` /
|
|
998
|
+
`muted` / `primary` / `secondary`) — there is NO `background` key. Writing
|
|
999
|
+
one is ACCEPTED and stored; the answer names it under `warnings` with
|
|
1000
|
+
`code: "unknown_prop_keys"`, and nothing renders. Always call
|
|
1001
|
+
`element_type_get_schema("section")` first to confirm the real prop keys.
|
|
1002
|
+
6. `yootheme_builder_element_add({ layout_id, parent_path: "<column-path>", element_type: "headline", props: { content: "<h1 text>", title_style: "h1" }, etag })`:
|
|
1003
|
+
add a headline INSIDE the column `scaffold_section` returned
|
|
1004
|
+
(`column_paths[0]`) — never directly in the row or the section. `title_style:
|
|
1005
|
+
"h1"` uses the semantic type scale (the tasteful default). See "Typography
|
|
1006
|
+
& heading styles" below before reaching for a `heading-*` display size.
|
|
1007
|
+
7. `yootheme_builder_element_add({ layout_id, parent_path: "<column-path>", element_type: "text", props: { content: "<sub text>" }, etag })`:
|
|
1008
|
+
add a text element into the SAME column.
|
|
1009
|
+
8. `yootheme_builder_element_add({ layout_id, parent_path: "<column-path>", element_type: "button", etag })`
|
|
636
1010
|
then add a `button_item` CHILD for the actual label/link/style:
|
|
637
|
-
`yootheme_builder_element_add({
|
|
1011
|
+
`yootheme_builder_element_add({ layout_id, parent_path: "<button-path>", element_type: "button_item", props: { content: "<cta>", link: "<url>", button_style: "primary" }, etag })`.
|
|
638
1012
|
The `button` element is a CONTAINER — its label, link and style live on
|
|
639
1013
|
a `button_item` child, NOT on the button's own props. Adding a lone
|
|
640
1014
|
`button` with `{content, link}` renders an empty button. (Same
|
|
641
1015
|
container→item pattern as `grid`→`grid_item`, `list`→`list_item`.)
|
|
642
|
-
9. `yootheme_builder_page_save({
|
|
1016
|
+
9. `yootheme_builder_page_save({ layout_id, etag })`:
|
|
643
1017
|
persist the working copy (visible in YOOtheme Customizer preview).
|
|
644
1018
|
`page_save` is a first-class L1 tool; call it directly.
|
|
645
|
-
|
|
1019
|
+
> **`render_preview` cannot see an element whose rendering depends on CMS
|
|
1020
|
+
> runtime state.** On Joomla a `module` / `module_position` subtree is EXCLUDED
|
|
1021
|
+
> from every render probe, whatever the module's state, and a `newsletter`
|
|
1022
|
+
> element is excluded wherever its provider is unconfigured. The response says
|
|
1023
|
+
> so with `fallback_used:true` + `dropped_count:N`, and `dropped_count_note`
|
|
1024
|
+
> names WHICH types were excluded, why, and that they are still in the saved
|
|
1025
|
+
> layout. So an `expect` aimed at such output can never match: a `matched:false`
|
|
1026
|
+
> there is **not** evidence that the element fails to render, and
|
|
1027
|
+
> `match_conclusive:false` marks exactly that case. A HIT is always conclusive.
|
|
1028
|
+
> To check a MODULE, read its placement with
|
|
1029
|
+
> `local_content_read({kind:"modules", include_usage:true, verify_on_urls:[...]})`.
|
|
1030
|
+
> That read answers for modules only; for any other excluded type it returns
|
|
1031
|
+
> nothing, which is why the response points at it only when a module was
|
|
1032
|
+
> actually excluded.
|
|
1033
|
+
|
|
1034
|
+
10. `yootheme_builder_page_publish({ layout_id, etag })`:
|
|
646
1035
|
make the changes live on the front-end. Also a first-class L1 tool.
|
|
1036
|
+
**`published:true` is a statement about the LAYOUT, not about the page.**
|
|
1037
|
+
A WordPress post left in `draft`, or a Joomla article at `state=0` /
|
|
1038
|
+
trashed, or one whose `publish_up` is still in the future, is not served
|
|
1039
|
+
to a visitor afterwards. Read `cms_visibility.findable_by_visitor` before you
|
|
1040
|
+
tell anyone the page is live; `cms_warning` names the exact fix when it is
|
|
1041
|
+
`false`.
|
|
1042
|
+
**For the ordinary case — build it and make it live — pass the CMS status
|
|
1043
|
+
in the same call**: `page_publish({ layout_id, cms_status: "publish" })` on
|
|
1044
|
+
WordPress, `cms_status: "published"` on Joomla, and on Joomla add
|
|
1045
|
+
`menu_item: true` when the article still needs a SEF route. Same words and
|
|
1046
|
+
same refusals as `pages_set_status`; the other platform's word publishes
|
|
1047
|
+
nothing. The response then carries `cms_status_applied` with the STORED
|
|
1048
|
+
status, and `cms_visibility` reflects the new one. Omit `cms_status` and this
|
|
1049
|
+
call changes nothing about who may see the page — that separation stays, it
|
|
1050
|
+
is simply no longer the only way through.
|
|
1051
|
+
> **On Joomla, "not findable" and "not reachable" are two different things, and
|
|
1052
|
+
> only one of them is a privacy control.** Measured anonymously with curl on
|
|
1053
|
+
> 2026-08-04: the STATE decides the 404 (`state` 1 published and 2 archived both
|
|
1054
|
+
> answer **200**, `state` 0 unpublished and -2 trashed both answer **404**) and
|
|
1055
|
+
> the ACCESS LEVEL decides the 403 (a level a guest does not hold answers **403**
|
|
1056
|
+
> at state 1 and at state 2 alike). A missing menu item decides NEITHER: it
|
|
1057
|
+
> costs the SEF url, the breadcrumb and the navigation path, while the article
|
|
1058
|
+
> keeps answering 200 on its own
|
|
1059
|
+
> `?option=com_content&view=article&id=N` url for anyone. So `findable_by_visitor:
|
|
1060
|
+
> false` on a published article usually means "no visitor will FIND it", not
|
|
1061
|
+
> "no visitor can OPEN it". Two consequences worth saying out loud: **archiving
|
|
1062
|
+
> does not hide anything** (it is a 200), and if an article must not be public,
|
|
1063
|
+
> set its state with `yootheme_builder_pages_set_status` or give it an access
|
|
1064
|
+
> level a guest does not hold. Never leave the menu item off and call that
|
|
1065
|
+
> private.
|
|
1066
|
+
|
|
1067
|
+
**The `cms_visibility` block carries the facts, so you do not have to infer
|
|
1068
|
+
them.** It rides on `page_publish`, `page_generate_dynamic` and `pages_set_status`.
|
|
1069
|
+
Read the field that answers YOUR question:
|
|
1070
|
+
|
|
1071
|
+
| field | question it answers | platform |
|
|
1072
|
+
|---|---|---|
|
|
1073
|
+
| `findable_by_visitor` | will a visitor FIND this via a navigation path? (NOT the same as reaching it: an unrouted article still answers on its direct URL) | both |
|
|
1074
|
+
| `status_permits_direct_url` | does the stored state ALONE serve the item's own direct URL? | both |
|
|
1075
|
+
| `state_and_access_permit_direct_url` | state AND access level together: is it served? | Joomla |
|
|
1076
|
+
| `access_is_guest_visible` | does an anonymous visitor hold this access level? | Joomla, when the probe ran |
|
|
1077
|
+
| `article_access` / `post_status` / `article_state` | the raw stored values | per platform |
|
|
1078
|
+
| `publish_up_in_future` | is it scheduled? | both |
|
|
1079
|
+
| `has_route` | does a menu item route it? (WordPress: always equals published) | both |
|
|
1080
|
+
|
|
1081
|
+
> **One row of that table is NOT in the block, and it is the one people
|
|
1082
|
+
> confuse it with:**
|
|
1083
|
+
>
|
|
1084
|
+
> | field | question it answers | where it rides |
|
|
1085
|
+
> |---|---|---|
|
|
1086
|
+
> | `visibility` | do the STORED FIELDS rule this page out for a visitor? | every `pages_list` row |
|
|
1087
|
+
>
|
|
1088
|
+
> `visibility` reads the CMS status plus, on WordPress, password
|
|
1089
|
+
> protection. It never reads the menu route or the access level, so on
|
|
1090
|
+
> Joomla it answers `undetermined` where `findable_by_visitor` gives a verdict.
|
|
1091
|
+
> That is not a contradiction: the per-page field looked at two levers the
|
|
1092
|
+
> listing never touched, so when they seem to disagree, believe the
|
|
1093
|
+
> per-page one.
|
|
1094
|
+
|
|
1095
|
+
On WordPress the first three necessarily agree: a published page resolves at
|
|
1096
|
+
its permalink and there is no separate route step. On Joomla they come
|
|
1097
|
+
apart, and that divergence is the whole reason the extra fields exist. A
|
|
1098
|
+
`null` in any of them means the input was not readable, never "no".
|
|
647
1099
|
|
|
648
1100
|
> **Building a whole page? Prefer ONE atomic `page_save` over ~30 incremental
|
|
649
1101
|
> writes.** The step-by-step `element_add` sequence above is clearest for
|
|
@@ -652,8 +1104,8 @@ to an existing page.
|
|
|
652
1104
|
> (especially when other agents edit the same site — the builder ETag is a
|
|
653
1105
|
> single shared option). For a full page, assemble the complete
|
|
654
1106
|
> `{type:"layout",children:[...]}` tree in memory and persist it in ONE call:
|
|
655
|
-
> `yootheme_builder_page_save({
|
|
656
|
-
>
|
|
1107
|
+
> `yootheme_builder_page_save({ layout_id, layout: <full-tree> })` (ETag is
|
|
1108
|
+
> required — read it via `yootheme_builder_get_etag` first). `dry_run:true` preflights
|
|
657
1109
|
> it first. A fast, robust pattern: `scaffold_section` one block, read it
|
|
658
1110
|
> back to learn the exact node shape, then build the rest of the tree from
|
|
659
1111
|
> that shape and `page_save` it whole.
|
|
@@ -661,16 +1113,35 @@ to an existing page.
|
|
|
661
1113
|
**Common pitfalls:**
|
|
662
1114
|
|
|
663
1115
|
- **Wrong parameter names.** Every tool uses snake_case. Use
|
|
664
|
-
`
|
|
1116
|
+
`layout_id` (not `pageId`), `parent_path` (not `parentPath`),
|
|
665
1117
|
`element_type` (not `type`), `props` (not `settings`), `etag`
|
|
666
1118
|
(not `ifMatch`). The MCP server rejects unknown keys with a
|
|
667
1119
|
Zod-validation error.
|
|
668
1120
|
- **Forgetting `etag`.** Every write tool needs the latest etag. The
|
|
669
1121
|
shared schema marks it required (min length 1). On `412 Precondition
|
|
670
|
-
Failed
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1122
|
+
Failed`, RE-READ the element or page you were writing before you retry.
|
|
1123
|
+
A fresh etag from `yootheme_builder_get_etag` makes the retry succeed,
|
|
1124
|
+
but the etag is site-wide and its hash does not cover per-page layouts,
|
|
1125
|
+
so a 412 cannot tell you whether the write that bumped it was on YOUR
|
|
1126
|
+
page. Retrying on a fresh etag alone overwrites whatever landed.
|
|
1127
|
+
- **Adding content elements directly to a section or a row.** A section
|
|
1128
|
+
accepts only `row`; a row accepts only `column`; headline/text/button and
|
|
1129
|
+
every other content element are rejected anywhere except inside a
|
|
1130
|
+
`column` — a `422 layout_invalid_hierarchy`. Use `scaffold_section` to
|
|
1131
|
+
build the whole `section → row → column` hull in one call and drop content
|
|
1132
|
+
into the returned `column_paths`; don't hand-build a section + row and add
|
|
1133
|
+
content straight into the row. The server returns a structured error with a
|
|
1134
|
+
human-readable hint when you skip a level.
|
|
1135
|
+
- **Adding a child to a leaf, or hitting an empty element registry.** A leaf
|
|
1136
|
+
type — one that never accepts ANY child, e.g. `text`, `headline`, `button`
|
|
1137
|
+
— refuses every child with `422 layout_leaf_has_children`, a distinct code
|
|
1138
|
+
from the mismatch above. Separately, if YOOtheme's element registry answers
|
|
1139
|
+
empty on the site (usually a transient boot-order state, not a fault in
|
|
1140
|
+
your layout), the parent/child check cannot run at all and the write is
|
|
1141
|
+
refused with `422 registry_unavailable` rather than guessed through —
|
|
1142
|
+
section, row, column and the sublayout are decided without the registry
|
|
1143
|
+
and keep working meanwhile. Both leave the tree untouched; for
|
|
1144
|
+
`registry_unavailable`, give the site a moment and retry.
|
|
674
1145
|
- **Saving without publishing.** `page_save` is the equivalent of the
|
|
675
1146
|
YOOtheme Customizer "Save" button. Content lives in the staging
|
|
676
1147
|
copy. Visitors see nothing until `page_publish`.
|
|
@@ -686,19 +1157,27 @@ to an existing page.
|
|
|
686
1157
|
**Worked example (tool-call snippet):**
|
|
687
1158
|
|
|
688
1159
|
```jsonc
|
|
689
|
-
// Step 4.
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
element_type: "section",
|
|
694
|
-
props: { style: "primary" }, // section background = `style`, NOT `background`
|
|
1160
|
+
// Step 4. Build the section → row → column hull in ONE call.
|
|
1161
|
+
yootheme_builder_scaffold_section({
|
|
1162
|
+
layout_id: "home",
|
|
1163
|
+
columns: 1,
|
|
695
1164
|
etag: "abc123" // from yootheme_builder_get_etag
|
|
696
1165
|
})
|
|
697
|
-
// Response: {
|
|
698
|
-
//
|
|
1166
|
+
// Response: { section_path: "/children/0/children/3", row_path: "/children/0/children/3/children/0",
|
|
1167
|
+
// column_paths: ["/children/0/children/3/children/0/children/0"], etag: "def456" }
|
|
1168
|
+
// → content goes into column_paths[0]; next call uses etag "def456"
|
|
1169
|
+
|
|
1170
|
+
// Step 5 (optional). Set the section background — `style`, NOT `background`.
|
|
1171
|
+
yootheme_builder_element_update_settings({
|
|
1172
|
+
layout_id: "home",
|
|
1173
|
+
element_path: "/children/0/children/3", // the section_path above
|
|
1174
|
+
props: { style: "primary" },
|
|
1175
|
+
etag: "def456"
|
|
1176
|
+
})
|
|
1177
|
+
// Response: { rel_path: "/children/0/children/3", etag: "ghi789" }
|
|
699
1178
|
|
|
700
1179
|
// Step 9. page_save is a first-class L1 tool; call it directly.
|
|
701
|
-
yootheme_builder_page_save({
|
|
1180
|
+
yootheme_builder_page_save({ layout_id: "home", etag: "<latest>" })
|
|
702
1181
|
```
|
|
703
1182
|
|
|
704
1183
|
**Edge case:** YOOtheme allows nested sections (rare). If the user
|
|
@@ -709,11 +1188,169 @@ section sits inside another section.
|
|
|
709
1188
|
**Success criterion:** After `page_publish`, navigating to the page
|
|
710
1189
|
URL on the front-end shows the new hero section above the previous
|
|
711
1190
|
content. Re-reading the layout via
|
|
712
|
-
`yootheme_builder_page_get_layout({
|
|
1191
|
+
`yootheme_builder_page_get_layout({ layout_id })` shows the new
|
|
713
1192
|
section as the last child of the template root.
|
|
714
1193
|
|
|
715
1194
|
---
|
|
716
1195
|
|
|
1196
|
+
## Transparent header (the exception — never a default)
|
|
1197
|
+
|
|
1198
|
+
A transparent header pulls the page's **first** section up behind the site
|
|
1199
|
+
header and recolours the navigation over it. It only works over a hero medium
|
|
1200
|
+
that was made for it. **The default is: do not pull the section behind the
|
|
1201
|
+
header.**
|
|
1202
|
+
|
|
1203
|
+
**Never turn `header_transparent` on by your own initiative** — not while
|
|
1204
|
+
scaffolding a hero, not while inserting a Library layout, not "because it looks
|
|
1205
|
+
more modern". Turn it on only when the customer asks for it, or when the
|
|
1206
|
+
template you inserted already ships a hero medium built for it. **When in
|
|
1207
|
+
doubt, leave it off.** That is always the safe answer, never a compromise.
|
|
1208
|
+
Whoever turns it on picks the colour deliberately; with no deliberate choice
|
|
1209
|
+
the colour is `""`, never `"light"`.
|
|
1210
|
+
|
|
1211
|
+
### The three props (they live on a `section`)
|
|
1212
|
+
|
|
1213
|
+
| Prop | Values | Read by YOOtheme only when |
|
|
1214
|
+
|---|---|---|
|
|
1215
|
+
| `header_transparent` | `true` / `false` | — (this is the master switch) |
|
|
1216
|
+
| `header_transparent_noplaceholder` | `true` / `false` | `header_transparent` is true |
|
|
1217
|
+
| `header_transparent_text_color` | `""` / `"light"` / `"dark"` | `header_transparent` is true |
|
|
1218
|
+
|
|
1219
|
+
### Three traps that break pages silently
|
|
1220
|
+
|
|
1221
|
+
1. **Only the FIRST section counts.** YOOtheme applies the transparent header
|
|
1222
|
+
from the first section of the layout and ignores the props on every later
|
|
1223
|
+
section — no error, no builder warning. "I set it and nothing happened" is
|
|
1224
|
+
almost always this. Put the props on the first section, or remove them.
|
|
1225
|
+
2. **Setting the colour without the master switch does nothing.**
|
|
1226
|
+
`header_transparent_text_color` / `header_transparent_noplaceholder` on a
|
|
1227
|
+
section whose `header_transparent` is off are dead props. They read as
|
|
1228
|
+
configuration and are never applied.
|
|
1229
|
+
3. **`"light"` is not a safe default — it is white navigation text.** The
|
|
1230
|
+
recolour fires even when the section has no image, no video and no `style`,
|
|
1231
|
+
so `"light"` over a light theme background is literally white-on-white. The
|
|
1232
|
+
THEME already states which colour fits each section style (its compiled
|
|
1233
|
+
`theme.css` declares `--uk-inverse` per style, e.g. `dark` for
|
|
1234
|
+
`.uk-section-default`). Read that, or — where no image or video is in the way
|
|
1235
|
+
— leave the colour at `""` and let the theme decide. Never assume `"light"`.
|
|
1236
|
+
|
|
1237
|
+
**Over a hero image or video, no colour choice is provable from the layout
|
|
1238
|
+
alone.** The image hides the background the theme's declaration describes, and
|
|
1239
|
+
which part of a `cover` image ends up under the header depends on the viewport.
|
|
1240
|
+
|
|
1241
|
+
Do not hand-write a colour there — and do **not** "play it safe" by pushing an
|
|
1242
|
+
existing one back to `""`. `""` is the theme's own default colour: a real
|
|
1243
|
+
rendered value, not an abstention. Over a dark hero on `.uk-section-default`
|
|
1244
|
+
(which declares `dark`) reverting to `""` puts dark navigation on a dark photo,
|
|
1245
|
+
which is the very break you were trying to avoid.
|
|
1246
|
+
|
|
1247
|
+
Let the tool leave it alone — `mode: "auto"` writes NO colour over a medium —
|
|
1248
|
+
then LOOK at the rendered page (see "Look at what you built") at the customer's
|
|
1249
|
+
viewport — unscrolled AND after scrolling, because a
|
|
1250
|
+
sticky header drops its transparency on scroll. Then write what you SAW with
|
|
1251
|
+
`mode: "explicit"`. Do not guess a colour, and do
|
|
1252
|
+
not reason about "the top N% of the image".
|
|
1253
|
+
|
|
1254
|
+
### Use `header_transparency_set` — do not hand-write these props
|
|
1255
|
+
|
|
1256
|
+
`yootheme_builder_header_transparency_set` (advanced gateway) is the tool for
|
|
1257
|
+
all of this. It **always targets the first section**, touches only the three
|
|
1258
|
+
props, and **`dry_run` defaults to `true`** — so the first call is an
|
|
1259
|
+
inspection that writes nothing.
|
|
1260
|
+
|
|
1261
|
+
```jsonc
|
|
1262
|
+
// 1. INSPECT (writes nothing — dry_run defaults true, no etag needed).
|
|
1263
|
+
// Returns the current state, the theme-declared --uk-inverse, risk_class,
|
|
1264
|
+
// confidence, a recommendation and would_change/changes.
|
|
1265
|
+
yootheme_builder_header_transparency_set({ layout_id: "home" })
|
|
1266
|
+
|
|
1267
|
+
// 2. REPAIR with the theme's own declared colour.
|
|
1268
|
+
yootheme_builder_header_transparency_set({
|
|
1269
|
+
layout_id: "home", mode: "auto", dry_run: false, etag: "<latest>"
|
|
1270
|
+
})
|
|
1271
|
+
|
|
1272
|
+
// 3. Turn it ON deliberately, without asserting a colour.
|
|
1273
|
+
// mode "auto" derives it; over a hero medium it writes NO colour and tells
|
|
1274
|
+
// you to look at the page instead.
|
|
1275
|
+
yootheme_builder_header_transparency_set({
|
|
1276
|
+
layout_id: "home", mode: "auto", transparent: true, dry_run: false, etag: "<latest>"
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
// 4. A colour you MEASURED over a hero medium (only mode "explicit" takes one).
|
|
1280
|
+
yootheme_builder_header_transparency_set({
|
|
1281
|
+
layout_id: "home", mode: "explicit", text_color: "light",
|
|
1282
|
+
dry_run: false, etag: "<latest>"
|
|
1283
|
+
})
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
`mode: "explicit"` refuses a colour the theme's declaration contradicts when no
|
|
1287
|
+
image or video hides that background (HTTP 409,
|
|
1288
|
+
`error_code: header_contrast_contradicts_theme`). Override with
|
|
1289
|
+
`acknowledge_contrast_risk: true` only after LOOKING at the rendered page.
|
|
1290
|
+
|
|
1291
|
+
To undo it, call with `transparent: false` — the tool removes all three props,
|
|
1292
|
+
so no dead configuration is left behind. It is never refused —
|
|
1293
|
+
**turning transparency OFF is always safe and never needs a justification.**
|
|
1294
|
+
|
|
1295
|
+
### Writing the props by hand — what `element_update_settings` refuses
|
|
1296
|
+
|
|
1297
|
+
You *can* write the props by hand with `element_update_settings` (`merge: true`,
|
|
1298
|
+
on `/children/0`). That path now refuses the one mistake it can prove on its own,
|
|
1299
|
+
and quietly does the safe thing for the other:
|
|
1300
|
+
|
|
1301
|
+
- **Dead props are rejected.** A `header_transparent_text_color` or
|
|
1302
|
+
`header_transparent_noplaceholder` that this write leaves without a true
|
|
1303
|
+
`header_transparent` comes back as
|
|
1304
|
+
`error_code: header_transparent_dead_prop` and nothing is written. A prop
|
|
1305
|
+
YOOtheme never reads is dead whatever the theme declares, so this needs no
|
|
1306
|
+
theme knowledge. Note the merge semantics: `element_update_settings` REPLACES
|
|
1307
|
+
props unless you pass `merge: true`, so a full-replace write that omits
|
|
1308
|
+
`header_transparent` switches the header off.
|
|
1309
|
+
- **Turning it on without a colour writes `""`** — the theme's own default —
|
|
1310
|
+
and tells you so. It is never defaulted to `"light"`. Under `merge: true` the
|
|
1311
|
+
stored colour cannot be read from here, so nothing is seeded and you are
|
|
1312
|
+
asked to state the colour yourself rather than have a guess clobber a value
|
|
1313
|
+
you measured.
|
|
1314
|
+
- **Over a hero image or video the write is ALLOWED**, with a
|
|
1315
|
+
`header_contrast_unverifiable_over_media` warning. Nothing is corrected there
|
|
1316
|
+
— only your eyes can settle it (see "Look at what you built"). That warning
|
|
1317
|
+
**only fires when THIS write states the `image`/`video` itself.** A medium
|
|
1318
|
+
already stored on the section is not readable from here, so under
|
|
1319
|
+
`merge: true` — the shape recommended just above — such a write goes through
|
|
1320
|
+
**silently**. Read no warning as "there is no medium": the usual hand-write
|
|
1321
|
+
(`merge: true` with only the header props, on a section that already has its
|
|
1322
|
+
hero) is exactly that silent case. `pages_audit` and
|
|
1323
|
+
`header_transparency_set` read the stored layout and do see it.
|
|
1324
|
+
|
|
1325
|
+
**What that hand-write path still cannot check for you:** the **first-section
|
|
1326
|
+
rule** — a single element write does not know where its section sits in the
|
|
1327
|
+
layout — and the **theme declaration**, because the compiled `theme.css` is not
|
|
1328
|
+
readable from the MCP server. A colour that contradicts `--uk-inverse` therefore
|
|
1329
|
+
goes through untouched there. `header_transparency_set` and `pages_audit` are
|
|
1330
|
+
where both of those are checked.
|
|
1331
|
+
|
|
1332
|
+
**You do not have to spot this by eye.** `pages_audit` carries two `design`
|
|
1333
|
+
codes for it. Both read the first section's props plus the theme's own
|
|
1334
|
+
`--uk-inverse` declaration for that section style out of the compiled
|
|
1335
|
+
`theme.css`:
|
|
1336
|
+
|
|
1337
|
+
- **`header-contrast-risk`** (severity `warning`) — the layout forces a colour
|
|
1338
|
+
the theme's declaration contradicts, and **no image or video covers** the
|
|
1339
|
+
background that declaration describes. Provable from the props alone: the
|
|
1340
|
+
recommended colour is the theme's own declared value, so it never guesses a colour.
|
|
1341
|
+
- **`header-contrast-unverifiable`** (severity `info`) — a transparent header
|
|
1342
|
+
over a hero image or video, **whatever the colour**. The medium covers exactly
|
|
1343
|
+
the background the declaration describes, so nothing is provable there and the
|
|
1344
|
+
code recommends no colour at all: go and look at the page.
|
|
1345
|
+
|
|
1346
|
+
The split is "is a medium in the way?", never "is the colour right?". Behind a
|
|
1347
|
+
hero medium no value is verifiable and none is safer than another — not even
|
|
1348
|
+
`""`, which is simply the theme's default colour. When `theme.css` cannot be
|
|
1349
|
+
read, both say nothing at all rather than judging on a hunch. See Workflow 6's
|
|
1350
|
+
remediation catalog.
|
|
1351
|
+
|
|
1352
|
+
---
|
|
1353
|
+
|
|
717
1354
|
## Multi-column layouts (columns that sit side-by-side)
|
|
718
1355
|
|
|
719
1356
|
**The single most common layout mistake: a `row` with several `column`
|
|
@@ -770,16 +1407,16 @@ never by narrowing the column. Narrowing a single column WITHOUT a matching
|
|
|
770
1407
|
**Easiest correct way — `scaffold_section` builds the whole factory-shaped
|
|
771
1408
|
hull for you (row grid string + per-column responsive widths in one call):**
|
|
772
1409
|
|
|
773
|
-
- `scaffold_section({
|
|
1410
|
+
- `scaffold_section({ layout_id, columns: 3, etag })` → a section whose row
|
|
774
1411
|
carries `layout: "1-3,1-3,1-3"` and whose 3 columns each carry
|
|
775
1412
|
`width_default: "1-1"` + `width_medium: "1-3"` → clean, gap-free, side-by-side
|
|
776
1413
|
from ≥960px. `columns: N` distributes equal widths (`1-N` for N ≤ 6,
|
|
777
1414
|
`expand` for N > 6).
|
|
778
1415
|
- For an ASYMMETRIC row pass explicit tokens, one per column:
|
|
779
|
-
`scaffold_section({
|
|
1416
|
+
`scaffold_section({ layout_id, columns: 2, column_widths: ["2-3", "1-3"], etag })`
|
|
780
1417
|
→ a row with `layout: "2-3,1-3"` and the matching column widths. A
|
|
781
1418
|
wrong-length array or a bad token returns a friendly `400` before the write.
|
|
782
|
-
- `scaffold_section({
|
|
1419
|
+
- `scaffold_section({ layout_id, columns: 1, etag })` → a single full-width
|
|
783
1420
|
column with no `row.layout` and no column width (the factory single-column
|
|
784
1421
|
shape).
|
|
785
1422
|
|
|
@@ -787,7 +1424,20 @@ hull for you (row grid string + per-column responsive widths in one call):**
|
|
|
787
1424
|
into them with `element_add({ parent_path: "<column-path>", ... })`. If you
|
|
788
1425
|
build columns by hand with `element_add` instead, remember to set BOTH
|
|
789
1426
|
`row.props.layout` (the grid string) AND each column's `width_default` /
|
|
790
|
-
`width_medium`, or the row reads as "Whole" and shows gaps in the builder
|
|
1427
|
+
`width_medium`, or the row reads as "Whole" and shows gaps in the builder:
|
|
1428
|
+
|
|
1429
|
+
```jsonc
|
|
1430
|
+
// Hand-built alternative to scaffold_section — set the grid string on the row…
|
|
1431
|
+
yootheme_builder_element_add({
|
|
1432
|
+
layout_id, parent_path: "<section-path>", element_type: "row",
|
|
1433
|
+
props: { layout: "1-3,1-3,1-3" }, etag,
|
|
1434
|
+
})
|
|
1435
|
+
// …and the matching responsive width on EACH column.
|
|
1436
|
+
yootheme_builder_element_add({
|
|
1437
|
+
layout_id, parent_path: "<row-path>", element_type: "column",
|
|
1438
|
+
props: { width_default: "1-1", width_medium: "1-3" }, etag,
|
|
1439
|
+
})
|
|
1440
|
+
```
|
|
791
1441
|
|
|
792
1442
|
**Success criterion:** in the builder's left panel the row shows N distinct
|
|
793
1443
|
columns (not one column + an empty gap), and after publish the columns sit
|
|
@@ -898,7 +1548,7 @@ token rotates (re-login, session timeout, a different tab), the preview
|
|
|
898
1548
|
renders as a plain page without the builder-data injection and the panel
|
|
899
1549
|
reports "not available". **Fix: reload the Customizer tab** (or re-open the
|
|
900
1550
|
builder from Pages → the page → Builder). The stored layout is intact —
|
|
901
|
-
confirm with `page_get_layout({
|
|
1551
|
+
confirm with `page_get_layout({ layout_id })`, which reads it directly
|
|
902
1552
|
and is unaffected by the browser session.
|
|
903
1553
|
|
|
904
1554
|
---
|
|
@@ -914,6 +1564,148 @@ version on a layout you read-modify-write is preserved, never overwritten.
|
|
|
914
1564
|
|
|
915
1565
|
---
|
|
916
1566
|
|
|
1567
|
+
## Per-element render quirks (the single-element tail)
|
|
1568
|
+
|
|
1569
|
+
The rules that apply to a whole CLASS of props ride in the schema itself:
|
|
1570
|
+
`element_type_get_schema` returns `renders_only_if`, `placeholder_fallback`,
|
|
1571
|
+
`blanked_by_parent`, `media_autoswap`, per-field `value_hint` / `no_effect` /
|
|
1572
|
+
`empty_means_inherit`, and a `rules` id list resolved by a `rule_notes` map
|
|
1573
|
+
(the `enable`-vs-render disagreements, the equal-height model, the `striptags`
|
|
1574
|
+
on link toggles, positional dependence, the collapse cascade, iframe video, the
|
|
1575
|
+
Maps API key, and link attributes under a lightbox). **Read those from the
|
|
1576
|
+
response, not from here.** The response also states which build they were read
|
|
1577
|
+
on — see `claims_verified_on` above.
|
|
1578
|
+
|
|
1579
|
+
What follows is the tail: quirks that belong to ONE element and would cost more
|
|
1580
|
+
in schema bytes than they are worth. Each was read in the YOOtheme **5.0.37**
|
|
1581
|
+
source at the cited location — so on a site running a different build, treat
|
|
1582
|
+
this section the way `claims_verified_on: "differs"` asks you to treat the
|
|
1583
|
+
schema's own claims: a report to check, not a fact about your build.
|
|
1584
|
+
|
|
1585
|
+
| Element / prop | The quirk |
|
|
1586
|
+
|---|---|
|
|
1587
|
+
| `grid.grid_divider` | The divider class is emitted only when the grid has **more than one child** (`grid/templates/template.php:44`, `=> count($children) > 1`). A one-item grid silently has no divider. |
|
|
1588
|
+
| `grid.grid_parallax_justify` | Setting it while `grid_parallax` is empty **forces `grid_parallax` to `'0'`** (`grid/templates/template.php:10-11`). You cannot have justify without parallax being on. |
|
|
1589
|
+
| `grid.panel_style: 'tile-checked'` | A **parent-only** value: the checked pattern comes from `uk-grid-checked` on the grid container, driven by the container's own `panel_style` (`grid/templates/template.php:56`; the render transform branches on it at `grid/element.php:118`). Setting it on an item does nothing. |
|
|
1590
|
+
| `grid_item.tags` | Inert unless the **parent grid's `filter`** is on: the whole tag-collection block runs inside `if (!empty($node->props['filter']))` (`grid/element.php:74`). Tags are also `strip_tags()`'d and their keys have **spaces replaced by dashes** (`:80-83`), so "New York" becomes the key `New-York`. |
|
|
1591
|
+
| `grid_item.title_color` vs `title_decoration` | `title_color: 'background'` and `title_decoration: 'line'` are decided by one `if`/`elseif` (`grid_item/templates/template-content.php:112-114`) — with `background` set, `line` never renders. Not additive. |
|
|
1592
|
+
| `grid.image_vertical_align` | Stops centering once `panel_expand` is active: the class carries `{@!panel_expand}` (`grid_item/templates/template.php:129`). |
|
|
1593
|
+
| `section` without `width` | Still emits a container when the section has a **video or an overlay** (`section/templates/template.php:158`), and that container then also gets `uk-panel` (`:167`) — which zeroes the last child's bottom margin. A width-less section is not always container-less. |
|
|
1594
|
+
| `slideshow` nav (not `thumbnav`) | Borrows the **slide's `title`** as the link text (`slideshow/templates/template-nav.php`: `$thumbnail ?: $child->props['title']`). An untitled slide gets an empty nav link. |
|
|
1595
|
+
| `accordion_item` | Declares **no `title_element`** at all (`grep -c title_element` = 0 in both `accordion_item/element.php` and `accordion/element.php`), so accordion titles can never carry a heading tag and never enter the heading outline. Do not try to write one. |
|
|
1596
|
+
|
|
1597
|
+
## Placing a module or widget
|
|
1598
|
+
|
|
1599
|
+
A module or widget can reach a page in three ways, and they are not
|
|
1600
|
+
interchangeable: they differ in how many pages the thing shows up on, in how
|
|
1601
|
+
many edits one change costs, and in what happens on a page nobody has thought
|
|
1602
|
+
about yet. `modules_create` therefore REQUIRES a `placement` argument and
|
|
1603
|
+
refuses a call without one as `placement_not_chosen`, answering with exactly
|
|
1604
|
+
the rows below for the platform in question, plus that platform's notes.
|
|
1605
|
+
|
|
1606
|
+
The three ways are the same on both platforms. What differs is the vocabulary,
|
|
1607
|
+
and one behaviour: Joomla can narrow a site-wide placement to chosen pages,
|
|
1608
|
+
WordPress cannot.
|
|
1609
|
+
|
|
1610
|
+
**Joomla** (module, template position):
|
|
1611
|
+
|
|
1612
|
+
| `placement` | Reach | What it does |
|
|
1613
|
+
| --- | --- | --- |
|
|
1614
|
+
| `position` | every page | The module sits in a template position such as top and appears on EVERY page. Pass the position with this call. No layout edit, and one module serves the whole site. To narrow it to chosen pages, set the menu assignment with modules_update. |
|
|
1615
|
+
| `module_element` | one page | A Module element inside a column of ONE page layout embeds this module, and it appears on that page only. Call scaffold_section first for a valid column path. |
|
|
1616
|
+
| `position_element` | one page | The module goes into one of the positions builder-1 to builder-6. On its own it appears NOWHERE. A Position element placed in a page layout is what renders it, and it then appears on that page only. |
|
|
1617
|
+
|
|
1618
|
+
Two notes travel with that answer:
|
|
1619
|
+
|
|
1620
|
+
- modules_create builds two native module types (mod_custom, mod_menu). A
|
|
1621
|
+
builder module (mod_yootheme_builder) must already exist. Once it does, all
|
|
1622
|
+
three placements work for it exactly as they do for a native module.
|
|
1623
|
+
- You may not need a module at all: a YOOtheme element such as alert placed
|
|
1624
|
+
straight into a page layout does the same job, at the cost of a separate copy
|
|
1625
|
+
per page.
|
|
1626
|
+
|
|
1627
|
+
**WordPress** (widget, widget area):
|
|
1628
|
+
|
|
1629
|
+
| `placement` | Reach | What it does |
|
|
1630
|
+
| --- | --- | --- |
|
|
1631
|
+
| `position` | every page | The widget sits in a widget area and appears on EVERY page. Pass the area with this call. No layout edit, and one widget serves the whole site. WordPress has no built-in per-page control, so narrowing it to chosen pages needs a plugin. |
|
|
1632
|
+
| `module_element` | one page | A Widget element inside a column of ONE page layout embeds this widget, and it appears on that page only. Needs a position: builder-1 to builder-6, the areas that render nothing on their own. Call scaffold_section first for a valid column path. |
|
|
1633
|
+
| `position_element` | one page | The widget goes into one of the areas builder-1 to builder-6. On its own it appears NOWHERE. An Area element placed in a page layout is what renders it, and it then appears on that page only. |
|
|
1634
|
+
|
|
1635
|
+
And its two notes:
|
|
1636
|
+
|
|
1637
|
+
- modules_create builds three native widget types (custom_html, text, block). A
|
|
1638
|
+
builder widget must already exist. Once it does, all three placements work
|
|
1639
|
+
for it exactly as they do for a native widget.
|
|
1640
|
+
- You may not need a widget at all: a YOOtheme element such as alert placed
|
|
1641
|
+
straight into a page layout does the same job, at the cost of a separate copy
|
|
1642
|
+
per page.
|
|
1643
|
+
|
|
1644
|
+
Which ways need a `position` argument is a PLATFORM fact since 2026-08-08. On
|
|
1645
|
+
JOOMLA it is two of the three: a real theme position (top, bottom, navbar) for
|
|
1646
|
+
`position`, one of builder-1 to builder-6 for `position_element`, and
|
|
1647
|
+
`module_element` takes none, because a module with no position is a native state
|
|
1648
|
+
there. On WORDPRESS it is ALL THREE — a widget exists only inside a registered
|
|
1649
|
+
area, so there is no positionless widget to create, and `module_element` takes
|
|
1650
|
+
builder-1 to builder-6. A real theme position under `module_element` stays
|
|
1651
|
+
refused on both, because the module would ALSO show site wide and render twice
|
|
1652
|
+
on that page. Every way that needs one is REQUIRED, never defaulted: omit it and
|
|
1653
|
+
the call creates NOTHING and comes back `placement_needs_a_position`. A position you DO name is checked against what
|
|
1654
|
+
this site declares or registers, and the two ways it can be wrong are answered
|
|
1655
|
+
by two different codes: a value this site has never heard of (or one too long
|
|
1656
|
+
to be a position at all) comes back as `placement_position_unknown`, and a
|
|
1657
|
+
value that is real but describes a different outcome than the way you chose
|
|
1658
|
+
comes back as `placement_position_conflict`. Each names which half of the call
|
|
1659
|
+
was meant, instead of guessing.
|
|
1660
|
+
|
|
1661
|
+
Two things the layout itself will not let you do: a module or module_position
|
|
1662
|
+
element belongs inside a `column`, never on the layout root (the root takes
|
|
1663
|
+
`section` only, and a root-level module renders outside the section flow, over
|
|
1664
|
+
the navigation on a transparent header), and a `module_position` element renders
|
|
1665
|
+
with YOOtheme's own default layout (`stack`) when you leave the `layout` prop
|
|
1666
|
+
out. Set it only if you want `grid`. Never copy the `sidebar` value the element
|
|
1667
|
+
schema reports as that field's default: it is not one of the two accepted values
|
|
1668
|
+
and is rejected with a 422.
|
|
1669
|
+
|
|
1670
|
+
**"Every page" above is the assignment a `position` placement makes. On
|
|
1671
|
+
Joomla it takes effect immediately: every module write evicts Joomla's own
|
|
1672
|
+
module-list cache exactly like the native module manager does. On WordPress
|
|
1673
|
+
the widget write goes through `update_option()`, which refreshes WordPress'
|
|
1674
|
+
own options cache the same way any core option save does - if the site runs
|
|
1675
|
+
a persistent object-cache drop-in, this server behaves exactly as WordPress
|
|
1676
|
+
itself does there, no better and no worse. If a write looks invisible
|
|
1677
|
+
anyway, you are almost certainly looking at one of two things. (1) On
|
|
1678
|
+
Joomla, the site caches aggressively and the module was written by an OLDER
|
|
1679
|
+
plugin version or by a tool that bypasses the module manager - Joomla's
|
|
1680
|
+
conservative cache then serves the pre-write module list for up to `cachetime`
|
|
1681
|
+
(15 minutes by default), while the database row is already correct. Clearing
|
|
1682
|
+
the site cache or waiting resolves it; nothing is wrong with the module. (2)
|
|
1683
|
+
The position is one of the `builder-N` slots, which never render standalone -
|
|
1684
|
+
they reach a page only through a Position/Area element. For a quick
|
|
1685
|
+
confirmation either way, call `local_content_read` with `include_usage:true`
|
|
1686
|
+
and `verify_on_urls:["<a page url>"]` and read `usage.observed.urls[].status`
|
|
1687
|
+
(`present`/`absent`/`not_detectable`/`error` - see T1.7 in the local-content
|
|
1688
|
+
chapter), or fetch the page as a guest and search for the module's own
|
|
1689
|
+
signature (`id="<widget-id>"` on WordPress, `id="module-<id>"` on Joomla).
|
|
1690
|
+
|
|
1691
|
+
**Before doing N per-page edits, check whether one site-wide module already
|
|
1692
|
+
covers the need.** If a site-wide Builder module already sits in the header
|
|
1693
|
+
area, adding the new content to that ONE module can replace a separate edit
|
|
1694
|
+
on every page. A `local_content_read({kind:"modules"})` pass (T1.6 shows
|
|
1695
|
+
what `usage` returns) tells you what is already there before you start a
|
|
1696
|
+
per-page loop.
|
|
1697
|
+
|
|
1698
|
+
**A module or widget placed BOTH in its position and as an element on the same
|
|
1699
|
+
page renders twice.** This is not a hypothetical: the T1.7 section of the
|
|
1700
|
+
local-content chapter documents it measured live on
|
|
1701
|
+
`dev5wp.wootsup.com/mcp-module-probe/`, where a widget carrying both an element
|
|
1702
|
+
reference and a position reference served its content twice on one document.
|
|
1703
|
+
Before adding a `module`/`module_position` element for something that is ALSO
|
|
1704
|
+
assigned a rendering position, check its `usage.element_refs` first (T1.6), or
|
|
1705
|
+
drop the position assignment.
|
|
1706
|
+
|
|
1707
|
+
---
|
|
1708
|
+
|
|
917
1709
|
## Workflow 2: Bind a dynamic source to a grid
|
|
918
1710
|
|
|
919
1711
|
**Goal:** Wire an existing Grid (or other multi-item element) to a
|
|
@@ -924,30 +1716,30 @@ renders dynamic items.
|
|
|
924
1716
|
|
|
925
1717
|
1. `yootheme_builder_health`: confirm host plugin reachable.
|
|
926
1718
|
2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` and
|
|
927
|
-
`yootheme_builder_page_get_layout({
|
|
1719
|
+
`yootheme_builder_page_get_layout({ layout_id: "<id>", flat: false })`:
|
|
928
1720
|
locate the target Grid. Note its JSON-Pointer `path` (e.g.
|
|
929
1721
|
`/children/0/children/2/children/0`).
|
|
930
|
-
3. `yootheme_builder_element_get({
|
|
1722
|
+
3. `yootheme_builder_element_get({ layout_id, element_path })`:
|
|
931
1723
|
fetch the Grid's current props so you can preserve them. Binding
|
|
932
1724
|
sets `props.source` and leaves the rest alone.
|
|
933
1725
|
4. `yootheme_builder_sources_list()`: enumerate available Sources.
|
|
934
1726
|
Each returns `{ name, label, origin, kind }`. Pick the one the
|
|
935
1727
|
user asked for.
|
|
936
|
-
5. `yootheme_builder_element_get_binding({
|
|
1728
|
+
5. `yootheme_builder_element_get_binding({ layout_id, element_path })`
|
|
937
1729
|
check whether the Grid is already bound (idempotency: skip step
|
|
938
1730
|
7 if `source_name` already matches the user's intent).
|
|
939
1731
|
`element_get_binding` is a first-class L1 tool (B4) — call it directly.
|
|
940
1732
|
6. `yootheme_builder_get_etag()`: fetch the optimistic-lock etag for
|
|
941
1733
|
the upcoming mutation.
|
|
942
|
-
7. `yootheme_builder_element_bind_source({
|
|
1734
|
+
7. `yootheme_builder_element_bind_source({ layout_id, element_path, source_name: "<name>", etag: "<etag>" })`
|
|
943
1735
|
applies the binding. Returns `{ path, etag, has_binding: true }`.
|
|
944
1736
|
Pass `source_id: "<origin>:<name>"` as well **only** when two
|
|
945
1737
|
plugins register a source with the same `source_name` (the server
|
|
946
1738
|
surfaces the ambiguity as an elicitation prompt; if the host
|
|
947
1739
|
doesn't support elicitation you'll see a structured error listing
|
|
948
1740
|
the candidates). This is a first-class L1 tool — call it directly.
|
|
949
|
-
8. `yootheme_builder_page_save({
|
|
950
|
-
then `yootheme_builder_page_publish({
|
|
1741
|
+
8. `yootheme_builder_page_save({ layout_id, etag: "<fresh>" })`
|
|
1742
|
+
then `yootheme_builder_page_publish({ layout_id, etag: "<fresh>" })`.
|
|
951
1743
|
|
|
952
1744
|
**Common pitfalls:**
|
|
953
1745
|
|
|
@@ -957,9 +1749,27 @@ renders dynamic items.
|
|
|
957
1749
|
`relation`/`field`, `arguments` and `directives`; see the Tool Catalog
|
|
958
1750
|
appendix for the full `element_bind_source` input-key list rather than
|
|
959
1751
|
guessing a subset.
|
|
960
|
-
- **Wrong parameter names.** Use `
|
|
1752
|
+
- **Wrong parameter names.** Use `layout_id` (not `pageId`),
|
|
961
1753
|
`element_path` (not `path`), `source_name` (not `sourceName`),
|
|
962
1754
|
`etag` (not `ifMatch`).
|
|
1755
|
+
- **A misspelled `field_mappings` KEY is refused, and be glad it is.** The keys
|
|
1756
|
+
are element PROPS; the values are source FIELDS. A key that is not a prop of
|
|
1757
|
+
the target element is rejected with `422 unknown_prop_keys`, listing
|
|
1758
|
+
`valid_prop_keys` and `did_you_mean`, and nothing is written. The reason is
|
|
1759
|
+
the ASYMMETRY, and the honest form of it is two measurements that disagree:
|
|
1760
|
+
on a native `articles` source, `{title, content}` rendered 10 rows while
|
|
1761
|
+
`{titel, content}` rendered **0** — the untouched `content` included; on a
|
|
1762
|
+
`grid_item` at an API-Mapper list source, the same substitution left all 5
|
|
1763
|
+
rows standing and cost only that slot. So a misspelled key sometimes empties
|
|
1764
|
+
the WHOLE binding and sometimes only its own slot, `element_get_binding`
|
|
1765
|
+
reads either back as healthy, and which one you get is not established. A
|
|
1766
|
+
wrong refusal costs you one call and tells you the valid names; a wrong
|
|
1767
|
+
acceptance can cost the page in silence. If you genuinely need a prop this
|
|
1768
|
+
build does not declare, `element_set_source` writes the carriers verbatim and
|
|
1769
|
+
only warns.
|
|
1770
|
+
(This is the opposite of the free `props` path — see Workflow 5, where an
|
|
1771
|
+
undeclared key is stored with a warning, because there real layouts carry
|
|
1772
|
+
such props and refusing them would refuse YOOtheme's own work.)
|
|
963
1773
|
- **Source not in the list.** API Mapper sources only appear once
|
|
964
1774
|
they're PUBLISHED in API Mapper (not just saved). If you bind an
|
|
965
1775
|
unknown `source_name`, `element_bind_source` rejects it and the error
|
|
@@ -974,8 +1784,9 @@ renders dynamic items.
|
|
|
974
1784
|
Switcher, Table) accept a source binding. Binding a single-item
|
|
975
1785
|
element like Headline returns a structured `validation` error.
|
|
976
1786
|
- **Forgetting `etag`.** Every write requires the optimistic-lock
|
|
977
|
-
etag. On `412 Precondition Failed
|
|
978
|
-
|
|
1787
|
+
etag. On `412 Precondition Failed`, re-read what you were writing
|
|
1788
|
+
first — a fresh etag alone makes the retry succeed without telling you
|
|
1789
|
+
whether someone else's change is what you are about to overwrite.
|
|
979
1790
|
- **Mis-routing bind vs the read helpers.** `element_bind_source` AND
|
|
980
1791
|
its verify sibling `element_get_binding` (B4) are both first-class L1
|
|
981
1792
|
tools — call them directly by name. Only `element_unbind_source` stays
|
|
@@ -988,7 +1799,7 @@ renders dynamic items.
|
|
|
988
1799
|
// Step 7. Bind a Posts source onto a Grid element. element_bind_source
|
|
989
1800
|
// is a first-class L1 tool — call it directly.
|
|
990
1801
|
yootheme_builder_element_bind_source({
|
|
991
|
-
|
|
1802
|
+
layout_id: "home",
|
|
992
1803
|
element_path: "/children/0/children/2/children/0",
|
|
993
1804
|
source_name: "wp_posts",
|
|
994
1805
|
etag: "abc123"
|
|
@@ -997,16 +1808,55 @@ yootheme_builder_element_bind_source({
|
|
|
997
1808
|
// Response: { rel_path: "/children/0/children/2/children/0", etag: "def456", has_binding: true }
|
|
998
1809
|
// Verify (direct L1 call):
|
|
999
1810
|
yootheme_builder_element_get_binding({
|
|
1000
|
-
|
|
1811
|
+
layout_id: "home", element_path: "/children/0/children/2/children/0"
|
|
1001
1812
|
})
|
|
1002
1813
|
// → { source_name: "wp_posts", source_config: { ... }, ... }
|
|
1003
1814
|
```
|
|
1004
1815
|
|
|
1005
|
-
**Edge case:** A Source can render zero items at runtime (e.g. empty
|
|
1006
|
-
search filter
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1816
|
+
**Edge case:** A Source can render zero items at runtime (e.g. an empty
|
|
1817
|
+
search filter, or an archive source on a single page that has no term to
|
|
1818
|
+
scope to). The bind call still succeeds; the front-end Grid just shows the
|
|
1819
|
+
YOOtheme "no items" placeholder. That is a DATA state, not a broken binding.
|
|
1820
|
+
|
|
1821
|
+
**But do not verify it by re-reading the binding.** `element_get_binding`
|
|
1822
|
+
answers "is a source attached", which is a different question from "does it
|
|
1823
|
+
deliver rows", and no amount of re-reading it will ever turn into the second
|
|
1824
|
+
answer. A customer once bound, published, looked, re-bound with `limit`,
|
|
1825
|
+
published, looked again, and only then ran `render_preview` — two full cycles
|
|
1826
|
+
for one fact.
|
|
1827
|
+
|
|
1828
|
+
There is ONE thing the re-read does now answer, and it is the one a blank card
|
|
1829
|
+
sends you looking for: if a `field_mappings` VALUE names a source field the
|
|
1830
|
+
source does not emit, `element_get_binding` says so — the same three keys the
|
|
1831
|
+
bind answers with, under the same names: `unknown_source_field_values`,
|
|
1832
|
+
`unknown_source_field_values_available` (what you should have written) and
|
|
1833
|
+
`unknown_source_field_values_hint`. That warning used to appear exactly once, at the
|
|
1834
|
+
moment of writing, so whoever debugged a blank card LATER read the state back
|
|
1835
|
+
and saw a binding indistinguishable from a healthy one. Absence of the keys
|
|
1836
|
+
means "nothing to report OR the source's field set could not be resolved here",
|
|
1837
|
+
never "checked and fine" — and it says nothing about the KEY side: a misspelled
|
|
1838
|
+
element prop is still read back as healthy, which is why the bind refuses it up
|
|
1839
|
+
front instead.
|
|
1840
|
+
|
|
1841
|
+
What the tools give you instead:
|
|
1842
|
+
|
|
1843
|
+
- Every bind answers with a `checks` block: `source_exists`, `field_mappings`,
|
|
1844
|
+
`field_mapping_props_checked`, `container_item_ok`. It costs nothing and is
|
|
1845
|
+
always there. It also states its own limit — none of them says whether rows
|
|
1846
|
+
arrive. Read the pair `field_mappings` + `field_mapping_props_checked`
|
|
1847
|
+
together: the first is a COUNT, not a verdict (a customer read
|
|
1848
|
+
`field_mappings: 2` as confirmation while one of the two was a typo), the
|
|
1849
|
+
second says whether those keys were measured against the element's declared
|
|
1850
|
+
props at all — `false` means the type's field list did not resolve here, not
|
|
1851
|
+
that a check failed.
|
|
1852
|
+
- `element_bind_source({… , probe: true})` renders the binding and answers
|
|
1853
|
+
`probe: {rows, rendered, empty, probed_path}`; at zero rows it adds a warning
|
|
1854
|
+
naming the likely causes. It costs a real render (~0.3-0.6s), so it is off by
|
|
1855
|
+
default. `probed_path` is the node that was rendered — the nearest container,
|
|
1856
|
+
because a `grid_item` renders nothing outside its `grid`.
|
|
1857
|
+
- `page_publish` / `page_save` report `binding-renders-nothing` in `quick_check`
|
|
1858
|
+
when the page's render produced zero rows and something on it was supposed to
|
|
1859
|
+
iterate. That one is free: the publish already renders for its own precheck.
|
|
1010
1860
|
|
|
1011
1861
|
**Success criterion:** After publish, the Grid on the front-end shows
|
|
1012
1862
|
items from the Source (verify by item count and at least one
|
|
@@ -1067,58 +1917,476 @@ a row layout. (Cross-template duplication is **not** supported by
|
|
|
1067
1917
|
`element_clone`. See "Important scope note" below.)
|
|
1068
1918
|
|
|
1069
1919
|
**Important scope note:** `yootheme_builder_element_clone` is
|
|
1070
|
-
**
|
|
1071
|
-
`{
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1920
|
+
**intra-template**. Its real schema is
|
|
1921
|
+
`{ layout_id, element_path, etag, to_parent_path?, to_index? }`.
|
|
1922
|
+
There is **no** `destPageId` and no cross-template argument. Omit
|
|
1923
|
+
`to_parent_path` and the copy lands at the same parent, right after
|
|
1924
|
+
the source; pass it and the copy lands under that parent instead
|
|
1925
|
+
(`to_index` picks the slot, default append) — the follow-up
|
|
1926
|
+
`element_move` this used to require is no longer needed. To duplicate
|
|
1927
|
+
into a DIFFERENT template, flag to the user that cross-template clone
|
|
1928
|
+
is not currently supported and suggest a CMS-level template
|
|
1929
|
+
duplication (in wp-admin or Joomla administrator).
|
|
1078
1930
|
|
|
1079
1931
|
**Canonical tool-call sequence (real parameter names, snake_case):**
|
|
1080
1932
|
|
|
1081
1933
|
1. `yootheme_builder_health`: confirm host plugin reachable.
|
|
1082
1934
|
2. `yootheme_builder_pages_list({ fields: ["id", "label"] })`:
|
|
1083
1935
|
locate the template by `label`. Note its `id`.
|
|
1084
|
-
3. `
|
|
1085
|
-
returns a flat
|
|
1086
|
-
element path + type. Pick the JSON-Pointer path of the
|
|
1087
|
-
to clone. `
|
|
1936
|
+
3. `yootheme_builder_element_list({ layout_id })`
|
|
1937
|
+
returns a flat view (lighter than `page_get_layout`) showing every
|
|
1938
|
+
element path + type + label + binding. Pick the JSON-Pointer path of the
|
|
1939
|
+
section to clone. `element_list` is L1; call it directly, no gateway.
|
|
1088
1940
|
4. `yootheme_builder_get_etag()`: fetch the optimistic-lock etag.
|
|
1089
|
-
5. `yootheme_builder_element_clone({
|
|
1941
|
+
5. `yootheme_builder_element_clone({ layout_id, element_path: "<src-path>", etag: "<etag>" })`
|
|
1090
1942
|
clones as sibling. Returns `{ path: "<new-path>", etag: "<fresh>" }`.
|
|
1091
1943
|
The new path is at the same parent, immediately after the source.
|
|
1092
|
-
6. (Optional)
|
|
1093
|
-
|
|
1094
|
-
|
|
1944
|
+
6. (Optional) To land the copy somewhere other than beside its source,
|
|
1945
|
+
pass the destination to step 5 directly:
|
|
1946
|
+
`yootheme_builder_element_clone({ layout_id, element_path: "<src-path>", to_parent_path: "<other-parent>", to_index: 0, etag: "<etag>" })`.
|
|
1947
|
+
`yootheme_builder_element_move` remains the way to relocate an
|
|
1948
|
+
element that already exists.
|
|
1949
|
+
7. `yootheme_builder_element_update_settings({ layout_id, element_path: "<final-path>", props: { ... }, etag: "<fresh>" })`
|
|
1095
1950
|
replaces the `props` on the clone. **Existing props NOT in the
|
|
1096
1951
|
request are removed** (update_settings is a full replace by default;
|
|
1097
1952
|
pass `merge: true` to apply a server-side deep-merge instead).
|
|
1098
1953
|
Read the current props first via `yootheme_builder_element_get`
|
|
1099
1954
|
if you only want to tweak a subset.
|
|
1100
|
-
8. `yootheme_builder_page_save({
|
|
1101
|
-
then `yootheme_builder_page_publish({
|
|
1955
|
+
8. `yootheme_builder_page_save({ layout_id, etag })`
|
|
1956
|
+
then `yootheme_builder_page_publish({ layout_id, etag })`.
|
|
1102
1957
|
|
|
1103
1958
|
**Common pitfalls:**
|
|
1104
1959
|
|
|
1105
1960
|
- **Inventing destination parameters.** `element_clone` does NOT
|
|
1106
|
-
accept `destPageId
|
|
1107
|
-
|
|
1961
|
+
accept `destPageId` or any cross-template argument. Its destination
|
|
1962
|
+
is optional and stays within ONE template: `to_parent_path` plus
|
|
1963
|
+
`to_index`, both snake_case.
|
|
1108
1964
|
- **Treating `element_update_settings` as a merge by default.** The handler
|
|
1109
1965
|
REPLACES the entire `props` object on the element unless you pass
|
|
1110
1966
|
`merge: true`. Read the existing shape via `yootheme_builder_element_get`
|
|
1111
1967
|
first if you only want to tweak a subset and prefer not to use merge.
|
|
1968
|
+
- **Reading a prop's value never needs a second call.** `element_get`'s
|
|
1969
|
+
output, both the JSON and the printed summary, lists every prop KEY
|
|
1970
|
+
together with its VALUE (e.g. `title_element: "h1"`), each value capped
|
|
1971
|
+
at roughly 200 characters with a visible `…(+N chars)` marker when
|
|
1972
|
+
truncated, never a silent drop. There's no need to cross-check via
|
|
1973
|
+
`element_list` just to see what a specific prop is currently set to.
|
|
1974
|
+
- **A `module`/`module_position` element is not a dead end either (T1.4).**
|
|
1975
|
+
On its own, a WordPress Widget or Joomla Module element's `props` are just
|
|
1976
|
+
a reference (`{widget: "custom_html-1"}` / `{module: 42}`) — no title, no
|
|
1977
|
+
content, no settings. `element_get` on that node ALSO carries a `resolved`
|
|
1978
|
+
field: the referenced widget/module's own row (title/type/settings/
|
|
1979
|
+
theme_overlay — the SAME shape `local_content_read(kind:"modules")`
|
|
1980
|
+
returns), or `resolved: null` plus a `resolved_reason` string when it can't
|
|
1981
|
+
be resolved (missing reference, deleted id, empty position) — never a
|
|
1982
|
+
throw. A `module_position` node resolves to `{position, modules: [...]}`,
|
|
1983
|
+
every widget assigned there. There's no need to separately call
|
|
1984
|
+
`local_content_read` just to see what a module element actually shows.
|
|
1985
|
+
- **"Where does this widget/module appear?" — `include_usage:true` (T1.6).**
|
|
1986
|
+
`local_content_read({ kind: "modules", include_usage: true })` adds a `usage`
|
|
1987
|
+
object per row: `element_refs` is the EXACT `{layout_id, path,
|
|
1988
|
+
via:"element"|"position"}` list of every layout location that reaches this
|
|
1989
|
+
widget/module — `via:"element"` is a DIRECT `module` element reference,
|
|
1990
|
+
`via:"position"` is an INDIRECT one (a `module_position` element pointing at
|
|
1991
|
+
this row's own `position`) — so you know whether to edit the element or the
|
|
1992
|
+
position assignment. `declared.position_visibility` classifies the row's
|
|
1993
|
+
`position` into `site-wide` (a theme panel/area — appears wherever that
|
|
1994
|
+
position renders), `never-standalone` (a `builder-1`..`builder-6` position —
|
|
1995
|
+
RENDERS ONLY through a `module_position` element, never on its own — live
|
|
1996
|
+
A/B-proven on Joomla), or `not-on-builder-pages` (`sidebar` — not rendered on
|
|
1997
|
+
Builder pages); null when the row has no position at all.
|
|
1998
|
+
`declared.assigned_pages` is Joomla-only real assignment info (`mode`
|
|
1999
|
+
`all`/`except`/`selected`/`none` resolved to concrete menu-item titles); on
|
|
2000
|
+
WordPress it is the explicit platform fact `{applicable:false, reason:
|
|
2001
|
+
"WordPress widget areas are global — widgets are not assigned to individual
|
|
2002
|
+
pages."}` — never simply absent. `declared` states what the CONFIGURATION
|
|
2003
|
+
claims, not what a visitor actually sees — a visibility plugin (Advanced
|
|
2004
|
+
Module Manager, Widget Context, …) or a time-gated rule can still override
|
|
2005
|
+
it. Default `false`: the layout scan this costs is never paid unasked.
|
|
2006
|
+
**A row with NO `usage` is not automatically a row you did not ask about.**
|
|
2007
|
+
There are THREE states and the key `usage_unavailable` is what separates the
|
|
2008
|
+
last two: neither `usage` nor `usage_unavailable` means `include_usage` was
|
|
2009
|
+
not requested; `usage` present means the enrichment ran; `usage_unavailable`
|
|
2010
|
+
present means it WAS requested and FAILED for that row. The row's primary
|
|
2011
|
+
fields (id, title, position, active, ...) stay fully trustworthy either way:
|
|
2012
|
+
an optional enrichment never deletes the answer it was decorating. Read
|
|
2013
|
+
`usage_unavailable` before concluding "this widget is used nowhere": on such
|
|
2014
|
+
a row nothing at all is known about where it appears.
|
|
2015
|
+
- **"What does a GUEST actually get served?" — `verify_on_urls` (T1.7).** Pair
|
|
2016
|
+
it with `include_usage:true` to add `usage.observed.urls[]` per row: fetches
|
|
2017
|
+
each url ONCE, anonymously, and checks whether this row's DOM signature
|
|
2018
|
+
(`id="<widget-id>"` on WordPress, `id="module-<id>"` on Joomla) appears —
|
|
2019
|
+
PLUGIN-AGNOSTIC, since it reads the rendered result, never any plugin's
|
|
2020
|
+
rules, so every visibility mechanism is covered without a line of
|
|
2021
|
+
plugin-specific code. Per url: `present`, `absent`, `not_detectable`, or
|
|
2022
|
+
`error`. `not_detectable` — NOT `absent` — covers FOUR cases, all meaning a
|
|
2023
|
+
missing signature proves nothing. **(1)** A `builder-1`..`builder-6`
|
|
2024
|
+
(`never-standalone`) position with no `via:"position"` reference: such a
|
|
2025
|
+
position renders EXCLUSIVELY through an explicit `module_position`
|
|
2026
|
+
element, so with none swept, YOOtheme's element-bound (DOM-anonymous, no id
|
|
2027
|
+
at all) rendering can't be ruled out. **(2)** The row sits in no position at
|
|
2028
|
+
all (empty `position` — an unplaced WordPress widget, Joomla's "NONE"
|
|
2029
|
+
group): nothing is expected on any url either way, so a missing signature
|
|
2030
|
+
says nothing about it. **(3)** The row is ALSO placed as a `module` ELEMENT
|
|
2031
|
+
**on the template THIS url renders**: that path carries no DOM id either, so
|
|
2032
|
+
even on an otherwise checkable position a not-found signature cannot tell
|
|
2033
|
+
"not rendered" from "rendered anonymously". MEASURED live on
|
|
2034
|
+
`dev5wp.wootsup.com/mcp-module-probe/`: `custom_html-1` has both an
|
|
2035
|
+
`element` and a `position` reference, and one document served its content
|
|
2036
|
+
**twice** while `id="custom_html-1"` appeared **once** — the element-rendered
|
|
2037
|
+
copy is `<div class="uk-panel widget widget_custom_html">`, with no instance
|
|
2038
|
+
attribute at all. The signature is not widened to cover it because the only
|
|
2039
|
+
token there is the widget TYPE class, which occurs 3× on that same page: a
|
|
2040
|
+
wider match would trade a false `absent` for a false `present`.
|
|
2041
|
+
**(4)** Where that element renders **could not be determined** — either the
|
|
2042
|
+
url did not map to a template (`pages_resolve` declines it; a Joomla site
|
|
2043
|
+
root served by a theme template is the common case), or the element sits on
|
|
2044
|
+
a THEME template, which governs many urls and is not enumerated here.
|
|
2045
|
+
**Cases (3) and (4) are decided PER URL, not per row.** An element reference
|
|
2046
|
+
on ANOTHER page cannot render on this url — a per-page layout renders on
|
|
2047
|
+
exactly its own page — so there you get the measured `present`/`absent`
|
|
2048
|
+
instead of a withheld answer. MEASURED on both platforms: `custom_html-1`'s
|
|
2049
|
+
element reference is on `page:2987` while the queried url was the site root
|
|
2050
|
+
(`page:1`), an anonymous fetch found 0 signature hits and 0 content hits, and
|
|
2051
|
+
`absent` is the correct decidable answer. The `reason` on every
|
|
2052
|
+
`not_detectable` names which case applies and, for (3)/(4), which template.
|
|
2053
|
+
Any OTHER
|
|
2054
|
+
position (`site-wide` — a real theme panel like navbar/header/a template's
|
|
2055
|
+
own `bottom`/`top` — or `not-on-builder-pages`) renders
|
|
2056
|
+
DIRECTLY from the theme with no element needed at all, so it's checkable
|
|
2057
|
+
even with completely empty `element_refs` — provided it has no `element`
|
|
2058
|
+
reference either. `error` = no observation was
|
|
2059
|
+
obtained from this url: a network failure, a timeout, a non-2xx, or a url
|
|
2060
|
+
the same-origin guard refused (off-site host, private address, disallowed
|
|
2061
|
+
scheme) — never treated as `absent`, never a throw. An `absent` result on a
|
|
2062
|
+
row `declared` calls unconditionally
|
|
2063
|
+
visible (`builder_visible:true`) adds a `discrepancy` — a named SUSPICION
|
|
2064
|
+
(a visibility plugin, a time-gated rule, a stale cache), never phrased as a
|
|
2065
|
+
diagnosis, and never attached to `not_detectable`/`error`.
|
|
2066
|
+
**Redirects are followed by this tool, bounded, and REPORTED.** When a url
|
|
2067
|
+
ends somewhere else, its entry carries `final_url` — the page whose markup
|
|
2068
|
+
was actually searched. That changes what the result means: `/contact` → 301
|
|
2069
|
+
→ `/kontakt` scans the KONTAKT page, so a module assigned to `/contact`'s
|
|
2070
|
+
own menu item is not expected in it, and the `discrepancy` then LEADS with
|
|
2071
|
+
the measured redirect instead of naming a visibility plugin. Re-check with
|
|
2072
|
+
the `final_url` before suspecting anything else. Every hop is re-checked
|
|
2073
|
+
against the same-origin guard, so a redirect off-site is refused with its
|
|
2074
|
+
own reason rather than silently fetched. Momentary
|
|
2075
|
+
snapshot per url, a sample (max 10 urls) — never a full crawl. Omit
|
|
2076
|
+
(default): zero requests, `observed` stays absent.
|
|
2077
|
+
- **"What's on THIS page from widgets/modules?" — `include_modules` (T1.8), the
|
|
2078
|
+
reverse of T1.6/T1.7.** Where `local_content_read`'s `usage` answers "for
|
|
2079
|
+
this ONE module, which pages?", `include_modules:true` on
|
|
2080
|
+
`yootheme_builder_page_get_layout` (→ `modules_on_page`) and
|
|
2081
|
+
`yootheme_builder_pages_audit` (→ `modules_by_page[layout_id]`) answers
|
|
2082
|
+
"for THIS page, which modules?" — the SAME D11 index, read backwards, never
|
|
2083
|
+
a second scan. Each result splits `from_elements` (a `module`/
|
|
2084
|
+
`module_position` element placed DIRECTLY in this page's own layout) from
|
|
2085
|
+
`from_positions` (a `site-wide` theme position — navbar/header/footer —
|
|
2086
|
+
rendering here with NO element in this page's layout at all). On Joomla, a
|
|
2087
|
+
`site-wide` module can still be assignment-EXCLUDED from one specific page
|
|
2088
|
+
(`except`); when that decision can't be resolved for this page (no menu
|
|
2089
|
+
item addresses it, or more than one does), the row lands in
|
|
2090
|
+
`from_positions_undecidable` instead of being guessed in or out. Default
|
|
2091
|
+
`false`: the layout sweep this costs is never paid unasked, and the
|
|
2092
|
+
response is byte-identical without it.
|
|
2093
|
+
- **A short list is never silently a FAILED list — two presence-only keys say
|
|
2094
|
+
so.** `unresolved_positions` appears when a `module_position` element on
|
|
2095
|
+
that page could not be resolved (a database error, unavailable widget
|
|
2096
|
+
APIs), and `unresolved_module_universe` appears when the site's module
|
|
2097
|
+
store could not be read AT ALL — which makes `from_positions` unknowable
|
|
2098
|
+
rather than empty. Both are emitted ONLY on failure, so a healthy answer
|
|
2099
|
+
omits them and is byte-identical to before. If you see either, the answer
|
|
2100
|
+
is INCOMPLETE: do not conclude "this page has no modules", and above all do
|
|
2101
|
+
not create a replacement module to "fix" it.
|
|
2102
|
+
- **The same rule for reading, one level up: an empty `local_content_read` is
|
|
2103
|
+
never a failed one.** A read that did not run to completion answers
|
|
2104
|
+
`error_code: local_content_unavailable` (HTTP 503), never a 200 with
|
|
2105
|
+
`{items:[], total:0}` — so an empty-and-OK response can only mean the site
|
|
2106
|
+
genuinely has none of that kind. The landscape tool
|
|
2107
|
+
(`local_content_types`) is the deliberate exception: it is a composite of
|
|
2108
|
+
independent sub-reads, so instead of failing the whole call it keeps the
|
|
2109
|
+
parts it could read and lists the rest under `degraded` — every count named
|
|
2110
|
+
there is a placeholder, not a measurement.
|
|
2111
|
+
- **"Now change what you found" — `modules_update` (W2), the write
|
|
2112
|
+
counterpart of `local_content_read(kind:"modules")` / `include_modules`.**
|
|
2113
|
+
Updates a widget's (WordPress) or module's (Joomla) SAFE writable subset —
|
|
2114
|
+
`title`, `position`, `active`, `theme_overlay` on BOTH platforms; `showtitle`,
|
|
2115
|
+
`ordering`, `access`, `assignment` on JOOMLA ONLY (WordPress rejects those
|
|
2116
|
+
four with a 400). `settings`/`params`/`content` are NOT in the schema at all
|
|
2117
|
+
— content editing stays the CMS editor's job; this tool is the
|
|
2118
|
+
display/placement layer.
|
|
2119
|
+
`dry_run` DEFAULTS true: a bare call inspects and writes NOTHING, returning
|
|
2120
|
+
the `{before, after, changed, notes, after_read_back}` diff of what a real
|
|
2121
|
+
write WOULD do.
|
|
2122
|
+
Every field you do NOT pass stays byte-identical — proven at the SQL/option-write
|
|
2123
|
+
level, not merely "looks unchanged".
|
|
2124
|
+
**A real write REPORTS WHAT IT READ BACK, not what you asked for.** After
|
|
2125
|
+
committing, the Joomla arm re-reads the stored record and fills `after` and
|
|
2126
|
+
`changed` from that read. So a field that did not land is reported at its
|
|
2127
|
+
STORED value, with a `notes` entry naming the divergence — never echoed back
|
|
2128
|
+
as if it had succeeded. This is the ONE way a dry run's answer and a real
|
|
2129
|
+
write's answer can differ, and it is deliberate: measured on Joomla,
|
|
2130
|
+
`modules_update({showtitle:false, ordering:3})` once answered
|
|
2131
|
+
`after.showtitle:false` about a row that held `3`. If the verification read
|
|
2132
|
+
itself fails, the write still stands and `notes` says the answer is
|
|
2133
|
+
unverified rather than pretending it was checked. That holds per FIELD, not
|
|
2134
|
+
only for the whole row: `assignment` is read back from a second table, and
|
|
2135
|
+
when THAT read fails the requested assignment is kept and named unverified,
|
|
2136
|
+
never replaced by the `none` an unread table looks like.
|
|
2137
|
+
**`after_read_back` tells a MEASURED `changed` entry from a merely REQUESTED
|
|
2138
|
+
one.** It lists exactly the keys whose `after` came out of storage. A key
|
|
2139
|
+
named in `changed` but ABSENT from `after_read_back` is a transition this
|
|
2140
|
+
call asked for and did not measure — the write committed, so `changed` keeps
|
|
2141
|
+
naming it, but check it before treating it as landed. Measured on Joomla with
|
|
2142
|
+
the `#__modules_menu` read-back failing after a committed write:
|
|
2143
|
+
`changed: ["title","ordering","assignment"]` with
|
|
2144
|
+
`after_read_back: ["title","ordering"]`. Before this field existed those three
|
|
2145
|
+
were indistinguishable, and the only qualification was a `notes` sentence.
|
|
2146
|
+
A DIVERGING read still counts as read back: this list answers "did we look?",
|
|
2147
|
+
never "did we like what we saw?". It is `[]` for every dry run (nothing was
|
|
2148
|
+
written, so nothing could be read back) and always `[]` on WordPress — a
|
|
2149
|
+
plain statement, never a failure signal.
|
|
2150
|
+
**On WORDPRESS there is no read-back, and `after` is what you REQUESTED.**
|
|
2151
|
+
Not an oversight: a widget's instance is one serialised `wp_options` value,
|
|
2152
|
+
so the per-field storage divergence the Joomla read-back exists to catch
|
|
2153
|
+
cannot arise, and the failure that CAN arise, the write not reaching storage,
|
|
2154
|
+
is raised as an ERROR rather than shown as a diverging `after`. So on
|
|
2155
|
+
WordPress do not read `after` as "checked against storage"; read it as "this
|
|
2156
|
+
is what was asked, and it did not fail".
|
|
2157
|
+
**Every value is TYPE-CHECKED, never coerced.** Send real JSON types: the
|
|
2158
|
+
string `"false"` is NOT the boolean `false` (PHP reads a non-empty string as
|
|
2159
|
+
TRUE, so a coerced `"false"` would have PUBLISHED the module), `"7abc"` is
|
|
2160
|
+
not the integer `7`, and an object is not a `title`. A wrong type is
|
|
2161
|
+
REFUSED with a 400 naming the expected one — this tool never rewrites a
|
|
2162
|
+
value it did not understand.
|
|
2163
|
+
**`notes` is the channel for "correctly did nothing, and that is not the
|
|
2164
|
+
end of it".** It is a list of plain sentences, usually empty. The case that
|
|
2165
|
+
motivated it: a Joomla module with `published = 1` whose `publish_up` is in
|
|
2166
|
+
the FUTURE reads `active:false`, so `{active:false}` is a genuine no-op with
|
|
2167
|
+
`changed: []` — and the module still goes live on schedule. `notes` says so
|
|
2168
|
+
instead of letting an empty `changed` read as "already handled".
|
|
2169
|
+
**Read `notes` whenever it is NON-EMPTY — an empty `changed` is not the
|
|
2170
|
+
trigger.** This used to say "read it whenever `changed` comes back empty",
|
|
2171
|
+
and that sent readers past the note that matters most: the one saying a field
|
|
2172
|
+
in a NON-empty `changed` was never read back. A note is attached exactly when
|
|
2173
|
+
something needs saying, so its presence is the signal.
|
|
2174
|
+
**One consequence is REPORTED in `notes`, not refused: the module will render
|
|
2175
|
+
TWICE.** If a Module/Widget element already embeds this module and you move it
|
|
2176
|
+
to a position that renders it on its own — a real theme position or widget
|
|
2177
|
+
area, not `builder-1`..`builder-6` — it appears once through each element and
|
|
2178
|
+
once through the position. `modules_create` refuses that same combination up
|
|
2179
|
+
front (`placement_position_conflict`); this verb has no `placement` argument
|
|
2180
|
+
to refuse it with, and both CMSs allow the move in their own UI, so blocking
|
|
2181
|
+
it here would be this tool inventing a restriction. The note names the
|
|
2182
|
+
elements and both ways back to one appearance. It rides on a `dry_run` too,
|
|
2183
|
+
which is where it is worth most.
|
|
2184
|
+
**`position_verified: false` reports on a check that did NOT happen, and it
|
|
2185
|
+
means here exactly what it means on `modules_create`.** Both verbs ask the
|
|
2186
|
+
same rule the same question, so the same site answers the same way: when the
|
|
2187
|
+
site's position list could not be read from there AND the name is not one of
|
|
2188
|
+
`builder-1` to `builder-6` (those are taken by name and need no list), the
|
|
2189
|
+
position is WRITTEN and the answer carries this field. The move happened; the
|
|
2190
|
+
check behind "an unknown position is refused BY NAME" did not, so a typo is
|
|
2191
|
+
still a typo. The field is ABSENT in the two cases with nothing to report —
|
|
2192
|
+
the value WAS vouched for, or this call wrote no position at all — so read
|
|
2193
|
+
`after.position` to tell those apart. A `dry_run` never carries it, and
|
|
2194
|
+
neither does an update that names no position: it reports on a position THIS
|
|
2195
|
+
call wrote, never on the one the module already had.
|
|
2196
|
+
On WordPress, `position` must be a REGISTERED widget area
|
|
2197
|
+
(`$wp_registered_sidebars` — on a YOOtheme site that is the 13 theme
|
|
2198
|
+
positions plus `builder-1`..`builder-6`); WordPress core does NOT validate
|
|
2199
|
+
this itself, so a typo used to park the widget in a phantom area that
|
|
2200
|
+
renders nowhere while the response reported success. `theme_overlay` MERGES onto the stored overlay: a
|
|
2201
|
+
key you OMIT keeps its current value; only the keys you explicitly include
|
|
2202
|
+
are written (so calling `theme_overlay:{style:"card-alt"}` never resets
|
|
2203
|
+
`visibility` or any other already-set key). The overlay's key list is
|
|
2204
|
+
PLATFORM-SPECIFIC — WordPress `_theme` (17 keys, incl. `showtitle`) vs.
|
|
2205
|
+
Joomla `params.yoo_config` (18 keys, incl. `language_parent_icon`/
|
|
2206
|
+
`language_icon` INSTEAD of `showtitle`) — an unknown or wrong-platform key
|
|
2207
|
+
(or a wrong value type) is REJECTED with a 400 naming every allowed key,
|
|
2208
|
+
never silently dropped, never coerced. Two further writes are REFUSED
|
|
2209
|
+
outright rather than half-kept, because the stored state cannot be
|
|
2210
|
+
expressed by the field you sent: a `title` on a WordPress `block` widget
|
|
2211
|
+
(that type has NO title — its instance is `{content}` only, WordPress never
|
|
2212
|
+
renders one, and storing it would additionally SUPPRESS the preview title
|
|
2213
|
+
derived from the block markup), and an `active:true` on a Joomla module
|
|
2214
|
+
whose publish window would still leave it `scheduled`/`expired`. On Joomla
|
|
2215
|
+
`active` means the `published` column AND the `publish_up`/`publish_down`
|
|
2216
|
+
window — the SAME derivation `local_content_read` uses — so a module
|
|
2217
|
+
scheduled for the future already reads `active:false`, and "deactivating"
|
|
2218
|
+
it is correctly a no-op instead of silently cancelling the schedule — with
|
|
2219
|
+
a `notes` entry so the no-op is not mistaken for "already handled".
|
|
2220
|
+
`access` (Joomla only) is the module's VIEW LEVEL, not a flag: Joomla
|
|
2221
|
+
filters modules by `access IN (authorised view levels)`, and `access:0` is
|
|
2222
|
+
in NOBODY's set — not even a Super User's — so such a module renders for no
|
|
2223
|
+
one while looking published. That is what `local_content_read` reports as
|
|
2224
|
+
`builder_visible:false`; `modules_update({access: 1})` is the repair.
|
|
2225
|
+
`access:0` itself is refused, because 0 is the very state being repaired.
|
|
2226
|
+
Advanced/gateway-only.
|
|
2227
|
+
- **"Build one from scratch" — `modules_create` (W3), then `element_add` —
|
|
2228
|
+
the two-step recipe.** `modules_create` supports exactly FIVE known types:
|
|
2229
|
+
WordPress `custom_html` (title+content), `text` (title+text), `block`
|
|
2230
|
+
(content only, NO title); Joomla `mod_custom` (title+content COLUMN+
|
|
2231
|
+
showtitle), `mod_menu` (title + the params keys menutype/base/startLevel/
|
|
2232
|
+
endLevel/showAllChildren/tag_id/class_sfx/window_open/layout/
|
|
2233
|
+
moduleclass_sfx/cache/cache_time/cachemode). An unknown type is rejected
|
|
2234
|
+
with this exact list. `placement` is REQUIRED (`position`/`module_element`/
|
|
2235
|
+
`position_element`, i.e. how the module reaches a page); a call that omits it
|
|
2236
|
+
creates NOTHING and instead returns `ways`, every way for this platform with
|
|
2237
|
+
its reach and a summary, plus `notes`. `content`/`text` are stored
|
|
2238
|
+
byte-for-byte, same no-sanitization guarantee as `modules_update`.
|
|
2239
|
+
**`position` is ONE argument with ONE meaning on both platforms**: the
|
|
2240
|
+
template position or widget area the module goes into, validated SERVER-side
|
|
2241
|
+
against the positions this site declares or registers, because the set of
|
|
2242
|
+
areas is a property of the SITE and no schema here can know it. **Joomla
|
|
2243
|
+
takes it AT CREATE TIME**, exactly as WordPress does; it stopped being a
|
|
2244
|
+
`modules_update`-only concern on 2026-08-06. The value must agree with
|
|
2245
|
+
`placement` or the call is refused BY NAME rather than guessed:
|
|
2246
|
+
`placement_needs_a_position` for `placement:"position"` OR
|
|
2247
|
+
`placement:"position_element"` with no position (both ways need one and they
|
|
2248
|
+
need different values, so neither is defaulted),
|
|
2249
|
+
`placement_position_conflict` for a `builder-N` position under
|
|
2250
|
+
`placement:"position"` (a builder position renders nothing on its own), for a
|
|
2251
|
+
REAL theme position under `placement:"module_element"` (measured on both
|
|
2252
|
+
platforms: the module then renders TWICE on that page), or for a non-builder
|
|
2253
|
+
position under `placement:"position_element"`; and
|
|
2254
|
+
`placement_position_unknown` for a value this site does not declare at all,
|
|
2255
|
+
or that could not be a position at all because it is longer than 50
|
|
2256
|
+
characters. That 50 is the width of the JOOMLA column that stores a position;
|
|
2257
|
+
WordPress keeps widget areas as option keys with no width of their own, and
|
|
2258
|
+
the bound is enforced on both platforms anyway, because `position` is one
|
|
2259
|
+
argument with one meaning and the narrower platform is what bounds it. That
|
|
2260
|
+
last one is answered by the SAME
|
|
2261
|
+
rule on both platforms, and when the site's list could be read the refusal
|
|
2262
|
+
carries it as `declared_positions` as well as naming it in the message.
|
|
2263
|
+
Under `placement:"module_element"` a Joomla module is created with no position
|
|
2264
|
+
and assigned to ALL pages, so it is immediately visible to `element_add`. On
|
|
2265
|
+
WordPress the same placement REQUIRES a builder area since 2026-08-08: a
|
|
2266
|
+
widget exists only inside a registered area, and this tool asks which one
|
|
2267
|
+
instead of picking `builder-1` for you.
|
|
2268
|
+
**A `position` is not by itself a rendering.** `builder-1`..`builder-6` is
|
|
2269
|
+
classified `never-standalone` on BOTH platforms (that is what
|
|
2270
|
+
`local_content_read(include_usage:true)` reports back), so a `builder-N`
|
|
2271
|
+
position means "it now has a home", not "it now shows up". The
|
|
2272
|
+
live A/B that proved a `builder-N` module produces zero frontend output was
|
|
2273
|
+
measured on Joomla; on WordPress the classifier reports the same value and
|
|
2274
|
+
the same rule is what `next_step` assumes. Either way, step 2 (`element_add`)
|
|
2275
|
+
is what actually puts it on a page. Do not read a freshly created row as
|
|
2276
|
+
visible until an element references it.
|
|
2277
|
+
**`builder_visible` can come back as the STRING `unknown_until_next_request`
|
|
2278
|
+
on a real WordPress create.** WordPress builds `$wp_registered_widgets` once
|
|
2279
|
+
per request, so the widget this very call just wrote is not in it yet: the
|
|
2280
|
+
honest answer is neither `true` nor `false`. It is a timing artefact of the
|
|
2281
|
+
same request, not a fault and not a warning. Read the row again (any later
|
|
2282
|
+
request, e.g. `local_content_read({kind:"modules"})`) to get the real
|
|
2283
|
+
boolean, and never branch on it as if it were `false`.
|
|
2284
|
+
`dry_run` DEFAULTS true, same discipline as `modules_update`: once
|
|
2285
|
+
`placement` is chosen, a bare call previews `{type, position, fields}` and
|
|
2286
|
+
creates nothing. A REAL create
|
|
2287
|
+
(`dry_run:false`) returns the full created `row` (the SAME
|
|
2288
|
+
`local_content_read(kind:'modules')` D2 shape) PLUS `next_step` — the
|
|
2289
|
+
EXACT follow-up call **for the placement you chose**, one shape per choice:
|
|
2290
|
+
| `placement` | `next_step` |
|
|
2291
|
+
|---|---|
|
|
2292
|
+
| `module_element` | `element_add{element_type:"module", props:{widget:"custom_html-3"}}` on WordPress, `props:{module:"122"}` on Joomla |
|
|
2293
|
+
| `position_element` | `element_add{element_type:"module_position", props:{content:"builder-1"}}` |
|
|
2294
|
+
| `position` | Joomla: `modules_update{id, assignment}` to narrow it to chosen pages. WordPress: `local_content_read{kind:"modules", include_usage:true}` — read-only, because WordPress has no per-page control to narrow it with; add `verify_on_urls` to PROVE guest-visible rendering rather than trust `active`/`builder_visible` alone |
|
|
2295
|
+
**Two live-measured details, both baked into `next_step` so you never have
|
|
2296
|
+
to remember them:**
|
|
2297
|
+
1. **Joomla's `props.module` is a STRING** (`"122"`), never a bare
|
|
2298
|
+
integer — a mismatch here silently fails to resolve, it does not error.
|
|
2299
|
+
2. **A `module_position` element names the POSITION, not one module** — it
|
|
2300
|
+
renders every published entry sitting there. That element accepts an
|
|
2301
|
+
optional `layout` prop from its own enum (`stack`/`grid`) — leaving it
|
|
2302
|
+
out is safe and defaults to `stack`; set it only if you want `grid`.
|
|
2303
|
+
`next_step` therefore leaves the key out. Never copy the `sidebar` value
|
|
2304
|
+
the element schema reports as that field's default: it is not one of the
|
|
2305
|
+
two accepted values and is rejected with a 422. This does NOT apply to
|
|
2306
|
+
the `module` form.
|
|
2307
|
+
Copy `next_step` verbatim, add `layout_id` + `etag` + `parent_path` when it
|
|
2308
|
+
is an `element_add` (that tool REQUIRES `layout_id` and `etag`, and unlike
|
|
2309
|
+
the fourteen tools that take an `element_path` it names its parent argument
|
|
2310
|
+
`parent_path`), then make the call. A `null` here is an answer, not a gap:
|
|
2311
|
+
the widget is already live site wide and nothing further is needed.
|
|
2312
|
+
**One more field the answer can carry, and it is the only one that reports on
|
|
2313
|
+
something that did NOT happen: `position_verified: false`.** It appears when a
|
|
2314
|
+
position was STORED that the server could not vouch for — the site's position
|
|
2315
|
+
list was not readable from there AND the name is not one of `builder-1` to
|
|
2316
|
+
`builder-6`, which are taken by name and need no list. The write happened; the
|
|
2317
|
+
check behind this tool's "an unknown position is refused BY NAME" promise did
|
|
2318
|
+
not, so the value is in place unexamined and a typo is still a typo. The field
|
|
2319
|
+
is ABSENT in the two cases where there is nothing to report — the stored
|
|
2320
|
+
position WAS vouched for, or no position was stored at all — so absence alone
|
|
2321
|
+
does not mean "checked". Read `preview.position` to tell those two apart: it
|
|
2322
|
+
is empty only in the second. A `dry_run` never carries the field, so a preview
|
|
2323
|
+
cannot warn you that the position it shows will be stored unverified.
|
|
2324
|
+
**The answer also names what will render the module before you asked for
|
|
2325
|
+
it: `element_refs`.** Each entry is `{layout_id, path, via}` and there are
|
|
2326
|
+
exactly two ways to get one. `via: "element"` means a `module` element in
|
|
2327
|
+
some layout ALREADY names the id this call just took — WordPress reuses a
|
|
2328
|
+
deleted widget's number (the allocator is highest-instance-plus-one), so a
|
|
2329
|
+
new widget can inherit a reference nobody meant for it. `via: "position"`
|
|
2330
|
+
means a `module_position` element already renders the position you created
|
|
2331
|
+
into; that needs no reuse at all, happens on both platforms, and is the
|
|
2332
|
+
common one. The field is ALWAYS present, so `[]` is the answer "nothing
|
|
2333
|
+
renders it yet" rather than a build without the concept. A `dry_run` carries
|
|
2334
|
+
it too, and that is where it is worth most, since `dry_run` defaults to true
|
|
2335
|
+
and is the first call you make. On JOOMLA a `dry_run` can report only
|
|
2336
|
+
`via: "position"`: the module id is assigned by the database at insert time,
|
|
2337
|
+
so on a preview there is no id for a layout to reference. That is a plain
|
|
2338
|
+
statement about that arm, not a failure.
|
|
2339
|
+
**One asymmetry to know about that resolution step:** on the plugin's own MCP
|
|
2340
|
+
transport a large answer is trimmed to a response budget, top-level keys are
|
|
2341
|
+
dropped largest first, and `preview` is one of the large ones while this flag
|
|
2342
|
+
is the smallest. So a widget with several KB of content can hand you
|
|
2343
|
+
`position_verified: false` and no `preview` to resolve it with. Every dropped
|
|
2344
|
+
key is named in `omitted_keys`, so check there before reading an absent
|
|
2345
|
+
`preview` as "no position was stored". (The `retry_hint` that comes with a
|
|
2346
|
+
trimmed answer suggests raising `max_chars`; `modules_create` has no such
|
|
2347
|
+
argument, so that hint does not apply here.)
|
|
2348
|
+
Advanced/gateway-only.
|
|
2349
|
+
- **The fifteen typed failures of `modules_update` / `modules_create` /
|
|
2350
|
+
`modules_delete`, and what each one licenses you to do next.** These are their
|
|
2351
|
+
OWN family, separate from the `pages_audit`/`heading_outline` target-error
|
|
2352
|
+
table above. Three of
|
|
2353
|
+
the four `placement_*` codes belong to `modules_create` alone. The fourth,
|
|
2354
|
+
`placement_position_unknown`, is also what `modules_update` answers for a
|
|
2355
|
+
position this site does not declare, so that one mistake reads the same
|
|
2356
|
+
whichever verb you sent it with. All four are the ones a caller meets first,
|
|
2357
|
+
because they are decided before anything is written:
|
|
2358
|
+
|
|
2359
|
+
| `error_code` | HTTP | What it means | What to do |
|
|
2360
|
+
| --- | --- | --- | --- |
|
|
2361
|
+
| `modules.placement_not_chosen` | 400 | `modules_create` only. `placement` was missing or not one of `position` / `module_element` / `position_element`. Nothing was created, not even a preview: the check runs before `dry_run` is honoured. | The error carries BOTH halves of the answer, `ways` and `notes`. Read them, pick a way, resend with `placement` set. |
|
|
2362
|
+
| `modules.placement_needs_a_position` | 400 | `modules_create` only. The way you chose needs a `position` and the call carried none. WHICH ways need one is a platform fact since 2026-08-08: all three on WordPress, where a widget exists only inside a registered area, and two on Joomla, where a module with no position is a native state. They take different values, and none is defaulted. | Resend the SAME `placement` with `position` set. Switching placement to get past this builds something other than what you asked for. |
|
|
2363
|
+
| `modules.placement_position_unknown` | 400 | `modules_create` AND `modules_update`, the one code of this family both verbs send. The `position` named is not one this site declares, or could not be a position at all (longer than 50 characters, the width of the Joomla column that stores it; the bound is enforced on BOTH platforms so one argument keeps one meaning). The SAME code on both platforms and on both verbs. What DIFFERS is the offer inside the message, and with it the advice: `modules_update` has no `placement` argument, so its message names the positions this site declares plus `builder-1` to `builder-6` and its runtime `hint` says exactly that. The advice cell to the right is the `modules_create` one. | The MESSAGE already names the values this call can use, narrowed to the placement you chose, so take the next value from there: `placement: "position"` takes a template position this site declares that is at most 50 characters long, `placement: "position_element"` takes `builder-1` to `builder-6` and nothing else, and `placement: "module_element"` takes no template position at all and only `builder-1` to `builder-6` if you name one. `declared_positions` rides alongside as the raw fact of everything this site declares, which is more than any one placement takes: it can even hold a value no call accepts, because a site may declare a position wider than the column that stores it. So when the two differ the message is the shorter, safer list. A value taken from `declared_positions` under the wrong placement is refused again as `placement_position_conflict`. When the error carries no list, the site's list could not be read and the value has to come from the template itself. |
|
|
2364
|
+
| `modules.placement_position_conflict` | 400 | `modules_create` only. `position` and `placement` are each fine alone and describe DIFFERENT outcomes together: a `builder-N` under `placement:"position"`, a real theme position under `placement:"module_element"`, a non-builder position under `placement:"position_element"`. | Change one of the two and resend. The message names the combination and the call that does what you asked for. |
|
|
2365
|
+
| `modules.invalid_field` | 400 | A field is unknown, unwritable (`settings`/`params`/`content`), wrong-platform, or the wrong JSON TYPE. The message names the allowed keys or the expected type. | Fix the call. Nothing was written; the module is untouched. |
|
|
2366
|
+
| `modules.field_value_invalid` | 400 | The KEY is right and the VALUE is not: a wrong JSON type (the string `"false"` is not the boolean `false`), a value outside a closed set (an unknown widget/module type, an unrecognised `assignment.mode`), or outside a permitted range (`menu_ids` must be positive). The largest of these classes. | Correct the VALUE of the named key and resend. Renaming the key will not help: the key is already right. The message names the expected type or the allowed values. |
|
|
2367
|
+
| `modules.field_not_on_this_type` | 400 | The field name is fine and this tool can write it, but THIS widget/module type has no such field. Nothing to rename. | The message names the types that DO carry it, and where this type keeps the equivalent instead (block markup, `theme_overlay`, or the type-specific params). Pick another type, or put the value where this type keeps it. |
|
|
2368
|
+
| `modules.field_combination_invalid` | 400 | Every field is valid ALONE; together they contradict each other, or one needs a partner the call did not carry. | Change the COMBINATION, not a value: add the missing field or drop one of the pair. The message names which of the two it is and in which direction. |
|
|
2369
|
+
| `modules.position_not_clearable` | 400 | **WordPress only.** You sent `position: ""` to take the widget out of every area, and on WordPress a widget exists only while it sits in one. Joomla accepts the same value, because a module there can genuinely sit in none. So this is about the platform, not about the value being malformed. | Send `active: false` instead: it parks the widget in `wp_inactive_widgets`, and naming a position again brings it back. Nothing was written. |
|
|
2370
|
+
| `modules.blocked_by_cms_state` | 409 | Name, value and combination are all correct. The CMS is in a state that would make the outcome untrue anyway, and this tool deliberately does not write the columns that would change it. | Nothing to fix in the call. The message names the exact field and value standing in the way; change it in the CMS module manager and repeat the call. |
|
|
2371
|
+
| `modules.invalid_id` | 400 | Joomla only, and only for the id-addressed verbs: `moduleId` was not a positive integer. WordPress has no counterpart, because its widget ids are strings such as `custom_html-2`. | Read the id off `local_content_read({kind:"modules"})` rather than constructing it. |
|
|
2372
|
+
| `modules.not_found` | 404 | The store WAS read and this id is genuinely not in it. | The module really is gone. Creating a replacement is legitimate here. |
|
|
2373
|
+
| `modules.element_refs_present` | 409 | `modules_delete` only. The request is fine; the STATE is not. Layout elements still reference this module directly, and the message names each one. | Delete those elements with `element_delete`, or repeat with `acknowledge_element_refs: true` to accept the dangling references. Nothing was removed. |
|
|
2374
|
+
| `modules.write_failed` | 500 | A write was ATTEMPTED and did not land. Every writing verb on both platforms answers this way: the create that stored nothing, the update whose change never reached the column, the delete that removed nothing. Reporting success would have been a positive false statement about the state of the store. | Re-read the module before retrying: the message names what did and did not reach storage. On Joomla the whole call shares one transaction, so it is all or nothing; on WordPress the widget instance and its placement are separate writes, so the message may report a half-applied call. |
|
|
2375
|
+
| `modules.unavailable` | 503 | The widget/module store could not be reached AT ALL. | **Never create a replacement.** Absence of an answer is not absence of the module. Retry later, or tell the user the store is down. |
|
|
2376
|
+
|
|
2377
|
+
The 404-vs-503 split is the load-bearing one: before it existed, an
|
|
2378
|
+
unreachable store answered 404 and an agent dutifully created a duplicate
|
|
2379
|
+
for a module that was there the whole time.
|
|
1112
2380
|
- **Clone-then-update path drift.** The clone returns a path that's
|
|
1113
2381
|
correct at the moment of the call. If you fire off many ops in
|
|
1114
2382
|
parallel, a concurrent edit may shift indices. Refresh via
|
|
1115
|
-
`get_etag` + `
|
|
2383
|
+
`get_etag` + `element_list` between independent batches.
|
|
1116
2384
|
- **Cloning a bound element keeps the binding.** `element_clone`
|
|
1117
2385
|
copies the entire element including `props.source`. If the user
|
|
1118
2386
|
wanted a "data-free" copy, call
|
|
1119
2387
|
`yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", arguments: { ... } })`
|
|
1120
2388
|
on the new path afterwards.
|
|
1121
|
-
- **Wrong parameter names.** Use `
|
|
2389
|
+
- **Wrong parameter names.** Use `layout_id`, `element_path`,
|
|
1122
2390
|
`etag` (NOT `pageId`, `srcPath`, `ifMatch`).
|
|
1123
2391
|
|
|
1124
2392
|
**Worked example (tool-call snippet):**
|
|
@@ -1126,7 +2394,7 @@ CMS-level template duplication (in wp-admin or Joomla administrator).
|
|
|
1126
2394
|
```jsonc
|
|
1127
2395
|
// Step 5. Clone the section element as a sibling.
|
|
1128
2396
|
yootheme_builder_element_clone({
|
|
1129
|
-
|
|
2397
|
+
layout_id: "home",
|
|
1130
2398
|
element_path: "/children/0/children/2", // the hero section to duplicate
|
|
1131
2399
|
etag: "abc123"
|
|
1132
2400
|
})
|
|
@@ -1134,11 +2402,11 @@ yootheme_builder_element_clone({
|
|
|
1134
2402
|
|
|
1135
2403
|
// Step 7. Tweak the clone (replace props entirely, or pass merge: true).
|
|
1136
2404
|
const current = yootheme_builder_element_get({
|
|
1137
|
-
|
|
2405
|
+
layout_id: "home",
|
|
1138
2406
|
element_path: "/children/0/children/3",
|
|
1139
2407
|
});
|
|
1140
2408
|
yootheme_builder_element_update_settings({
|
|
1141
|
-
|
|
2409
|
+
layout_id: "home",
|
|
1142
2410
|
element_path: "/children/0/children/3",
|
|
1143
2411
|
props: { ...current.props, style: "secondary" }, // section background = `style`
|
|
1144
2412
|
etag: "def456"
|
|
@@ -1152,7 +2420,7 @@ copy, follow up with
|
|
|
1152
2420
|
on the new path. Verify with a direct `element_get_binding` call.
|
|
1153
2421
|
|
|
1154
2422
|
**Success criterion:** After publish,
|
|
1155
|
-
`
|
|
2423
|
+
`yootheme_builder_element_list({ layout_id })`
|
|
1156
2424
|
shows the new section at the cloned path with the user's tweaks reflected in
|
|
1157
2425
|
`element_get` on that path.
|
|
1158
2426
|
|
|
@@ -1165,32 +2433,98 @@ guessing, and without rotating the user's key unnecessarily.
|
|
|
1165
2433
|
|
|
1166
2434
|
**Canonical tool-call sequence:**
|
|
1167
2435
|
|
|
1168
|
-
1. `yootheme_builder_diagnose` is a single
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
2436
|
+
1. `yootheme_builder_diagnose` is a single self-diagnostic. Returns
|
|
2437
|
+
`{ platform, plugin_version, site_url, kid, scopes, token_scope,
|
|
2438
|
+
served_areas, capabilities, auth_bound, etag_ok, resolved_user_id,
|
|
2439
|
+
effective_capabilities, capability_ok, capability_warning, library_key,
|
|
2440
|
+
active_site }`. EVERY field is optional: one is absent when the host does
|
|
2441
|
+
not report it. Call this **before** any other tool when you see auth
|
|
2442
|
+
errors. (Takes no arguments beyond the optional `site_id`.)
|
|
2443
|
+
|
|
2444
|
+
`served_areas` and `effective_capabilities` name similar-sounding, DIFFERENT
|
|
2445
|
+
things: `served_areas` lists the tool AREAS this server serves (`pages`,
|
|
2446
|
+
`elements`, `sources`, `inspection`) — a server property, the same list
|
|
2447
|
+
`yootheme_builder_health` carries under the same name. `effective_capabilities`
|
|
2448
|
+
is what THIS caller's resolved CMS user may actually do
|
|
2449
|
+
(`can_create`/`can_edit`/`can_delete`) — see the bullet below. `capabilities`
|
|
2450
|
+
is `served_areas`' RETIRING name, kept one release as a byte-identical alias.
|
|
2451
|
+
|
|
2452
|
+
This list used to be longer, and the extra names never arrived — measured on
|
|
2453
|
+
four live installs. `plugin_error`, `yootheme_loaded`, `yootheme_version`,
|
|
2454
|
+
`endpoint_count`, `bearer_valid`, `bearer_error`, `summary` and `home_url`
|
|
2455
|
+
belonged to an older transport that assembled this report from two HTTP
|
|
2456
|
+
probes; today the plugin answers it in-process. For install facts (YOOtheme
|
|
2457
|
+
version, endpoint list, `home_url`) call `yootheme_builder_health`, which
|
|
2458
|
+
does carry them. For the bearer verdicts, read `auth_bound` and `etag_ok`
|
|
2459
|
+
right here — they are the successors of `bearer_valid` / `bearer_error`.
|
|
2460
|
+
|
|
2461
|
+
The two verdicts to read first are `auth_bound` (the Bearer key was accepted
|
|
2462
|
+
and resolved to a user) and `etag_ok` (a layout state was readable with it).
|
|
2463
|
+
There is no `plugin_reachable` field, and there is nothing to miss: the
|
|
2464
|
+
diagnostic is assembled by the plugin that received your call, so an answer
|
|
2465
|
+
at all IS the reachability answer. An unreachable plugin produces a
|
|
2466
|
+
transport error, never a payload.
|
|
2467
|
+
|
|
2468
|
+
Four of those are easy to skip past and answer questions you would
|
|
2469
|
+
otherwise take several calls to answer:
|
|
2470
|
+
|
|
2471
|
+
- `token_scope` (`read` / `write`). A `read` key fails every write tool
|
|
2472
|
+
with `read_only_token`, and that is a key problem, not a permissions one.
|
|
2473
|
+
- `effective_capabilities` + `capability_ok` + `capability_warning`. The
|
|
2474
|
+
CMS user the key resolves to, and what that user may actually do. A
|
|
2475
|
+
write-scoped key mapped to a user without edit rights still 403s;
|
|
2476
|
+
`capability_ok: false` is the only field that says so before you try.
|
|
2477
|
+
- `active_site` `{ site_id, via, process_scoped }`. Which site this call
|
|
2478
|
+
actually reached, and by which decision: `explicit` (you passed `site_id`
|
|
2479
|
+
— isolated from other clients of this server process), `use_site` (a
|
|
2480
|
+
runtime switch this process is on) or `registry_default` (the default in
|
|
2481
|
+
the sites file). The two inherited paths are kept apart deliberately: a
|
|
2482
|
+
call that reaches the wrong install answers just as confidently as one
|
|
2483
|
+
that reaches the right one. The field rides on EVERY tool call, not only
|
|
2484
|
+
this one. It replaces `process_default_site_id` + `resolved_via`, which
|
|
2485
|
+
this chapter kept naming after they had stopped shipping.
|
|
2486
|
+
- `site_url`. WHICH install answered, as the plugin reports it. `kid` and
|
|
2487
|
+
`auth_bound` cannot show that a perfectly valid key simply belongs to a
|
|
2488
|
+
different install, and that is the commonest cause of the refusal you are
|
|
2489
|
+
triaging. Read it against `active_site.site_id`, which names the registry
|
|
2490
|
+
entry your own config picked: when those two disagree, the disagreement
|
|
2491
|
+
is the finding.
|
|
2492
|
+
- `library_key`. Why the Layout Library tools can or cannot reach
|
|
2493
|
+
`api.yootheme.com`, as `{ configured, state, hint }`. Read `state`
|
|
2494
|
+
before you tell anyone to check their apikey field: see the Layout
|
|
2495
|
+
Library prerequisite section for the five states and who has to act on
|
|
2496
|
+
each. Never contains the key.
|
|
1174
2497
|
2. **Interpret the result:**
|
|
1175
|
-
-
|
|
1176
|
-
|
|
1177
|
-
Plugins → activate "YT Builder MCP"**
|
|
1178
|
-
administrator → Extensions → Plugins → enable
|
|
1179
|
-
MCP" and the matching webservices + component
|
|
1180
|
-
Do not retry until they confirm.
|
|
1181
|
-
- `
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
2498
|
+
- **The call itself failed** (a transport error, not a payload) → the
|
|
2499
|
+
WordPress / Joomla install is down OR the host plugin is deactivated.
|
|
2500
|
+
Send the user to **wp-admin → Plugins → activate "YT Builder MCP"**
|
|
2501
|
+
(WordPress) or **Joomla administrator → Extensions → Plugins → enable
|
|
2502
|
+
"System - YT Builder MCP" and the matching webservices + component
|
|
2503
|
+
entries** (Joomla). Do not retry until they confirm.
|
|
2504
|
+
- `auth_bound: false` → the Bearer key is wrong (typo, revoked, or wrong
|
|
2505
|
+
key for this install). Check `site_url` first: a key that is valid
|
|
2506
|
+
somewhere else fails here for a reason nothing about the key will show.
|
|
2507
|
+
For the upstream HTTP status call `yootheme_builder_sites_test`, which
|
|
2508
|
+
probes `/etag` itself and answers `bearer_valid` + `summary`; diagnose
|
|
2509
|
+
names no status of its own, because the request it is answering already
|
|
2510
|
+
got through. Then send the user to:
|
|
1185
2511
|
- **WordPress:** wp-admin → Tools → "YT Builder MCP" → Bearer Keys
|
|
1186
2512
|
→ copy the existing key into their MCP client config, or
|
|
1187
2513
|
generate a new one.
|
|
1188
2514
|
- **Joomla:** Components → YT Builder MCP → Bearer Keys → same.
|
|
1189
|
-
- `
|
|
2515
|
+
- `auth_bound: true, etag_ok: true` but the original
|
|
1190
2516
|
tool returned a 403 → the key works but the scope is too low for
|
|
1191
2517
|
the tool's required scope (`write` for mutations, `admin` for
|
|
1192
2518
|
destructive operations). Ask the user to regenerate the key with
|
|
1193
2519
|
a higher scope and restart the AI client.
|
|
2520
|
+
- `auth_bound: true, capability_ok: false` →
|
|
2521
|
+
the Bearer key's scope IS write, but the CMS user it resolves to
|
|
2522
|
+
(`effective_capabilities.can_edit`, `can_create`, `can_delete`, all
|
|
2523
|
+
false) has no write capability at all, so writes will still 403
|
|
2524
|
+
despite the write-scoped token. `capability_warning` carries the
|
|
2525
|
+
exact remediation sentence. Re-mint the key under a user with edit
|
|
2526
|
+
rights, or grant that user the capability. A write-scoped token
|
|
2527
|
+
alone is not enough.
|
|
1194
2528
|
3. **Walk the user through key rotation if needed:**
|
|
1195
2529
|
- WordPress: "wp-admin → Tools → YT Builder MCP → Bearer Keys."
|
|
1196
2530
|
Joomla: "Components → YT Builder MCP → Bearer Keys."
|
|
@@ -1202,7 +2536,8 @@ guessing, and without rotating the user's key unnecessarily.
|
|
|
1202
2536
|
- "Restart Claude / Cursor / Zed / Continue / Cline / Roo Code /
|
|
1203
2537
|
Claude Code / Codex CLI."
|
|
1204
2538
|
- "Confirm with `yootheme_builder_diagnose` that
|
|
1205
|
-
`
|
|
2539
|
+
`auth_bound: true` and `etag_ok: true` before retrying the original
|
|
2540
|
+
task."
|
|
1206
2541
|
|
|
1207
2542
|
**Common pitfalls:**
|
|
1208
2543
|
|
|
@@ -1229,29 +2564,33 @@ guessing, and without rotating the user's key unnecessarily.
|
|
|
1229
2564
|
yootheme_builder_diagnose({})
|
|
1230
2565
|
// Response shape:
|
|
1231
2566
|
// {
|
|
1232
|
-
//
|
|
2567
|
+
// platform: "wordpress",
|
|
1233
2568
|
// plugin_version: "1.2.0",
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1238
|
-
//
|
|
1239
|
-
// site_url: "https://example.com",
|
|
1240
|
-
//
|
|
2569
|
+
// kid: "5cb829a21f4d5e4f",
|
|
2570
|
+
// scopes: ["read", "write"],
|
|
2571
|
+
// token_scope: "write",
|
|
2572
|
+
// auth_bound: false, // ← key is bad
|
|
2573
|
+
// etag_ok: false,
|
|
2574
|
+
// site_url: "https://example.com", // ← check this FIRST: right key, wrong install?
|
|
2575
|
+
// active_site: { site_id: "default", via: "registry_default", process_scoped: true }
|
|
1241
2576
|
// }
|
|
1242
2577
|
// → diagnosis: rotate the key. Send user to Tools/Components → YT Builder MCP.
|
|
2578
|
+
// → for the upstream HTTP status: yootheme_builder_sites_test({ site_id: "default" }).
|
|
1243
2579
|
```
|
|
1244
2580
|
|
|
1245
|
-
**Edge case:** `
|
|
1246
|
-
|
|
1247
|
-
isn't active.
|
|
2581
|
+
**Edge case:** `yootheme_builder_health` answers but reports
|
|
2582
|
+
`yootheme_loaded: false` — the user installed the MCP host plugin but YOOtheme
|
|
2583
|
+
Pro itself isn't active. Read that off **health**, not diagnose: diagnose used
|
|
2584
|
+
to declare the field and never filled it. (Health's WordPress arm reports it;
|
|
2585
|
+
the Joomla health answer is assembled before the builder bootstraps and does
|
|
2586
|
+
not carry it, so its absence there means "not knowable here", never `false`.) The MCP server still answers, but every tool that
|
|
1248
2587
|
touches the YOOtheme layout returns an empty/error response. Surface
|
|
1249
2588
|
the mismatch ("YOOtheme Pro is not active on this install") instead
|
|
1250
2589
|
of retrying. On Joomla this can also surface as a "YOOtheme Pro
|
|
1251
2590
|
required" admin notice in the component dashboard.
|
|
1252
2591
|
|
|
1253
2592
|
**Success criterion:** A subsequent `yootheme_builder_diagnose`
|
|
1254
|
-
returns `
|
|
2593
|
+
returns `auth_bound: true` AND `etag_ok: true`. The
|
|
1255
2594
|
original tool now returns a non-auth response.
|
|
1256
2595
|
|
|
1257
2596
|
---
|
|
@@ -1276,22 +2615,28 @@ an instance with a sensible default props payload.
|
|
|
1276
2615
|
`element_type` (`type_name` is a DEPRECATED alias, kept only for
|
|
1277
2616
|
1.0.x back-compatibility).
|
|
1278
2617
|
4. `yootheme_builder_pages_list({ fields: ["id", "label"] })` and
|
|
1279
|
-
`yootheme_builder_page_get_layout({
|
|
2618
|
+
`yootheme_builder_page_get_layout({ layout_id, flat: false })`
|
|
1280
2619
|
locate the `parent_path` (JSON-Pointer) where the new element
|
|
1281
|
-
should land.
|
|
2620
|
+
should land. A content/leaf type is only accepted inside a `column`
|
|
2621
|
+
(never directly in a `section` or a `row`) — if the layout has none yet,
|
|
2622
|
+
call `yootheme_builder_scaffold_section` first and use its
|
|
2623
|
+
`column_paths` as `parent_path` instead of hunting for one by hand.
|
|
1282
2624
|
5. `yootheme_builder_get_etag()`: fetch the optimistic-lock etag.
|
|
1283
|
-
6. `yootheme_builder_element_add({
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
`did_you_mean` suggestion when a close field
|
|
1288
|
-
to `content`). Read
|
|
1289
|
-
|
|
2625
|
+
6. `yootheme_builder_element_add({ layout_id, parent_path: "<path>", element_type: "<picked-name>", props: { ... }, etag })`
|
|
2626
|
+
checks `props` against the live type schema. On a type with a known
|
|
2627
|
+
schema, a prop key the type does not declare is ACCEPTED and stored —
|
|
2628
|
+
the answer carries a `warnings` entry with `code: "unknown_prop_keys"`
|
|
2629
|
+
naming the key, plus a `did_you_mean` suggestion when a close field
|
|
2630
|
+
exists (e.g. `contnet` to `content`). Read that and correct; do not
|
|
2631
|
+
re-guess. The VALUE side is stricter: an invalid enum member, an
|
|
2632
|
+
emptied required field, or a `<p>`-wrapped headline `content` is
|
|
2633
|
+
REFUSED with 422 before anything is written.
|
|
2634
|
+
7. (Optional) `yootheme_builder_element_update_settings({ layout_id, element_path: "<new-path>", props: { ... }, etag })`
|
|
1290
2635
|
iterates on the props. **Note: this REPLACES `props` entirely by
|
|
1291
2636
|
default; pass `merge: true` for a server-side deep-merge.** When
|
|
1292
2637
|
replacing, include every key you want to keep.
|
|
1293
|
-
8. `yootheme_builder_page_save({
|
|
1294
|
-
then `yootheme_builder_page_publish({
|
|
2638
|
+
8. `yootheme_builder_page_save({ layout_id, etag })`
|
|
2639
|
+
then `yootheme_builder_page_publish({ layout_id, etag })`.
|
|
1295
2640
|
|
|
1296
2641
|
**Common pitfalls:**
|
|
1297
2642
|
|
|
@@ -1300,14 +2645,22 @@ an instance with a sensible default props payload.
|
|
|
1300
2645
|
still works as a DEPRECATED alias, but prefer `element_type` to match
|
|
1301
2646
|
every other tool in this domain. The server's Zod schema rejects any
|
|
1302
2647
|
other key.
|
|
1303
|
-
- **Unknown prop key =
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
2648
|
+
- **Unknown prop key = a warning, not a rejection — and not silence
|
|
2649
|
+
either.** On a type with a known schema, a prop key the schema does not
|
|
2650
|
+
declare is STORED and reported: `warnings: [{ code: "unknown_prop_keys",
|
|
2651
|
+
message }]`, with a `did_you_mean` suggestion inside the message when a
|
|
2652
|
+
close field exists. It is a warning rather than a refusal because an
|
|
2653
|
+
undeclared key is not always wrong — YOOtheme itself stores props a type
|
|
2654
|
+
does not declare (a `column`'s `order_first` is written by the ROW control
|
|
2655
|
+
and is read at render), and a layout authored on a newer YOOtheme carries
|
|
2656
|
+
props the installed build has not declared yet. Refusing them would refuse
|
|
2657
|
+
layouts YOOtheme wrote. So: read the warning, and either fix the typo with
|
|
2658
|
+
`unset: ["<key>"]` — valid WITHOUT a `props` body, in which case only those keys go and every other prop stays — or keep the key knowingly. To bind a source, use
|
|
2659
|
+
`yootheme_builder_element_bind_source` rather than writing `source` /
|
|
2660
|
+
`source_extended` through `props` — those carriers are not filtered out of
|
|
2661
|
+
a props write, so a hand-written one silently competes with the bind
|
|
2662
|
+
tool's.
|
|
2663
|
+
- **Wrong parameter names on `element_add`.** Use `layout_id`
|
|
1311
2664
|
(not `pageId`), `parent_path` (not `parentPath`), `element_type`
|
|
1312
2665
|
(not `type` / `name`), `props` (not `settings`), `etag` (not
|
|
1313
2666
|
`ifMatch`).
|
|
@@ -1349,13 +2702,13 @@ yootheme_builder_element_type_get_schema({ element_type: "headline" })
|
|
|
1349
2702
|
// <p> block — see "Typography & heading styles"). If you omit title_element it
|
|
1350
2703
|
// DEFAULTS to h1, so set it explicitly on every headline to avoid multiple h1s.
|
|
1351
2704
|
yootheme_builder_element_add({
|
|
1352
|
-
|
|
1353
|
-
parent_path: "/children/0/children/2", // row inside section
|
|
2705
|
+
layout_id: "home",
|
|
2706
|
+
parent_path: "/children/0/children/2/children/0", // a column inside a row inside a section
|
|
1354
2707
|
element_type: "headline",
|
|
1355
2708
|
props: { content: "Welcome", title_element: "h1" },
|
|
1356
2709
|
etag: "abc123"
|
|
1357
2710
|
})
|
|
1358
|
-
// Response: { rel_path: "/children/0/children/2/children/0", etag: "def456" }
|
|
2711
|
+
// Response: { rel_path: "/children/0/children/2/children/0/children/0", etag: "def456" }
|
|
1359
2712
|
```
|
|
1360
2713
|
|
|
1361
2714
|
**Edge case:** A child theme can override a built-in element's
|
|
@@ -1387,37 +2740,179 @@ exposes. Never analyze findings by hand and never bulk-apply a fix.
|
|
|
1387
2740
|
write-approval prompt). Omit `checks`/`types` to run every category
|
|
1388
2741
|
against every scanned template.
|
|
1389
2742
|
2. Read the response's `findings` array. Each finding carries a `code`,
|
|
1390
|
-
`category`, `severity`,
|
|
2743
|
+
`category`, `severity`, an `affected_pages` total, up to 30 affected
|
|
2744
|
+
`pages`, and
|
|
1391
2745
|
(where a safe automatic fix exists) a `fix_hint` naming the exact
|
|
1392
2746
|
write tool and args template to run. `duplicate-layout` and
|
|
1393
2747
|
`title-collision` never carry a `fix_hint` — they need a human
|
|
1394
2748
|
decision (rename, merge, or intentionally keep as-is), not an
|
|
1395
2749
|
automatic patch.
|
|
1396
|
-
3. `yootheme_builder_template_summary({
|
|
1397
|
-
page a finding names. It carries the same
|
|
1398
|
-
`images_missing_alt`, and `disabled_elements` counters
|
|
1399
|
-
used,
|
|
1400
|
-
|
|
2750
|
+
3. `yootheme_builder_template_summary({ layout_id, include_nodes: true })`
|
|
2751
|
+
drills into one page a finding names. It carries the same
|
|
2752
|
+
`heading_levels`, `images_missing_alt`, and `disabled_elements` counters
|
|
2753
|
+
the audit used, plus (with `include_nodes:true` — opt-in, omit it for
|
|
2754
|
+
the smaller default) a per-node `content_nodes` breakdown (`path`,
|
|
2755
|
+
`type`, `status`, `effective_visible`, `hidden_on_mobile`, `headings`,
|
|
2756
|
+
`alts`), so you can see exactly WHICH node is affected, whether it
|
|
2757
|
+
is disabled or just hidden on mobile, in one call instead of pulling
|
|
2758
|
+
the raw `page_get_layout`. Page a large `content_nodes` list with
|
|
2759
|
+
`limit`/`cursor`; drill into several findings' pages in ONE call with
|
|
2760
|
+
`layout_ids:[...]` instead of `layout_id` (returns `summaries` keyed
|
|
2761
|
+
by id + `not_found`). `heading_levels` rides alongside a
|
|
2762
|
+
`heading_levels_note` sibling stating exactly which sources feed the
|
|
2763
|
+
census: every element whose title/meta channel resolves to a heading
|
|
2764
|
+
level, PLUS real `<h1>`-`<h6>` tags found inside `html`/`text`/`code`
|
|
2765
|
+
element markup. It does NOT count `module`/widget output — a WordPress
|
|
2766
|
+
widget or Joomla module has no content field this counter can parse.
|
|
2767
|
+
Since T1.4 that content is still readable, just not counted here:
|
|
2768
|
+
`yootheme_builder_element_get` on the `module`/`module_position` element
|
|
2769
|
+
resolves its referenced widget/module via a `resolved` field (see
|
|
2770
|
+
Workflow 3) — this counter is unaffected either way.
|
|
1401
2771
|
4. Fix that ONE page via the write tool named in `fix_hint.tool`, with
|
|
1402
2772
|
`fix_hint.args_template` as a starting point (`element_update_settings`,
|
|
1403
2773
|
`element_add`, etc), ETag-locked like every other write in this
|
|
1404
2774
|
server.
|
|
1405
|
-
5. `yootheme_builder_page_publish({
|
|
2775
|
+
5. `yootheme_builder_page_publish({ layout_id, etag })` republishes.
|
|
1406
2776
|
The response's `quick_check` block (default on) re-checks THIS page
|
|
1407
2777
|
with the same per-page codes, so you can confirm the fix landed
|
|
1408
2778
|
without a second full sweep.
|
|
1409
2779
|
|
|
2780
|
+
**Disabling instead of editing.** Sometimes the right remediation is to
|
|
2781
|
+
exclude an element entirely rather than edit its content (a stray leftover
|
|
2782
|
+
section, a duplicate block). `yootheme_builder_element_set_status({
|
|
2783
|
+
layout_id, element_path, status: false, etag })` writes `props.status:
|
|
2784
|
+
'disabled'` — the SAME field YOOtheme's own native "Disable element"
|
|
2785
|
+
checkbox writes, so the element is excluded from BOTH the live frontend
|
|
2786
|
+
render AND the NEXT `pages_audit`/`template_summary` (both read the same
|
|
2787
|
+
`props.status`/`disabled_elements`/`content_nodes.status` fields, so the fix
|
|
2788
|
+
is visible without a DB round-trip). Pass `status: true` to re-enable it,
|
|
2789
|
+
which restores the YOOtheme default (no status field means active) rather
|
|
2790
|
+
than persisting a fabricated `props.status`.
|
|
2791
|
+
|
|
2792
|
+
**Need a real document outline instead of findings?** `pages_audit` tells you
|
|
2793
|
+
WHAT is wrong (a missing h1, a skipped level); it never shows you the whole
|
|
2794
|
+
heading tree in one look. `yootheme_builder_heading_outline({ layout_id })`
|
|
2795
|
+
(read-only, reached via `yootheme_builder_advanced_read`) is a companion
|
|
2796
|
+
STRUCTURE view for ONE page: every H1-H6 heading in true document order, each
|
|
2797
|
+
carrying its TEXT and the element that carries it (`element_type`/`path` —
|
|
2798
|
+
`headline`, or the `html`/`text`/`code` element for a raw-HTML heading), plus
|
|
2799
|
+
the alt-status of every image on the page (`alt: null` for missing, plus
|
|
2800
|
+
`is_generic_alt` for a lazy "Logo"/"Bild"-style alt). Use it BEFORE editing a
|
|
2801
|
+
page's heading hierarchy by hand, or to double-check a `heading-skip` finding
|
|
2802
|
+
in context rather than as an isolated hit:
|
|
2803
|
+
`yootheme_builder_advanced_read({ tool: "yootheme_builder_heading_outline", arguments: { layout_id: "page:4816" } })`
|
|
2804
|
+
returns `{outline, images, h1_count, has_level_skip, images_missing_alt}` —
|
|
2805
|
+
`has_level_skip` mirrors the `heading-skip` check's EXACT rule (a level more
|
|
2806
|
+
than one above the highest seen so far, including a document that opens below
|
|
2807
|
+
h1). Pass `mode: "rendered"` to read the ACTUAL rendered DOM instead of the
|
|
2808
|
+
saved layout tree (the same trade-off `pages_audit`'s own `mode` makes: sees
|
|
2809
|
+
module/widget output, but `element_type` degrades to `""` since there is no
|
|
2810
|
+
builder node to point at).
|
|
2811
|
+
|
|
1410
2812
|
**Common pitfalls:**
|
|
1411
2813
|
|
|
1412
2814
|
- **Promising a full SEO audit.** `pages_audit` covers Builder structure
|
|
1413
2815
|
and CMS basics only: heading hierarchy, empty/thin pages, missing
|
|
1414
2816
|
image alt text, duplicate layouts, title collisions, non-pretty URL
|
|
1415
|
-
fallbacks, stale content, and dead in-page anchors. Every response
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
It does not read or
|
|
1419
|
-
title, meta description, or
|
|
1420
|
-
|
|
2817
|
+
fallbacks, stale content, and dead in-page anchors. Every response carries
|
|
2818
|
+
a `disclaimer` field naming what is out of scope, and that field says
|
|
2819
|
+
itself that its list is **not exhaustive** — pass that on rather than
|
|
2820
|
+
presenting it as the complete set of blind spots. It does not read or
|
|
2821
|
+
evaluate Yoast/RankMath/any SEO-plugin meta title, meta description, or
|
|
2822
|
+
Open Graph tags, and it does not see whether the page may be INDEXED at
|
|
2823
|
+
all. Say this scope boundary out loud and point the user at their SEO
|
|
2824
|
+
plugin for that layer.
|
|
2825
|
+
- **Answering "we don't show up on Google" from this tool alone.** The first
|
|
2826
|
+
suspect is not a heading level — it is a `noindex`, which `pages_audit` has
|
|
2827
|
+
no check for in any category. Staging sites often carry a site-wide
|
|
2828
|
+
`<meta name="robots" content="noindex, nofollow">`, and until that is lifted
|
|
2829
|
+
every other SEO finding is beside the point. Settle it first:
|
|
2830
|
+
`yootheme_builder_fetch_public_url({ url, expect: "noindex" })` —
|
|
2831
|
+
`expect_found: true` means the page asks search engines to stay away.
|
|
2832
|
+
- **Assuming builder props are the only content scanned.** Headings and
|
|
2833
|
+
image alt text are also detected inside raw HTML: an `html`/`text`
|
|
2834
|
+
element's markup content, not just builder props like
|
|
2835
|
+
`title`/`image_alt`. A sticky-bar `<h1>` or an alt-less `<img>` buried
|
|
2836
|
+
in raw markup is caught, not silently missed, unless the whole
|
|
2837
|
+
`content` prop is dynamically bound (a bound value is a stale editor
|
|
2838
|
+
preview and is skipped). Since T-A1, `code` joined `html`/`text` as a raw-HTML
|
|
2839
|
+
carrier, and anchor text + heading length/order are ALSO detected the same
|
|
2840
|
+
way: an empty/generic `<a>` link, a heading level skip, or an over-long
|
|
2841
|
+
heading buried in raw markup is caught by `generic-alt-text`/
|
|
2842
|
+
`anchor-text-quality`/`heading-skip`/`long-heading`, not just by
|
|
2843
|
+
`missing-h1`/`multiple-h1`/`no-headings`/`images-missing-alt`/`dead-anchor`.
|
|
2844
|
+
`module`/`module_position` (WordPress "Widget"/"Widget Area", Joomla
|
|
2845
|
+
"Module"/"Module Position") are NOT scanned - verified live that neither
|
|
2846
|
+
carries a markup-shaped `content` field on either platform; their actual
|
|
2847
|
+
content lives in an external WP widget instance / Joomla `#__modules` row,
|
|
2848
|
+
outside the layout JSON entirely. Since W1/T1.8 that content IS readable,
|
|
2849
|
+
just not through THIS scanner: `include_modules:true` on
|
|
2850
|
+
`yootheme_builder_pages_audit`/`yootheme_builder_page_get_layout` lists
|
|
2851
|
+
every widget/module placed on this page (`modules_by_page`/
|
|
2852
|
+
`modules_on_page`), and `yootheme_builder_advanced_read({ tool:
|
|
2853
|
+
"yootheme_builder_local_content_read", arguments: { kind: "modules" } })`
|
|
2854
|
+
returns each one's actual `title`/`settings`/`theme_overlay` plus a
|
|
2855
|
+
`resolved` companion field on `element_get` for one `module`/
|
|
2856
|
+
`module_position` element (see Workflow 3) — this scanner's blind spot is
|
|
2857
|
+
unaffected either way.
|
|
2858
|
+
- **`mode:"rendered"` sees what the builder tree structurally cannot** (T-A2,
|
|
2859
|
+
2026-07-24) — pass `mode:"rendered"` (default is `"builder"`, the tree read
|
|
2860
|
+
above) to instead render each page first (the SAME engine
|
|
2861
|
+
`yootheme_builder_render_preview` uses) and audit the ACTUAL rendered DOM.
|
|
2862
|
+
This is the one way to catch a `module`/widget's own runtime output — a
|
|
2863
|
+
newsletter-signup widget that itself renders an `<h1>`, dynamically-bound
|
|
2864
|
+
content, anything a plugin injects only at render time — none of which the
|
|
2865
|
+
builder-JSON walk above can see AT ALL. Only a curated subset of checks runs
|
|
2866
|
+
in this mode (`no-headings`, `missing-h1`, `multiple-h1`, `heading-skip`,
|
|
2867
|
+
`images-missing-alt`, `generic-alt-text`, `dead-anchor`, `anchor-text-quality`,
|
|
2868
|
+
`long-heading`, plus the three `design-*` markup codes — every check whose
|
|
2869
|
+
evidence is honestly derivable from a flat rendered document);
|
|
2870
|
+
`thin-content`/`duplicate-layout`/`empty-page`/`heading-hidden-on-mobile`/
|
|
2871
|
+
`non-pretty-url`/`stale-content` and both `header-contrast-*` codes still
|
|
2872
|
+
need the builder tree and do not run here (`header_transparent*` are section
|
|
2873
|
+
PROPS — they are gone by render time, so a rendered sweep can never produce
|
|
2874
|
+
those two findings). **Locators carry NO builder element
|
|
2875
|
+
path in this mode** — `breadcrumb`/`element_type` are empty strings BY
|
|
2876
|
+
DESIGN (there is no builder node to point at, not a resolution failure) —
|
|
2877
|
+
locate the hit by its `content_snippet` (the actual rendered text) or the
|
|
2878
|
+
page's `builder_url`/`frontend_url` instead. A site-wide rendered sweep is
|
|
2879
|
+
capped (rendering is expensive — each page runs the full YOOtheme Builder
|
|
2880
|
+
pipeline, not a JSON walk): the response always carries
|
|
2881
|
+
`render_scan: {attempted, rendered, capped}`, even when the cap never
|
|
2882
|
+
engaged, so you can always see whether every page was actually rendered.
|
|
2883
|
+
When the cap DID engage, `render_scan.skipped_by_cap` names the templates
|
|
2884
|
+
it skipped (`{id, label}`), so a code found on the scanned subset is never
|
|
2885
|
+
silently generalised to the rest — re-run narrowed with `types:[…]` to
|
|
2886
|
+
reach them. That list is itself bounded at 30 entries and says so with
|
|
2887
|
+
`skipped_by_cap_not_listed` when it had to stop.
|
|
2888
|
+
A page whose render failed is excluded from findings (never reported as a
|
|
2889
|
+
false "no headings") and listed in `render_errors` instead — check both
|
|
2890
|
+
fields before trusting a clean rendered-mode result. Each `render_errors`
|
|
2891
|
+
row carries `error_class` (the throwable's own class), and for an
|
|
2892
|
+
engine-level failure (`TypeError` and friends — a component executed during
|
|
2893
|
+
the render did not get something its own code requires) also a `reason`
|
|
2894
|
+
saying the layout is not the place to look and that the template is
|
|
2895
|
+
UNCHECKED rather than clean.
|
|
2896
|
+
- **A Joomla page carrying `module`/`module_position` is NOT a render
|
|
2897
|
+
failure** (render-fallback follow-up, 2026-07-30) — it renders via a
|
|
2898
|
+
tolerant fallback (the module/module-position subtree excluded, everything
|
|
2899
|
+
else rendered normally) rather than falling into `render_errors`, because
|
|
2900
|
+
Joomla's admin/REST context structurally cannot probe those two types (a
|
|
2901
|
+
CMS-runtime gap, not a broken layout). When this happens, the response
|
|
2902
|
+
carries `render_fallback: {pages: [{id, label, dropped_count}], suppressed_checks}`
|
|
2903
|
+
— `dropped_count` is how many module(-position) subtrees were excluded from
|
|
2904
|
+
THAT page's render, and `suppressed_checks` names which of `no-headings`,
|
|
2905
|
+
`missing-h1`, `heading-skip` were skipped on those pages specifically (the
|
|
2906
|
+
only three whose absence-based logic a hidden widget heading could
|
|
2907
|
+
fabricate — every other check, e.g. `multiple-h1`, only ever reports
|
|
2908
|
+
content that DID survive the fallback, so it keeps running). Same idea on
|
|
2909
|
+
`yootheme_builder_heading_outline` in `mode:"rendered"`: the response
|
|
2910
|
+
carries `render_fallback_used` (bool) + `render_dropped_count` (int),
|
|
2911
|
+
always present in that mode — treat `h1_count`/`has_level_skip`/the outline
|
|
2912
|
+
as possibly incomplete when `render_fallback_used` is true (content inside
|
|
2913
|
+
the excluded widget is invisible to the outline too). `WordPress renders
|
|
2914
|
+
these types successfully today, so this never engages there — WordPress
|
|
2915
|
+
pages never carry `render_fallback` / never set `render_fallback_used`.
|
|
1421
2916
|
- **Confusing the CONTENT count with `pages_list.elements_count`.**
|
|
1422
2917
|
`pages_list.elements_count` counts every node, including
|
|
1423
2918
|
`section`/`row`/`column`/`fragment` layout wrappers. `pages_audit`
|
|
@@ -1538,7 +3033,7 @@ What changes in the response:
|
|
|
1538
3033
|
tool returns `target_not_scannable` (HTTP 422). Tell the user the page could
|
|
1539
3034
|
not be audited and why; then audit it via `template_summary`, or pick a page
|
|
1540
3035
|
`pages_list` returns. Never present it as a clean result.
|
|
1541
|
-
- **`resolved_target`** — `{
|
|
3036
|
+
- **`resolved_target`** — `{ layout_id, post_id, type, label, frontend_url,
|
|
1542
3037
|
context, resolved_via, matched_by }`. This is the echo of WHICH page a fuzzy
|
|
1543
3038
|
address landed on. Always report it: `special: "home"` and
|
|
1544
3039
|
`title: "SunWest"` are guesses until the server says what they resolved to.
|
|
@@ -1548,36 +3043,29 @@ What changes in the response:
|
|
|
1548
3043
|
- **Richer per-hit evidence.** Single mode raises the per-page hit cap from 5 to
|
|
1549
3044
|
**100 hits per check** and adds a `content_snippet` to every locator (the site
|
|
1550
3045
|
sweep omits snippets to keep a whole-site payload small). The text leg also
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
the
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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`.
|
|
3046
|
+
keeps the per-hit locators under `findings[].pages[].locators` in both scopes.
|
|
3047
|
+
Both legs of the answer are the same JSON payload — there is no rendered
|
|
3048
|
+
summary table and no `Where to fix:` block on either, in either scope, so
|
|
3049
|
+
nothing labels a column for you and the two counters below are the whole
|
|
3050
|
+
story.
|
|
3051
|
+
- **Two counters, two levels, and neither is called `count`.**
|
|
3052
|
+
`findings[].affected_pages` is how many PAGES that finding matched. In
|
|
3053
|
+
single-target mode it is structurally `1` for every finding, so it is never
|
|
3054
|
+
the size of the problem there. `findings[].pages[].occurrences_here` is how
|
|
3055
|
+
many occurrences sit on THAT page — 19 alt-less images ⇒ `19`. It is the
|
|
3056
|
+
uncapped total: `hits_truncated` shortens the listed locators, never this
|
|
3057
|
+
figure. Report `occurrences_here` as the size of a single-page problem, and
|
|
3058
|
+
never `affected_pages` — an agent once reported "one image is affected"
|
|
3059
|
+
where nineteen were.
|
|
3060
|
+
- **A page entry with no `occurrences_here` is not a gap.** Some checks make the
|
|
3061
|
+
PAGE ITSELF the hit — `thin-content`, `empty-page`, `duplicate-layout` — and
|
|
3062
|
+
those entries carry no occurrence total at all, because there is nothing on
|
|
3063
|
+
the page to count more than once.
|
|
3064
|
+
- **What `occurrences_here` counts, the response says once.** The top-level
|
|
3065
|
+
`occurrences_unit` states it in words: matching ELEMENTS in the saved layout
|
|
3066
|
+
tree, a FLOOR when a bound row repeats or content is conditional, since the
|
|
3067
|
+
saved tree enumerates neither. Pass that caveat on rather than presenting the
|
|
3068
|
+
number as what a visitor's browser renders.
|
|
1581
3069
|
|
|
1582
3070
|
Two request shapes are refused up front, with a typed error rather than a
|
|
1583
3071
|
round-trip: more than one address key → `too_many_targets` (pick one; use
|
|
@@ -1587,6 +3075,88 @@ exactly one page, so there is nothing for a type filter to narrow). A `title`
|
|
|
1587
3075
|
that matches several pages returns `title_ambiguous` with a candidate list —
|
|
1588
3076
|
show it, let the user choose.
|
|
1589
3077
|
|
|
3078
|
+
### Design-Consistency-Check (`checks: ["design"]`)
|
|
3079
|
+
|
|
3080
|
+
A fifth audit category, alongside `seo`/`structure`/`content`/`links`, in the
|
|
3081
|
+
DEFAULT-ALL set (no opt-in flag needed — just call `pages_audit` normally, or
|
|
3082
|
+
narrow to it with `checks: ["design"]`). It flags markup that breaks OUT of
|
|
3083
|
+
the site's UIkit design system — most commonly a **plugin's own output**
|
|
3084
|
+
(Formidable, ACF, a shortcode-rendered form) carrying none of YOOtheme's
|
|
3085
|
+
`uk-*` classes, e.g. a form submit button with no `uk-button`. Native
|
|
3086
|
+
YOOtheme builder elements (grid_item, panel, button, …) are never checked —
|
|
3087
|
+
the theme always renders them with the right `uk-*` class; there is nothing
|
|
3088
|
+
to flag there.
|
|
3089
|
+
|
|
3090
|
+
Three finding codes, all `warning` severity, none carrying a `fix_hint` (see
|
|
3091
|
+
below for why):
|
|
3092
|
+
|
|
3093
|
+
- **`design-missing-uikit-class`** — a contract-bearing element
|
|
3094
|
+
(button/select/textarea/input/`a[role=button]`/form/label-in-a-form) with
|
|
3095
|
+
no class at all, or one that already carries a DIFFERENT real `uk-*` class
|
|
3096
|
+
(on-system, just incomplete) — its expected class is simply absent.
|
|
3097
|
+
- **`design-foreign-class`** — the SAME element universe, but it carries
|
|
3098
|
+
class(es) and NONE of them are real UIkit classes — positive evidence a
|
|
3099
|
+
DIFFERENT framework (Bootstrap's `btn btn-primary`, a plugin's own
|
|
3100
|
+
`form-control`) is actively styling it instead of UIkit.
|
|
3101
|
+
- **`design-inline-style`** — ANY element (no tag restriction) with a
|
|
3102
|
+
non-empty `style=` attribute inside embedded content — bypasses the theme's
|
|
3103
|
+
design system entirely.
|
|
3104
|
+
|
|
3105
|
+
A FOURTH and FIFTH code share the `design` category but are not about markup at
|
|
3106
|
+
all — they read section props, not embedded HTML, so unlike the three above they
|
|
3107
|
+
are **builder-mode only** and never run in `mode: "rendered"` (the props do not
|
|
3108
|
+
exist in a rendered document):
|
|
3109
|
+
|
|
3110
|
+
- **`header-contrast-risk`** (`warning`) — the first section pulls the
|
|
3111
|
+
navigation over itself in a colour the theme's own `--uk-inverse` declaration
|
|
3112
|
+
contradicts, with no image or video hiding the background that declaration
|
|
3113
|
+
describes. Unlike the three above it DOES carry a `fix_hint`.
|
|
3114
|
+
- **`header-contrast-unverifiable`** (`info`) — the same transparent header over
|
|
3115
|
+
a hero medium, whatever the colour: nothing there is provable from the props,
|
|
3116
|
+
so no colour is recommended and readability is only verifiable by looking.
|
|
3117
|
+
|
|
3118
|
+
See their remediation-catalog entries below.
|
|
3119
|
+
|
|
3120
|
+
**Run the three markup codes in `mode: "rendered"` for the highest-value
|
|
3121
|
+
result.** A Formidable/ACF form's markup exists ONLY at render time — it is
|
|
3122
|
+
invisible to a builder-tree walk unless it happens to be typed static markup
|
|
3123
|
+
inside an `html`/`text`/`code` element. `mode: "rendered"` renders each page
|
|
3124
|
+
first (the SAME engine `render_preview` uses) and audits the ACTUAL output, so
|
|
3125
|
+
a plugin shortcode's real markup is exactly what gets checked. (The two
|
|
3126
|
+
`header-contrast-*` codes do NOT run in that mode — use the default
|
|
3127
|
+
`mode: "builder"` when you want them.)
|
|
3128
|
+
|
|
3129
|
+
```jsonc
|
|
3130
|
+
yootheme_builder_advanced_read({
|
|
3131
|
+
tool: "yootheme_builder_pages_audit",
|
|
3132
|
+
arguments: { mode: "rendered", checks: ["design"], target: { special: "contact" } }
|
|
3133
|
+
})
|
|
3134
|
+
```
|
|
3135
|
+
|
|
3136
|
+
**No `fix_hint` on any of the three codes — this is deliberate, not a gap.**
|
|
3137
|
+
The offending markup lives inside an opaque `props.content` string (an
|
|
3138
|
+
html/text/code element) or a plugin's own rendered output — there is no
|
|
3139
|
+
structured prop `element_update_settings` could merge a class into without
|
|
3140
|
+
string-surgery on customer HTML, which this server's read-only-analysis
|
|
3141
|
+
policy forbids on the write path. Report the locator's `content_snippet`
|
|
3142
|
+
(the offending element's OWN markup, e.g. `<button class="btn btn-primary">`
|
|
3143
|
+
— unlike every other finding's snippet, this one deliberately keeps the
|
|
3144
|
+
class/style attribute instead of stripping it, since that attribute IS the
|
|
3145
|
+
evidence) and tell the user to add the class by hand, or fix it in the
|
|
3146
|
+
plugin's own settings/template.
|
|
3147
|
+
|
|
3148
|
+
**Design-consistency fixes: ground the exact class via Context7 when it is
|
|
3149
|
+
available.** A `design-missing-uikit-class` or `design-foreign-class` finding
|
|
3150
|
+
asks you to name a real UIkit class by hand; recalling one purely from
|
|
3151
|
+
training data risks a subtly wrong suggestion, since class names can drift
|
|
3152
|
+
across UIkit versions. When the user's client has Context7 (or an equivalent
|
|
3153
|
+
documentation-grounding MCP) configured, query it (library id
|
|
3154
|
+
`/websites/getuikit`) to confirm the exact class before naming it in the fix.
|
|
3155
|
+
Without Context7, degrade cleanly: keep working from the tool's own output
|
|
3156
|
+
(the locator's `content_snippet`) plus the class names already catalogued
|
|
3157
|
+
above, and never invent a class name you have not confirmed one way or the
|
|
3158
|
+
other.
|
|
3159
|
+
|
|
1590
3160
|
### Remediation catalog — what each finding means and how to fix it
|
|
1591
3161
|
|
|
1592
3162
|
The audit reports; YOU explain. Every finding below carries its fixed
|
|
@@ -1607,6 +3177,87 @@ on the CONTENT count (`elements_content`), so wrapper-only pages — three neste
|
|
|
1607
3177
|
columns around nothing — are correctly flagged rather than passing as "not
|
|
1608
3178
|
empty".
|
|
1609
3179
|
|
|
3180
|
+
#### `dangling-source` — structure · critical
|
|
3181
|
+
|
|
3182
|
+
**Why it matters.** The page is not empty and nothing errored: an element IS
|
|
3183
|
+
there, bound to a source name that no longer exists. YOOtheme resolves the
|
|
3184
|
+
binding to nothing and renders nothing, so the gap looks identical to a layout
|
|
3185
|
+
problem while the cause sits in the source list. Without this finding the only
|
|
3186
|
+
way to spot it is to compare every binding on the site against
|
|
3187
|
+
`sources_list` by hand.
|
|
3188
|
+
**How to fix.** Compare each reported `source_name` against
|
|
3189
|
+
`yootheme_builder_sources_list`. Renamed elsewhere → repair the reference with
|
|
3190
|
+
`yootheme_builder_repair_source_references({from, to})` (that is the shipped
|
|
3191
|
+
`fix_hint`; `to` must be a currently registered name). Genuinely deleted →
|
|
3192
|
+
unbind the element instead, or rebuild the binding against a source that
|
|
3193
|
+
exists.
|
|
3194
|
+
**Severity.** Critical: the element renders nothing at all, and the page ships
|
|
3195
|
+
looking finished.
|
|
3196
|
+
|
|
3197
|
+
#### `unknown-source-fields` — structure · critical
|
|
3198
|
+
|
|
3199
|
+
**Why it matters.** The value-side sibling of `dangling-source`: the bound
|
|
3200
|
+
source EXISTS, but one or more `field_mappings` values name a source field the
|
|
3201
|
+
source does not emit, so that mapped slot binds to nothing and renders blank.
|
|
3202
|
+
Neither `empty-page` (an element IS there) nor `dangling-source` (the source
|
|
3203
|
+
name IS known) catches this — it falls through both rasters, and the same
|
|
3204
|
+
diagnosis is what `element_get_binding`/`inspect_multi_items_binding` already
|
|
3205
|
+
surface for a single element inspected by hand.
|
|
3206
|
+
**How to fix.** Compare each reported `unknown_source_field_values` entry
|
|
3207
|
+
against the `unknown_source_field_values_available` list that rides alongside
|
|
3208
|
+
it, then `element_bind_source` with a corrected `field_mappings` value (that is
|
|
3209
|
+
the shipped `fix_hint`).
|
|
3210
|
+
**Severity.** Critical: the mapped value renders nothing at all, and — unlike a
|
|
3211
|
+
typo'd element prop, which is refused before it writes — this one persists
|
|
3212
|
+
silently.
|
|
3213
|
+
|
|
3214
|
+
#### `container-missing-item-child` — structure · warning
|
|
3215
|
+
|
|
3216
|
+
**Why it matters.** A multi-item container (`grid`, `button`, `slideshow`,
|
|
3217
|
+
`accordion`, …) repeats its `*_item` child once per source row. With no such
|
|
3218
|
+
child there is nothing to repeat, so the container renders nothing — again with
|
|
3219
|
+
no error anywhere. `element_add` already says this at write time; this finding
|
|
3220
|
+
catches the states that write never saw, such as the child being deleted later
|
|
3221
|
+
or a library insert that dropped it.
|
|
3222
|
+
**How to fix.** `element_add` the type named in each locator's
|
|
3223
|
+
`expected_child_type` under the reported container path (that is the shipped
|
|
3224
|
+
`fix_hint`). Read its required props from `element_type_get_schema`, whose
|
|
3225
|
+
`minimal_layout` shows exactly this container-plus-child shape. If the container
|
|
3226
|
+
is meant to be bound to a source, bind the CHILD, never the container.
|
|
3227
|
+
**Severity.** Warning, not critical: the element genuinely renders nothing, but
|
|
3228
|
+
building the shell first and its child second is the order `minimal_layout`
|
|
3229
|
+
itself teaches — a page caught mid-build is half-finished, not broken.
|
|
3230
|
+
|
|
3231
|
+
#### `binding-renders-nothing` — structure · warning (publish/save only)
|
|
3232
|
+
|
|
3233
|
+
**Why it matters.** A bind that persisted is not a bind that delivers. The
|
|
3234
|
+
element is there, the source name is valid, the field mappings are in place, the
|
|
3235
|
+
save succeeded — and the page renders zero rows. Nothing on that path says so,
|
|
3236
|
+
which is why a customer once bound, published, looked, re-bound with `limit`,
|
|
3237
|
+
published, looked again, and only then ran `render_preview` and read
|
|
3238
|
+
`rendered_item_wrapper_count: 0`. This finding is that sentence, arriving two
|
|
3239
|
+
cycles earlier.
|
|
3240
|
+
**How to fix.** Each locator names the element AND its `source_name`. Usual
|
|
3241
|
+
causes in order: the source needs a context this template does not have (an
|
|
3242
|
+
archive source on a single page has no term to scope to — check with
|
|
3243
|
+
`source_get_schema` whether it takes a term/id argument), a filter or
|
|
3244
|
+
`query_arguments` narrowed it to nothing, or the source genuinely holds no
|
|
3245
|
+
entries yet. To check ONE binding as you make it rather than at publish, pass
|
|
3246
|
+
`probe: true` to `element_bind_source`.
|
|
3247
|
+
**Severity.** Warning: a source with no rows yet is a legitimate state (an empty
|
|
3248
|
+
category), and the page is not broken — it is empty where content was expected.
|
|
3249
|
+
|
|
3250
|
+
> **Scope, and it is narrower than every other `per-page` code.** This check
|
|
3251
|
+
> needs a ROW COUNT, and nothing in this product can produce one without
|
|
3252
|
+
> rendering. It therefore reads the render report that `page_save` / `page_publish`
|
|
3253
|
+
> has ALREADY computed for its own render-precheck — so it rides `quick_check`
|
|
3254
|
+
> and **never appears in `pages_audit`**, not even with `checks: ["structure"]`.
|
|
3255
|
+
> Rendering every page of a site to run one check would turn a sweep into
|
|
3256
|
+
> minutes. It also stays silent when the render threw (the precheck owns that
|
|
3257
|
+
> verdict), when the render used its module fallback (a partial render cannot
|
|
3258
|
+
> settle a zero), and when nothing on the page was supposed to iterate at all —
|
|
3259
|
+
> a `headline` bound to one field renders content and zero rows by construction.
|
|
3260
|
+
|
|
1610
3261
|
#### `no-headings` — structure · critical
|
|
1611
3262
|
|
|
1612
3263
|
**Why it matters.** With no heading of any level the page has no machine-readable
|
|
@@ -1644,6 +3295,25 @@ second, independent heading channel), which is exactly what the raw path cannot.
|
|
|
1644
3295
|
N times through an iterating container or a Multi-Items binding, so the fix is
|
|
1645
3296
|
one edit on the item, not N edits; the locator's `×repeat` shows the multiplier.
|
|
1646
3297
|
|
|
3298
|
+
#### `heading-hidden-on-mobile` - structure · warning
|
|
3299
|
+
|
|
3300
|
+
**Why it matters.** Google indexes mobile-first: a heading that renders on
|
|
3301
|
+
desktop but is hidden at the base (phone) UIkit breakpoint is effectively
|
|
3302
|
+
invisible for ranking, even though it exists in the markup and a desktop
|
|
3303
|
+
review would never catch it.
|
|
3304
|
+
**How to fix.** No `fix_hint`. Hiding a heading on mobile is sometimes
|
|
3305
|
+
intentional (a duplicate mobile-only title elsewhere). Check the node's
|
|
3306
|
+
`props.visibility` (own or an ancestor's) and either remove the
|
|
3307
|
+
`hidden@`/`visible@` class or confirm an equivalent heading is visible on
|
|
3308
|
+
mobile some other way.
|
|
3309
|
+
**Severity.** Warning. Strict subset of ONLY the headline-TYPE portion of the
|
|
3310
|
+
pool `heading-skip` reads (since T-A1, `heading-skip` ALSO covers raw-HTML
|
|
3311
|
+
h1-h6 tags inside `html`/`text`/`code` elements - this check does not; a
|
|
3312
|
+
raw-HTML heading's mobile-visibility is not tracked): `hidden@<bp>` hides FROM
|
|
3313
|
+
that breakpoint upward (mobile stays visible), `visible@<bp>` is the mirror
|
|
3314
|
+
(hidden below it, mobile included), a bare `hidden` hides everywhere. Only the
|
|
3315
|
+
last two ever fire this check.
|
|
3316
|
+
|
|
1647
3317
|
#### `heading-skip` — structure · info
|
|
1648
3318
|
|
|
1649
3319
|
**Why it matters.** Jumping h1 → h3 breaks the document outline: assistive tech
|
|
@@ -1652,9 +3322,12 @@ reports a missing level, and the hierarchy stops describing the real structure.
|
|
|
1652
3322
|
the jumped-to heading (`element_update_settings`, `props.title_element`) or add
|
|
1653
3323
|
the missing intermediate level. The evidence lists each skip as
|
|
1654
3324
|
`{ path, from, to }`.
|
|
1655
|
-
**Severity.** Info: it is a real hierarchy defect but the page remains usable
|
|
1656
|
-
|
|
1657
|
-
|
|
3325
|
+
**Severity.** Info: it is a real hierarchy defect but the page remains usable.
|
|
3326
|
+
Reads BOTH `headline` elements AND raw-HTML h1-h6 tags inside `html`/`text`/
|
|
3327
|
+
`code` elements' `content` (interleaved in one true document-order sequence,
|
|
3328
|
+
since T-A1) - but container-STARTED hierarchies (e.g. a page whose only
|
|
3329
|
+
headings are 3× h3 grid-item titles with no h1/h2 anywhere) remain invisible
|
|
3330
|
+
to it. Do not present it as complete heading-hierarchy coverage.
|
|
1658
3331
|
|
|
1659
3332
|
#### `images-missing-alt` — content · warning
|
|
1660
3333
|
|
|
@@ -1750,7 +3423,12 @@ prop is `title`. For an icon-only or image-only link, the right fix is
|
|
|
1750
3423
|
`props.link_aria_label`, not invented visible text. Only link-bearing elements
|
|
1751
3424
|
whose visible-text prop is known are checked — `button`, `button_item`,
|
|
1752
3425
|
`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`.
|
|
3426
|
+
this is an allowlist, not a sweep over every element carrying a `link`. Since
|
|
3427
|
+
T-A1, a raw `<a href>` tag inside an `html`/`text`/`code` element's `content` is
|
|
3428
|
+
ALSO checked - that path judges every anchor with a set `href` (any
|
|
3429
|
+
destination, not just in-page), exempting an empty one only when it carries an
|
|
3430
|
+
`aria-label` or a nested `<img alt>` (the raw-HTML mirror of the allowlist's
|
|
3431
|
+
icon/image/aria-label exemption).
|
|
1754
3432
|
**Severity.** Warning. The locator's `reason` says which rule fired: `empty` or
|
|
1755
3433
|
`generic` (an **exact match** against the DE+EN generic list — `mehr`, `hier`,
|
|
1756
3434
|
`weiterlesen`, `click here`, `read more`, `learn more`, …). Exact match only, so
|
|
@@ -1779,7 +3457,9 @@ the image actually shows ("SunWest Büro in Manresa").
|
|
|
1779
3457
|
the EMPTY case). Only an **exact match** against the generic list (`image`,
|
|
1780
3458
|
`photo`, `picture`, `logo`, `icon`, `bild`, `foto`, `grafik`) fires, so
|
|
1781
3459
|
`"ACME Logo"` is already a good alt text and is never flagged. Bound alt/image
|
|
1782
|
-
props are exempt.
|
|
3460
|
+
props are exempt. Since T-A1, a raw `<img alt="...">` inside an `html`/`text`/
|
|
3461
|
+
`code` element's `content` is checked the SAME way, judged against the
|
|
3462
|
+
identical generic list.
|
|
1783
3463
|
|
|
1784
3464
|
#### `long-heading` — structure · info
|
|
1785
3465
|
|
|
@@ -1790,8 +3470,9 @@ keyword focus, get truncated in search results and wrap badly on mobile.
|
|
|
1790
3470
|
detail into the body text below it. The locator carries the measured `length`.
|
|
1791
3471
|
**Severity.** Info — a long heading is a quality signal, never a defect. The
|
|
1792
3472
|
threshold is **70 characters**, measured multibyte-safely over the
|
|
1793
|
-
markup-stripped text, on unbound static text only. Scope
|
|
1794
|
-
|
|
3473
|
+
markup-stripped text, on unbound static text only. Scope: `headline` plus the
|
|
3474
|
+
standalone title carriers (`alert`, `overlay`, `panel`), PLUS (since T-A1) every
|
|
3475
|
+
h1-h6 tag's own text inside an `html`/`text`/`code` element's raw `content`.
|
|
1795
3476
|
Multi-Items containers keep their title text on the paired `*_item` children and
|
|
1796
3477
|
are not measured — do not claim full heading-length coverage.
|
|
1797
3478
|
|
|
@@ -1799,21 +3480,172 @@ are not measured — do not claim full heading-length coverage.
|
|
|
1799
3480
|
checks, so they also ride in the `quick_check` block on `page_publish` /
|
|
1800
3481
|
`page_save` alongside the other per-page codes.
|
|
1801
3482
|
|
|
3483
|
+
#### `design-missing-uikit-class` — design · warning
|
|
3484
|
+
|
|
3485
|
+
**Why it matters.** A contract-bearing element (a form's submit button, its
|
|
3486
|
+
text inputs, its labels) with no `uk-*` class at all renders unstyled — it
|
|
3487
|
+
visually breaks out of the site's design system, most visibly on a
|
|
3488
|
+
plugin-rendered form (Formidable, ACF) dropped onto an otherwise polished
|
|
3489
|
+
page.
|
|
3490
|
+
**How to fix.** No `fix_hint` — see "Design-Consistency-Check" above for why.
|
|
3491
|
+
Add the missing class by hand (`uk-button` on a submit button, `uk-input` on
|
|
3492
|
+
a text input, `uk-form-label` on a label inside a form, `uk-form-stacked` or
|
|
3493
|
+
`uk-form-horizontal` on the form itself) — via the plugin's own class/CSS
|
|
3494
|
+
settings when it is a plugin form, or by editing the raw markup when it is a
|
|
3495
|
+
custom html/text/code element.
|
|
3496
|
+
**Severity.** Warning — a visual/consistency defect, not a functional one.
|
|
3497
|
+
Fires when the element carries NO class at all, or already carries a
|
|
3498
|
+
DIFFERENT real UIkit class (on-system, just incomplete) — never confuse this
|
|
3499
|
+
with `design-foreign-class` below.
|
|
3500
|
+
|
|
3501
|
+
#### `design-foreign-class` — design · warning
|
|
3502
|
+
|
|
3503
|
+
**Why it matters.** The SAME contract-bearing universe as
|
|
3504
|
+
`design-missing-uikit-class`, but here the element DOES carry class(es) and
|
|
3505
|
+
NONE of them are real UIkit classes — positive evidence that a DIFFERENT
|
|
3506
|
+
framework (Bootstrap's `btn btn-primary`, a plugin's own `form-control`) is
|
|
3507
|
+
actively styling it instead of UIkit. This is a REPLACEMENT problem, not a
|
|
3508
|
+
silence: the fix is a class swap, not an addition.
|
|
3509
|
+
**How to fix.** No `fix_hint`. Replace the foreign class with UIkit's
|
|
3510
|
+
equivalent (`btn btn-primary` → `uk-button uk-button-primary`), typically in
|
|
3511
|
+
the plugin's own template/class settings rather than in YOOtheme.
|
|
3512
|
+
**Severity.** Warning. The locator's `content_snippet` shows the element's
|
|
3513
|
+
own markup WITH its class attribute — the exact evidence, never tag-stripped
|
|
3514
|
+
like every other snippet in this audit.
|
|
3515
|
+
|
|
3516
|
+
#### `design-inline-style` — design · warning
|
|
3517
|
+
|
|
3518
|
+
**Why it matters.** A `style=` attribute inside embedded content (a
|
|
3519
|
+
`<div style="color:red">` in an html/text/code element, or in a plugin's
|
|
3520
|
+
rendered output) hard-codes styling that bypasses the theme's design system
|
|
3521
|
+
entirely — it cannot be themed, cannot respond to a style/color-mode change,
|
|
3522
|
+
and is invisible to any CSS audit that only looks at stylesheets.
|
|
3523
|
+
**How to fix.** No `fix_hint`. Move the styling into a UIkit class or the
|
|
3524
|
+
theme's own CSS, or remove it if it was accidental (a rich-text editor
|
|
3525
|
+
sometimes injects one). ANY tag can carry this finding — there is no
|
|
3526
|
+
tag restriction, unlike the two class-based checks above.
|
|
3527
|
+
**Severity.** Warning.
|
|
3528
|
+
|
|
3529
|
+
All three are `appliesTo: per-page` but — unlike `anchor-text-quality` /
|
|
3530
|
+
`generic-alt-text` / `long-heading` above — they deliberately do NOT ride the
|
|
3531
|
+
`quick_check` block on `page_publish` / `page_save`: `quick_check` is a
|
|
3532
|
+
publish-time advisory scoped to what a single-template pass can see without a
|
|
3533
|
+
live vocabulary, and design detection needs the site's actual compiled UIkit
|
|
3534
|
+
CSS. Run `pages_audit` (ideally `mode: "rendered"`) to see these three markup
|
|
3535
|
+
findings. The two `header-contrast-*` codes below skip `quick_check` for the
|
|
3536
|
+
same reason but are **builder-mode only** — `mode: "rendered"` never emits them.
|
|
3537
|
+
|
|
3538
|
+
#### `header-contrast-risk` — design · warning
|
|
3539
|
+
|
|
3540
|
+
**Why it matters.** The page's FIRST section pulls the navigation over itself
|
|
3541
|
+
(`header_transparent`) and forces a `header_transparent_text_color` the site's
|
|
3542
|
+
own theme does not declare for that section style. The forced colour wins —
|
|
3543
|
+
`.uk-inverse-light` / `.uk-inverse-dark` carry `!important` and beat the
|
|
3544
|
+
style's own declaration — so this is not a taste question: it is white
|
|
3545
|
+
navigation on a white hero, or dark navigation on a dark one. Nothing in the
|
|
3546
|
+
builder warns about it. See "Transparent header (the exception — never a
|
|
3547
|
+
default)" for the whole trap family.
|
|
3548
|
+
|
|
3549
|
+
**One class only, and it is decidable.** The page entry carries `risk_class`,
|
|
3550
|
+
`confidence`, `text_color`, `recommended_text_color`, `style` and
|
|
3551
|
+
`media_backdrop`:
|
|
3552
|
+
|
|
3553
|
+
- **`risk_class: "B"`, `confidence: "high"`** — no image or video covers the
|
|
3554
|
+
section background, so the contradiction is decidable from the layout alone.
|
|
3555
|
+
`recommended_text_color` is the theme's OWN declared inverse for that style
|
|
3556
|
+
(READ from the compiled `theme.css` — not a guess, not a computed contrast
|
|
3557
|
+
ratio).
|
|
3558
|
+
|
|
3559
|
+
**`pages_audit` never emits this code over a hero image or video.** A medium
|
|
3560
|
+
covers exactly the background `--uk-inverse` describes, so there is nothing to
|
|
3561
|
+
contradict and nothing to recommend; that state is the
|
|
3562
|
+
`header-contrast-unverifiable` info finding below, at one severity whatever the
|
|
3563
|
+
colour. A transparent header you do not see reported here is therefore *not*
|
|
3564
|
+
endorsed — look for the info finding.
|
|
3565
|
+
|
|
3566
|
+
**How to fix.** `fix_hint` → `header_transparency_set` with `mode: "auto"` and
|
|
3567
|
+
`dry_run: true`. The dry-run reports the current state, the theme-declared
|
|
3568
|
+
inverse, `risk_class`, `confidence` and exactly what would change; repeat with
|
|
3569
|
+
`dry_run: false` + an `etag` to write it. The tool resolves the FIRST section
|
|
3570
|
+
itself — you do not pass an `element_path`, and the repair therefore cannot
|
|
3571
|
+
land on a section whose props YOOtheme never reads. `mode: "auto"` writes the
|
|
3572
|
+
theme's own declared value, so no colour is ever guessed. Turning the
|
|
3573
|
+
transparent header off entirely is always the safe direction and is never
|
|
3574
|
+
refused (`transparent: false`).
|
|
3575
|
+
|
|
3576
|
+
**Severity.** Warning. It never blocks a write, and it is per-page but does NOT
|
|
3577
|
+
ride `quick_check` (same reason as the three design codes above: the verdict
|
|
3578
|
+
needs the site's compiled `theme.css`, which the publish-time pass does not
|
|
3579
|
+
read). **When that CSS cannot be read, or the theme declares nothing for that
|
|
3580
|
+
style, there is no finding at all** — an unreadable theme yields silence, never
|
|
3581
|
+
a guess. It is also **builder-mode-only**: `header_transparent*` are section
|
|
3582
|
+
props, so `mode: "rendered"` never emits this code — a clean rendered audit is
|
|
3583
|
+
NOT evidence that the header is fine.
|
|
3584
|
+
|
|
3585
|
+
**What it does NOT report.** A dead prop (a colour set while
|
|
3586
|
+
`header_transparent` is off) and props on a non-first section are not contrast
|
|
3587
|
+
risks and are not reported here — see the transparent-header section for those
|
|
3588
|
+
two traps.
|
|
3589
|
+
|
|
3590
|
+
#### `header-contrast-unverifiable` — design · info
|
|
3591
|
+
|
|
3592
|
+
**Why it matters.** The page's FIRST section pulls the navigation over itself
|
|
3593
|
+
AND carries a hero image or video —
|
|
3594
|
+
**whatever `header_transparent_text_color` holds**.
|
|
3595
|
+
Nothing here is provable: the theme's `--uk-inverse` declaration
|
|
3596
|
+
describes the section's BACKGROUND COLOUR, and the medium hides exactly that
|
|
3597
|
+
background, so "is the navigation readable?" cannot be answered from the layout
|
|
3598
|
+
at all. Which part of a `cover` image ends up under the header depends on the
|
|
3599
|
+
viewport.
|
|
3600
|
+
|
|
3601
|
+
`risk_class: "C"`, `confidence: "low"`, `recommended_text_color: null`.
|
|
3602
|
+
|
|
3603
|
+
**`null`, not `""` — this code recommends no colour at all.**
|
|
3604
|
+
`""` is not the humble answer: it is the theme's default colour, a concrete
|
|
3605
|
+
rendered value, so writing it back over a colour someone set deliberately (or
|
|
3606
|
+
measured in a browser) is a change, not an abstention. Over a dark hero photo
|
|
3607
|
+
on a `--uk-inverse: dark` style, `""` renders dark navigation on a dark image.
|
|
3608
|
+
|
|
3609
|
+
**One severity for the whole media case, deliberately.** An earlier revision
|
|
3610
|
+
graded it — `""` info, any other colour a warning — and that punished the
|
|
3611
|
+
correct action: the colour a browser measurement finds readable over a dark
|
|
3612
|
+
hero is usually the one the theme does NOT declare, so an eyes-verified page
|
|
3613
|
+
scored a warning whose remedy was to revert it. Behind a medium there is no
|
|
3614
|
+
baseline to deviate from, so there is nothing to grade.
|
|
3615
|
+
|
|
3616
|
+
**How to fix.** No `fix_hint`, because there is nothing to write. **Look at
|
|
3617
|
+
it** — render the page (see "Look at what you built") at the customer's
|
|
3618
|
+
viewport, unscrolled AND after scrolling, and read the navigation. If it is
|
|
3619
|
+
unreadable, set the colour that IS readable over that medium with
|
|
3620
|
+
`header_transparency_set({ mode: "explicit", text_color: … })` —
|
|
3621
|
+
a value you have seen, never one
|
|
3622
|
+
you inferred. `mode: "auto"` deliberately writes no colour here, so it cannot
|
|
3623
|
+
help you: it has not seen the page either. If it reads fine, say so and move on.
|
|
3624
|
+
|
|
3625
|
+
**The finding stays at `info` afterwards, and that is correct.** A transparent
|
|
3626
|
+
header over a medium is never provable from props, so the honest end state is
|
|
3627
|
+
"no warning, still only verifiable by eye" — not "clean". Report it as verified
|
|
3628
|
+
and move on; do not go looking for a colour that makes it disappear.
|
|
3629
|
+
|
|
3630
|
+
**Severity.** Info — an open question, not a defect. Same fail-open rule as the
|
|
3631
|
+
warning above: no readable `theme.css`, no finding. Same builder-mode-only
|
|
3632
|
+
restriction, same absence from `quick_check`.
|
|
3633
|
+
|
|
1802
3634
|
### The report contract — how to present an audit
|
|
1803
3635
|
|
|
1804
3636
|
The raw response is a machine payload. What you hand the user follows this
|
|
1805
3637
|
shape every time, so two audits of two sites read the same way:
|
|
1806
3638
|
|
|
1807
3639
|
1. **Lead with the scope.** When `scope: "single"`, name the page the audit
|
|
1808
|
-
actually ran on from `resolved_target` — label, `
|
|
3640
|
+
actually ran on from `resolved_target` — label, `layout_id`, and
|
|
1809
3641
|
`matched_by` when the address was fuzzy ("`special: "home"` resolved to
|
|
1810
3642
|
*Startseite* (`page:4816`)"). When the response has no `scope`, it was a site
|
|
1811
3643
|
sweep: lead with `scanned.templates` and the `summary` totals.
|
|
1812
3644
|
2. **Order by severity: critical → warning → info.** The server already sorts
|
|
1813
3645
|
that way; never re-rank findings by how easy they look to fix.
|
|
1814
3646
|
3. **Group page → category → finding.** One block per affected page, its
|
|
1815
|
-
findings grouped by `category` (`structure`, `content`, `seo`, `links
|
|
1816
|
-
the user can fix one page in one pass instead of hopping between pages per
|
|
3647
|
+
findings grouped by `category` (`structure`, `content`, `seo`, `links`,
|
|
3648
|
+
`design`), so the user can fix one page in one pass instead of hopping between pages per
|
|
1817
3649
|
code.
|
|
1818
3650
|
4. **Render every hit from its locator**, in this shape:
|
|
1819
3651
|
|
|
@@ -1832,7 +3664,13 @@ shape every time, so two audits of two sites read the same way:
|
|
|
1832
3664
|
`element_type`) plus the `breadcrumb` that locates it in the builder. A **bare
|
|
1833
3665
|
path list is NOT acceptable output** — `/children/0/children/2` tells the user
|
|
1834
3666
|
nothing about which box on their page to click. Add the page's `builder_url`
|
|
1835
|
-
as the "open in builder" link
|
|
3667
|
+
as the "open in builder" link and its `frontend_url` as the "view live" link,
|
|
3668
|
+
whenever the response carries them — every finding page carries both today
|
|
3669
|
+
(previously only the nine element-located checks carried `builder_url`, and
|
|
3670
|
+
only `non-pretty-url` carried `frontend_url`). A theme/archive/internal
|
|
3671
|
+
template with no public route or no resolvable admin base legitimately omits
|
|
3672
|
+
one or both — that is not a bug, and it is not the agent's job to look the
|
|
3673
|
+
URL up separately via pages_list/pages_resolve when it is missing here.
|
|
1836
3674
|
6. **State what did and did not run.** On a site sweep, say the site-wide checks
|
|
1837
3675
|
ran (`duplicate-layout`, `title-collision` compare pages against each other).
|
|
1838
3676
|
On a single-page audit, say they did not — reuse the response's own `note`
|
|
@@ -1847,7 +3685,7 @@ shape every time, so two audits of two sites read the same way:
|
|
|
1847
3685
|
first (every field of `alt_paths` / `anchors` / … also rides on the locator
|
|
1848
3686
|
for the same hit, so nothing is lost), then the locator detail — halved
|
|
1849
3687
|
before it is dropped — then the remaining raw evidence, then whole evidence
|
|
1850
|
-
pages (`
|
|
3688
|
+
pages (`finding_pages_truncated`), so page ids and labels outlive the prose. Both
|
|
1851
3689
|
transports shed in exactly that order; it is pinned by a contract test that
|
|
1852
3690
|
derives the order from each implementation. Say so, and name the two ways
|
|
1853
3691
|
back: raise `max_chars` (it bounds both legs on this tool), or
|
|
@@ -1859,9 +3697,10 @@ shape every time, so two audits of two sites read the same way:
|
|
|
1859
3697
|
even for that floor, the plugin-served transport refuses with
|
|
1860
3698
|
`max_chars_too_small` and tells you the minimum to ask for — it never returns
|
|
1861
3699
|
a half-written payload.
|
|
1862
|
-
8. **Repeat the scope boundary.** Close with the response's `disclaimer`
|
|
1863
|
-
|
|
1864
|
-
nobody mistakes a clean audit for a clean SEO
|
|
3700
|
+
8. **Repeat the scope boundary.** Close with the response's `disclaimer`
|
|
3701
|
+
verbatim, INCLUDING its own statement that the list is not exhaustive, so
|
|
3702
|
+
nobody mistakes a clean audit for a clean SEO setup. On a search-visibility
|
|
3703
|
+
question, check indexability first — see the pitfall above.
|
|
1865
3704
|
|
|
1866
3705
|
Beyond that the prose is yours: group, summarise and prioritise however serves
|
|
1867
3706
|
the user. Rules 2, 4, 5 and 6 are the parts that must not vary.
|
|
@@ -1878,7 +3717,7 @@ WARNING · structure · multiple-h1 — page renders 2 h1s [open in bui
|
|
|
1878
3717
|
• Willkommen bei SunWest (Hero ▸ Headline) · headline
|
|
1879
3718
|
→ keep this one as the page's h1
|
|
1880
3719
|
• Unsere Standorte (Karten ▸ Panel "Standorte") · panel-title · "Unsere Standorte"
|
|
1881
|
-
→ Fix: element_update_settings({
|
|
3720
|
+
→ Fix: element_update_settings({ layout_id: "page:4816",
|
|
1882
3721
|
element_path: "/children/1/children/0", props: { title_element: "h2" } })
|
|
1883
3722
|
Why: exactly one h1 anchors the page's topic; several dilute it.
|
|
1884
3723
|
|
|
@@ -1887,7 +3726,9 @@ WARNING · content · images-missing-alt — 1 image without alt text
|
|
|
1887
3726
|
→ Fix: element_update_settings({ …, props: { image_alt: "<what it shows>" } })
|
|
1888
3727
|
Why: an empty alt makes the image invisible to screen readers and image search.
|
|
1889
3728
|
|
|
1890
|
-
|
|
3729
|
+
Not covered here, and not an exhaustive list: your CMS or SEO plugin owns meta
|
|
3730
|
+
title/description/OG, and whether search engines may index the page at all —
|
|
3731
|
+
check that with yootheme_builder_fetch_public_url({url, expect:"noindex"}).
|
|
1891
3732
|
```
|
|
1892
3733
|
|
|
1893
3734
|
**Worked example (tool-call snippet):**
|
|
@@ -1899,62 +3740,80 @@ yootheme_builder_advanced_read({
|
|
|
1899
3740
|
arguments: { checks: ["seo", "structure"] }
|
|
1900
3741
|
})
|
|
1901
3742
|
// A finding: { code: "missing-h1", category: "structure", severity: "warning",
|
|
1902
|
-
//
|
|
3743
|
+
// affected_pages: 3, pages: [{ id: "page:42", label: "About" }, ...],
|
|
1903
3744
|
// fix_hint: { tool: "element_update_settings",
|
|
1904
3745
|
// args_template: { props: { title_element: "h1" } }, note: "..." } }
|
|
1905
3746
|
|
|
1906
3747
|
// Step 3. Drill into the flagged page.
|
|
1907
|
-
yootheme_builder_template_summary({
|
|
3748
|
+
yootheme_builder_template_summary({ layout_id: "page:42" })
|
|
1908
3749
|
// heading_levels: { h1: 0, h2: 2, h3: 5, div: 1 }, images_missing_alt: 2
|
|
1909
3750
|
|
|
1910
3751
|
// Step 4. Fix that ONE page via the write tool the fix_hint named.
|
|
1911
3752
|
yootheme_builder_element_update_settings({
|
|
1912
|
-
|
|
3753
|
+
layout_id: "page:42",
|
|
1913
3754
|
element_path: "/children/0/children/0",
|
|
1914
3755
|
props: { title_element: "h1" },
|
|
1915
3756
|
etag: "abc123"
|
|
1916
3757
|
})
|
|
1917
3758
|
|
|
1918
3759
|
// Step 5. Republish; quick_check re-confirms this page is clean.
|
|
1919
|
-
yootheme_builder_page_publish({
|
|
3760
|
+
yootheme_builder_page_publish({ layout_id: "page:42", etag: "def456" })
|
|
1920
3761
|
// Response: { ..., quick_check: { clean: true } }
|
|
1921
3762
|
```
|
|
1922
3763
|
|
|
1923
3764
|
**Success criterion:** A re-run of `pages_audit` (or the `quick_check`
|
|
1924
3765
|
block on the next publish) no longer lists the fixed finding for that
|
|
1925
|
-
page, and the response's `summary.
|
|
1926
|
-
down.
|
|
3766
|
+
page, and the response's `summary.critical_finding_kinds` /
|
|
3767
|
+
`warning_finding_kinds` / `info_finding_kinds` totals go down. Those three
|
|
3768
|
+
count finding KINDS, not pages — `summary.affected_pages_summed_across_findings`
|
|
3769
|
+
is the page-level tally beside them.
|
|
1927
3770
|
|
|
1928
3771
|
---
|
|
1929
3772
|
|
|
1930
3773
|
## When something doesn't fit one of these 6 workflows
|
|
1931
3774
|
|
|
1932
3775
|
- **Move an element** (intra-template reorder/reparent): use
|
|
1933
|
-
`yootheme_builder_element_move({
|
|
3776
|
+
`yootheme_builder_element_move({ layout_id, element_path,
|
|
1934
3777
|
to_parent_path, to_index, etag })`. Reorders or reparents without
|
|
1935
3778
|
re-creating.
|
|
1936
3779
|
- **Delete an element**: use `yootheme_builder_element_delete({
|
|
1937
|
-
|
|
3780
|
+
layout_id, element_path, etag, confirm: true })`.
|
|
1938
3781
|
Elicitation-aware. Confirms via the AI client prompt before
|
|
1939
3782
|
destroying state when `confirm` is omitted. On hosts without
|
|
1940
3783
|
elicitation, it returns a preview-with-confirm-required response;
|
|
1941
3784
|
call again with `confirm: true`.
|
|
1942
3785
|
- **Unbind a source**: call through the gateway:
|
|
1943
|
-
`yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", arguments: {
|
|
3786
|
+
`yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", arguments: { layout_id, element_path, etag, confirm: true } })`.
|
|
1944
3787
|
Same elicitation flow as delete.
|
|
1945
3788
|
- **Orient in a large template before editing** (cheapest first read):
|
|
1946
|
-
`yootheme_builder_page_get_layout({
|
|
3789
|
+
`yootheme_builder_page_get_layout({ layout_id, mode: "outline" })`
|
|
1947
3790
|
returns a token-efficient INDENTED text tree — one line per node,
|
|
1948
|
-
`label ▸ type (bound: …) [sN]`.
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
3791
|
+
`label ▸ type (bound: …) [sN]`. **The handle `[sN]` is a PER-READ
|
|
3792
|
+
(state-scoped) reading alias only, never a write address — it is
|
|
3793
|
+
stable only at the etag you just read, and re-reads after ANY
|
|
3794
|
+
mutation (clone/move/delete/bind) get fresh handles.** The structured
|
|
3795
|
+
leg carries a SUMMARY (which sections are named, how many nodes each
|
|
3796
|
+
holds, the etag), not a path per node — for the actual write address
|
|
3797
|
+
(a RELATIVE `element_path`) call `yootheme_builder_element_list`,
|
|
3798
|
+
scoped to one named section with `root_path` from the outline's
|
|
3799
|
+
`named_sections` if you already know which chunk you want. For
|
|
3800
|
+
binding-focused work prefer `mode:"bindings_only"` instead — and read it as
|
|
3801
|
+
an inventory that checks exactly ONE thing. It compares every binding's
|
|
3802
|
+
SOURCE against the sources this site registers: a source that is gone gets
|
|
3803
|
+
`unresolved: true` on that binding and is named once in
|
|
3804
|
+
`unresolved_sources`, with an `unresolved_sources_note` beside it saying what
|
|
3805
|
+
to repair. That list is present but EMPTY when the comparison ran and found
|
|
3806
|
+
nothing, and is replaced
|
|
3807
|
+
by `unresolved_sources_not_checked` when the registered-source list could not
|
|
3808
|
+
be read at all — in that state an unmarked binding means nothing, so do not
|
|
3809
|
+
read it as a clean bill. What it does NOT check is the field VALUES inside a
|
|
3810
|
+
source that does resolve: a `props` value naming a field that does not exist
|
|
3811
|
+
looks exactly like one that does and will render blank. `element_get_binding`
|
|
3812
|
+
names those dead VALUES on one element; `pages_audit`'s `dangling-source`
|
|
3813
|
+
finding reports the same dead sources across the whole site, with paths.
|
|
1955
3814
|
- **Flat schema inspection** (e.g. enumerate every element path +
|
|
1956
|
-
type without fetching the whole nested tree): call
|
|
1957
|
-
|
|
3815
|
+
type without fetching the whole nested tree): call the L1 tool directly:
|
|
3816
|
+
`yootheme_builder_element_list({ layout_id })`.
|
|
1958
3817
|
- **Etag-only fetch** (e.g. polling for concurrent edits): use
|
|
1959
3818
|
`yootheme_builder_get_etag()` (takes no arguments) is cheaper than
|
|
1960
3819
|
fetching the full layout.
|
|
@@ -1965,13 +3824,26 @@ down.
|
|
|
1965
3824
|
`frontend_url_template` (with placeholders the user fills in).
|
|
1966
3825
|
- **Find out which site / install you are connected to**: call
|
|
1967
3826
|
`yootheme_builder_health` (Bearer-authenticated payload includes
|
|
1968
|
-
`site_url` + `home_url`) or `yootheme_builder_diagnose
|
|
3827
|
+
`site_url` + `home_url`), or `yootheme_builder_diagnose` for `site_url`
|
|
3828
|
+
alone. `active_site.site_id`, which rides on every answer, names the registry
|
|
3829
|
+
entry your own config picked — a different question from which install the
|
|
3830
|
+
plugin believes it is.
|
|
3831
|
+
- **Find WHERE `implode` directives sit on the site** (rows rendering as one
|
|
3832
|
+
comma-joined string, and you do not know which page): call through the
|
|
3833
|
+
read gateway
|
|
3834
|
+
`yootheme_builder_advanced_read({ tool: "yootheme_builder_layout_query", arguments: { has_implode_directives: true, max_paths: 50 } })`
|
|
3835
|
+
— no `layout_id`, so the sweep covers every page layout, and `paths[]`
|
|
3836
|
+
gives you the `layout_id` + `path` pairs to act on. The detection is the
|
|
3837
|
+
same one `inspect_multi_items_binding` reports as `has_implode_directives`,
|
|
3838
|
+
so opening a hit confirms it. Every count in that answer describes the
|
|
3839
|
+
matching subset, not the site.
|
|
1969
3840
|
- **Strip legacy `implode` directives** from an element binding (audit-clean
|
|
1970
3841
|
source props that pre-date the wrapper-source refactor): call through the
|
|
1971
3842
|
gateway:
|
|
1972
|
-
`yootheme_builder_advanced({ tool: "yootheme_builder_clean_implode_directives", arguments: {
|
|
3843
|
+
`yootheme_builder_advanced({ tool: "yootheme_builder_clean_implode_directives", arguments: { layout_id, element_path, etag } })`.
|
|
1973
3844
|
Returns the audit log + a fresh ETag; idempotent (`cleaned_count: 0` when
|
|
1974
|
-
there is nothing to remove).
|
|
3845
|
+
there is nothing to remove). It also removes EMPTY `implode` stubs, which
|
|
3846
|
+
the finder above does not report — an empty stub joins nothing.
|
|
1975
3847
|
- **Start from a pre-designed layout or element preset instead of
|
|
1976
3848
|
hand-building** (the user wants a "Home"/"Contact"/"Services" page, or a
|
|
1977
3849
|
ready-made Accordion/Alert preset, not a section built element-by-element):
|
|
@@ -1998,14 +3870,14 @@ layout root, then child 1 of that); a three-level path is
|
|
|
1998
3870
|
into the Nth child (0-based). This is the form `element_list` emits as
|
|
1999
3871
|
`rel_path`, and the form every worked example in this skill uses.
|
|
2000
3872
|
|
|
2001
|
-
- **Fully-qualified:** `/templates/<
|
|
3873
|
+
- **Fully-qualified:** `/templates/<layout_id>/layout/children/0/...` is the
|
|
2002
3874
|
same address with the template prefix. Read/write tools return the
|
|
2003
3875
|
fully-qualified pointer as `path`; you can pass it straight back.
|
|
2004
3876
|
|
|
2005
|
-
**What the tools EMIT:** `element_list`
|
|
3877
|
+
**What the tools EMIT:** `element_list` returns both `path`
|
|
2006
3878
|
(fully-qualified) and `rel_path` (the canonical `/children/0/...` form); every
|
|
2007
3879
|
write-tool response (`element_add`, `element_update_settings`, `element_clone`,
|
|
2008
|
-
`element_move`, `element_bind_source`, `
|
|
3880
|
+
`element_move`, `element_bind_source`, `page_generate_dynamic`, `scaffold_section`) and
|
|
2009
3881
|
`element_get` return the fully-qualified `path` plus the canonical `rel_path`.
|
|
2010
3882
|
Copy either straight back into the next call.
|
|
2011
3883
|
|
|
@@ -2025,6 +3897,58 @@ hand you.
|
|
|
2025
3897
|
layout node itself, which is not an addressable element. Use `""` for
|
|
2026
3898
|
"the top".
|
|
2027
3899
|
|
|
3900
|
+
**What is NOT an element address.** The grammar above is exhaustive: an
|
|
3901
|
+
element is the layout root plus zero or more `/children/<index>` steps. These
|
|
3902
|
+
tools refuse anything else rather than writing into it — `element_add`,
|
|
3903
|
+
`element_delete`, `element_move`, `element_clone`, `element_update_settings`,
|
|
3904
|
+
`element_set_status`, `element_bind_source`, `element_unbind_source`,
|
|
3905
|
+
`element_set_source`, `clean_implode_directives`, `page_generate_dynamic`,
|
|
3906
|
+
`library_insert` (both `element_path` and `sublayout.element_path`) and
|
|
3907
|
+
`library_save_as`. In particular these resolve to real JSON but are **not**
|
|
3908
|
+
elements:
|
|
3909
|
+
|
|
3910
|
+
- `…/children` — the LIST of a node's children, not a child. Drop the
|
|
3911
|
+
trailing `/children` to name the element that should contain the child.
|
|
3912
|
+
- `…/props` — an element's settings. Pass the element's own path; the tool
|
|
3913
|
+
writes into `props` for you. A props map that happens to carry a `type` key
|
|
3914
|
+
(`props: {"type":"primary"}` is an ordinary element prop) is still props.
|
|
3915
|
+
- `…/type` — a scalar member of an element, not the element. Any member other
|
|
3916
|
+
than `children` ends the element grammar; pass the element's own path.
|
|
3917
|
+
- `/templates/<id>` — the template record. Its element tree starts one
|
|
3918
|
+
segment further down, at `/templates/<id>/layout`.
|
|
3919
|
+
- `…/children/007` — a leading-zero or gapped reference-token. Array positions
|
|
3920
|
+
are canonical RFC-6901 §4 indices; anything else names no position.
|
|
3921
|
+
|
|
3922
|
+
The layout root itself (`parent_path: ""`, or `/templates/<id>/layout`) is a
|
|
3923
|
+
valid **parent** for `element_add` / `element_move`, and never a thing to
|
|
3924
|
+
`element_delete`, `element_move` or `element_clone` — deleting it takes the
|
|
3925
|
+
whole page with it. Each refusal names the offending argument and what to
|
|
3926
|
+
pass instead; nothing is changed. (`page_generate_dynamic` is the one exception
|
|
3927
|
+
on the root: it COPIES rather than mutates, so `element_path` may name the
|
|
3928
|
+
whole page.)
|
|
3929
|
+
|
|
3930
|
+
**`children` must be a JSON array.** `pages_create`, `page_save` and
|
|
3931
|
+
`element_add` refuse a `children` sent as an object with named keys — `{"0":…,"5":…}` — with
|
|
3932
|
+
`layout_children_not_a_list`. The Builder walks `children` by position, so a
|
|
3933
|
+
keyed object there renders in insertion order, makes every `/children/<index>`
|
|
3934
|
+
path address a different node than it reads, and can overwrite a sibling on the
|
|
3935
|
+
next append. A `{"0":…,"1":…}` object with contiguous keys is accepted: it
|
|
3936
|
+
re-encodes as a JSON array, which is what lands on disk either way.
|
|
3937
|
+
|
|
3938
|
+
Only the layout root must declare `children`. An empty section, row or
|
|
3939
|
+
column may omit the key — that is how the builder itself stores them
|
|
3940
|
+
(YOOtheme's save-transforms remove an empty `children`). A `children` value
|
|
3941
|
+
that is present must be a JSON array, never an object.
|
|
3942
|
+
|
|
3943
|
+
**Theme templates in `library_insert` / `library_save_as`.** Those two tools
|
|
3944
|
+
resolve their path against the template as stored, and a THEME template
|
|
3945
|
+
(a `pages_list` id with no `page:` / `article:` prefix) stores its tree one
|
|
3946
|
+
segment down. `element_path: ""` still means "the top of this template" and
|
|
3947
|
+
works on both kinds; a deeper path on a theme template starts at
|
|
3948
|
+
`/layout/children/<index>`. Pass a `page:<id>` / `article:<id>` path there and
|
|
3949
|
+
the refusal says so. (The `element_*` tools are unaffected — their `rel_path`
|
|
3950
|
+
is layout-relative for every template kind.)
|
|
3951
|
+
|
|
2028
3952
|
**Reading the index.** In `/children/0/children/2/children/1`: child 0 of the layout,
|
|
2029
3953
|
then child 2 of THAT, then child 1 of THAT. Indices are positional and shift
|
|
2030
3954
|
when siblings are added or removed; re-read after a structural mutation before
|
|
@@ -2032,10 +3956,12 @@ reusing a path (see the ETag handshake below).
|
|
|
2032
3956
|
|
|
2033
3957
|
**Don't hand-build deep paths.** Enumerate with
|
|
2034
3958
|
`yootheme_builder_element_list` (flat `{path, rel_path, element_type}`
|
|
2035
|
-
rows)
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
3959
|
+
rows) — copy `path` from a row there, never count `/children/`
|
|
3960
|
+
segments by eye on a big template. `yootheme_builder_page_get_layout({
|
|
3961
|
+
layout_id, mode: "outline" })` only ORIENTS (an indented
|
|
3962
|
+
`label ▸ type [sN]` tree plus a per-section node-count summary); scope
|
|
3963
|
+
`element_list` to one named chunk with `root_path` from the outline's
|
|
3964
|
+
`named_sections` when you already know which one you want.
|
|
2039
3965
|
|
|
2040
3966
|
## ETag handshake and self-correcting errors
|
|
2041
3967
|
|
|
@@ -2053,9 +3979,12 @@ carrying the real `path`). Copy the `path` from a row — never count
|
|
|
2053
3979
|
carrying `current_etag` — read it and retry.
|
|
2054
3980
|
|
|
2055
3981
|
So a write never leaves you guessing: on a lock error the response hands
|
|
2056
|
-
you the etag to use next. On
|
|
2057
|
-
|
|
2058
|
-
|
|
3982
|
+
you the etag to use next. On a `412` specifically, take that etag as
|
|
3983
|
+
permission to retry only AFTER re-reading — the site-wide hash does not
|
|
3984
|
+
cover per-page layouts, so the conflict may well be your own page under
|
|
3985
|
+
someone else's edit, and the error cannot tell the two apart. On success,
|
|
3986
|
+
`changed_paths` tells you exactly which addresses moved, so you can
|
|
3987
|
+
re-target without re-reading the whole tree.
|
|
2059
3988
|
|
|
2060
3989
|
**Self-correcting path errors.** When a path-addressed read cannot find
|
|
2061
3990
|
the element at the path you sent, the `element_not_found` error does NOT
|
|
@@ -2087,50 +4016,555 @@ to re-read the entire layout after a wrong-path error.
|
|
|
2087
4016
|
> `{given, nearest_path, nearest_path_label, available_paths}` envelope is
|
|
2088
4017
|
> byte-identical across WordPress and Joomla.
|
|
2089
4018
|
|
|
4019
|
+
## Response budgets — how a read tool shortens an oversized answer
|
|
4020
|
+
|
|
4021
|
+
Every high-volume read tool takes `max_chars`. When a response does not fit,
|
|
4022
|
+
it is **shortened by dropping whole entries, never by cutting the JSON**, and
|
|
4023
|
+
it tells you so. Read the verdict before you report a result as complete
|
|
4024
|
+
(one read caps a text FIELD instead — last paragraph, where these flags are
|
|
4025
|
+
absent):
|
|
4026
|
+
|
|
4027
|
+
- `truncated: true` — this response is not the whole answer.
|
|
4028
|
+
- `omitted_count: N` — exactly how many entries were left out.
|
|
4029
|
+
- `next_cursor` — present whenever the resume point is derivable; pass it back
|
|
4030
|
+
as `cursor` to read the entries that were dropped. It is **omitted rather
|
|
4031
|
+
than guessed**: no cursor means the tool could not derive one, not that
|
|
4032
|
+
nothing is missing. Use `omitted_count` in that case. A cursor that could not
|
|
4033
|
+
be rewound past the dropped entries is **removed, never left stale**, so a
|
|
4034
|
+
`next_cursor` you receive is always safe to follow.
|
|
4035
|
+
- `omitted_keys: [...]` — on a single-object response, which fields were shed
|
|
4036
|
+
(the small identifying ones like `layout_id` / `etag` are kept last).
|
|
4037
|
+
- `retry_hint` — the concrete lever: a higher `max_chars`, a narrower
|
|
4038
|
+
`fields[]` projection, or a smaller `limit`.
|
|
4039
|
+
|
|
4040
|
+
The payload always parses — in this shape the text leg is reduced entry by
|
|
4041
|
+
entry rather than cut mid-string. If you ever receive a response where
|
|
4042
|
+
**neither** leg parses, that is a bug — report it rather than working around it.
|
|
4043
|
+
|
|
4044
|
+
Here both legs carry the **same shortened payload**: reading `structuredContent`
|
|
4045
|
+
gives you exactly what the text leg already said, no more, so it is not a way
|
|
4046
|
+
around the cap. Decide whether an answer is whole from `truncated` /
|
|
4047
|
+
`omitted_count`, never from which leg you happened to read — and **absence of
|
|
4048
|
+
`truncated` means the answer is complete**, so do not hedge a full response with
|
|
4049
|
+
"there may be more".
|
|
4050
|
+
|
|
4051
|
+
**`page_get_layout(mode:"outline")` shortens differently, and none of the three
|
|
4052
|
+
rules above holds for it.** The cap bounds ONLY the outline text (both legs
|
|
4053
|
+
carry that same string); the summary beside it in `structuredContent` —
|
|
4054
|
+
`named_sections`, `total_elements`, `etag` — is never shortened. So here a
|
|
4055
|
+
bounded text leg DOES sit beside a complete structured one, the text is
|
|
4056
|
+
cut mid-string, not entry by entry, and the cut is announced by a trailing
|
|
4057
|
+
`--- TRUNCATED (N chars total, showing first M) ---` line, with **no
|
|
4058
|
+
`truncated` field and no `omitted_count`**. Read that marker; a missing flag
|
|
4059
|
+
means nothing here.
|
|
4060
|
+
|
|
4061
|
+
Two levers beat raising `max_chars`, because they reduce what is produced
|
|
4062
|
+
rather than what survives: ask for fewer fields (`fields[]`) and fewer rows
|
|
4063
|
+
(`limit` + `cursor`). Reach for `max_chars` last.
|
|
4064
|
+
|
|
4065
|
+
Every default is derived from measured response sizes, not chosen, and the same
|
|
4066
|
+
default applies on both transports — the proxy and the plugin's own `/mcp` are
|
|
4067
|
+
two paths to one tool (see `docs/mcp-response-budgets.md`). A derived class is
|
|
4068
|
+
applied **raise-only**: measurement may lift a default, never lower it, so no
|
|
4069
|
+
upgrade silently returns less than the version before it.
|
|
4070
|
+
|
|
4071
|
+
### Ask for the fields you need — `element_type_get_schema`
|
|
4072
|
+
|
|
4073
|
+
A big element type is the sharpest case: `grid` declares **154** field
|
|
4074
|
+
descriptors. Do not read all of them and do not raise the cap — **name what you
|
|
4075
|
+
want**:
|
|
4076
|
+
|
|
4077
|
+
- `field_names: ["image_expand","grid_masonry"]` — exact names, case-insensitive.
|
|
4078
|
+
- `name_contains: "height"` — substring of the field's **name OR label**, so it
|
|
4079
|
+
also finds `image_expand`, whose label is "Height".
|
|
4080
|
+
- Both together **UNION** — each argument names what you want; adding one never
|
|
4081
|
+
removes what the other selected.
|
|
4082
|
+
|
|
4083
|
+
A selected descriptor comes back **COMPLETE** — its `enable` condition, `enum`
|
|
4084
|
+
and `default` are all there, which is exactly what a truncated read destroyed.
|
|
4085
|
+
Every response carries `field_count` (what the type declares) and
|
|
4086
|
+
`returned_count` (what you received), so a narrowed answer is never silent and
|
|
4087
|
+
you always know the size of what you did not ask for. A pattern that matches
|
|
4088
|
+
nothing returns an **empty list plus the counts, not an error**.
|
|
4089
|
+
|
|
4090
|
+
`field_names`/`name_contains` pick WHICH descriptors; `fields[]` picks the
|
|
4091
|
+
sub-keys of each one. They compose — selection runs first, so
|
|
4092
|
+
`name_contains:"height", fields:["name","enable"]` is a legitimate call.
|
|
4093
|
+
|
|
4094
|
+
### Read a field's MEANING, not just its `label` — `text` and `description`
|
|
4095
|
+
|
|
4096
|
+
A descriptor carries YOOtheme's own two meaning-bearing keys, and on a
|
|
4097
|
+
**checkbox** they are not optional reading:
|
|
4098
|
+
|
|
4099
|
+
- **A checkbox's value is a boolean (`true`/`false`); its MEANING is in `text`,
|
|
4100
|
+
because `label` on a checkbox is the GROUP heading, not the option.** `grid`'s
|
|
4101
|
+
equal-height option is
|
|
4102
|
+
`{"name":"image_expand","type":"checkbox","label":"Height","text":"Expand image","enable":"!grid_masonry"}`.
|
|
4103
|
+
Read `label` alone and it says "Height: true/false" — which is not what the
|
|
4104
|
+
option does. The caption rendered next to the box is `text`: "Expand image".
|
|
4105
|
+
Same shape for `image_loading` ("Loading" / "Load image eagerly"), `panel_link`
|
|
4106
|
+
("Link" / "Link panel") and `panel_match` ("Height" / "Match panel heights").
|
|
4107
|
+
- **`description` is the Builder's own tooltip** for the field, e.g.
|
|
4108
|
+
`grid.panel_content_width` → "Set an optional content width which doesn't
|
|
4109
|
+
affect the image if there is just one column."
|
|
4110
|
+
|
|
4111
|
+
Both keys are **absent when YOOtheme declares none**, so a descriptor without
|
|
4112
|
+
`text` means "this field has no caption", never "the caption was dropped".
|
|
4113
|
+
|
|
4114
|
+
### When the field says nothing, the GROUP does — `group` and `groups[]`
|
|
4115
|
+
|
|
4116
|
+
**YOOtheme parks the semantics of a whole class of fields on the enclosing
|
|
4117
|
+
fieldset GROUP, not on the field.** So a descriptor with no `label` and no
|
|
4118
|
+
`description` is not undocumented — you are reading at the wrong level.
|
|
4119
|
+
`group` names the nearest enclosing group (the heading the Builder renders beside
|
|
4120
|
+
the field), and the schema's **root `groups[]`** states that group's own sentence
|
|
4121
|
+
once. Join the two on the group name:
|
|
4122
|
+
|
|
4123
|
+
`{"name":"image_width","type":"number","placeholder":"auto","enable":"show_image || show_video","group":"Width/Height"}`
|
|
4124
|
+
|
|
4125
|
+
`"groups":[{"name":"Width/Height","description":"Setting just one value preserves the original proportions. The image will be resized and cropped automatically, and where possible, high resolution images will be auto-generated."}]`
|
|
4126
|
+
|
|
4127
|
+
Read the field alone and `image_width` looks like a plain number. The rule you
|
|
4128
|
+
actually needed — **set only ONE of width/height and the proportions are kept;
|
|
4129
|
+
set BOTH and the image is CROPPED** — lives on the group shared by `image_width`
|
|
4130
|
+
and `image_height`. Same shape for the `lightbox_image_*` pair.
|
|
4131
|
+
|
|
4132
|
+
- The sentence is the **group's**, never re-labelled as the field's, so a field
|
|
4133
|
+
may carry its own `description` while its group carries another at once —
|
|
4134
|
+
they are two different statements. It sits at the root because it is shared:
|
|
4135
|
+
measured over nine element types, 32 fields carried only 5 distinct sentences,
|
|
4136
|
+
one of them 20 times.
|
|
4137
|
+
- `groups[]` **narrows with your selection** — a `field_names` /`name_contains`
|
|
4138
|
+
read carries only the groups its descriptors name, never the whole type's.
|
|
4139
|
+
- `placeholder` is `attrs.placeholder`, which for several fields IS the
|
|
4140
|
+
documented fallback: `grid.image_width` → `"auto"`, `countdown.label_days` →
|
|
4141
|
+
`"Days"`. An empty value renders the placeholder, it does not render empty.
|
|
4142
|
+
|
|
4143
|
+
### What an enum VALUE does not say — `enum_labels`
|
|
4144
|
+
|
|
4145
|
+
`enum` carries the values you write. `enum_labels` carries the Builder's label for
|
|
4146
|
+
a value **where the value does not already carry it**:
|
|
4147
|
+
|
|
4148
|
+
`{"name":"grid_default","type":"select","label":"Phone Portrait","enum":["1","2","3","4","5","6","auto"],"default":"1","enum_labels":{"1":"1 Column","2":"2 Columns","3":"3 Columns","4":"4 Columns","5":"5 Columns","6":"6 Columns"}}`
|
|
4149
|
+
|
|
4150
|
+
Without it, `["1"…"6"]` is six digits with nothing saying they are **column
|
|
4151
|
+
counts**; `grid_column_gap`'s `"collapse"` is a word with nothing saying the
|
|
4152
|
+
Builder calls it **None**.
|
|
4153
|
+
|
|
4154
|
+
- **A value with no entry is not unlabelled.** Its label is the value itself, up
|
|
4155
|
+
to case and punctuation — `"small"` is "Small", `"arrow-right"` is
|
|
4156
|
+
"Arrow Right". Only genuine renamings are listed, which is why the key is
|
|
4157
|
+
affordable: measured over nine element types, 611 of 2043 label/value pairs
|
|
4158
|
+
(29.9 %) are genuine renamings and the other 1432 are silent.
|
|
4159
|
+
- **`""` is a real choice, not a gap.** YOOtheme uses the empty string as a
|
|
4160
|
+
meaningful selection — `panel.video_autoplay` declares `'On' => ''`,
|
|
4161
|
+
`switcher_item.item_element` declares `'div' => ''` — and since an empty value
|
|
4162
|
+
carries no characters at all, its label is always in `enum_labels`
|
|
4163
|
+
(`{"":"On"}`). So `""` in an `enum` is a selection to make.
|
|
4164
|
+
- **A missing entry can also mean "not decidable".** When two options collapse
|
|
4165
|
+
onto the same value — `'On' => ''` beside `'No' => false`, because `false`
|
|
4166
|
+
stringifies to `""` — neither label is emitted for that value rather than one
|
|
4167
|
+
guessed. The same value in a DIFFERENT field may legitimately mean something
|
|
4168
|
+
else: in `grid`, `""` carries twelve different labels across its fields, and
|
|
4169
|
+
`"1-2"` is "50%" on one field and "Halves" on another. Read `enum_labels` per
|
|
4170
|
+
field; never carry one field's label to another.
|
|
4171
|
+
|
|
4172
|
+
### A prop that is declared and does nothing — `no_effect`
|
|
4173
|
+
|
|
4174
|
+
A few props are declared in YOOtheme's config, ship a label and a tooltip, and
|
|
4175
|
+
are **read by no consumer**. For those the descriptor drops `text`/`description`
|
|
4176
|
+
— forwarding a promise nothing keeps is worse than silence — and carries
|
|
4177
|
+
`no_effect` with what was actually read instead:
|
|
4178
|
+
|
|
4179
|
+
`{"name":"lightbox_image_orientation","type":"checkbox","label":"Image Orientation","no_effect":"No effect in YOOtheme Pro 5.0.37: declared in grid/element.php and gallery/element.php only, read by no template. Writing it changes nothing.","enable":"lightbox","group":"Lightbox"}`
|
|
4180
|
+
|
|
4181
|
+
**A descriptor with `no_effect` is not a prop to set** — the write succeeds and
|
|
4182
|
+
nothing changes. The other one today is `panel-slider.show_hover_video`, dead
|
|
4183
|
+
because `panel-slider_item` lists `hover_image` twice and `hover_video` never.
|
|
4184
|
+
The prop stays writable on purpose: a stored layout may already carry the key,
|
|
4185
|
+
and rejecting it would break a round-trip. `enable` and `label` survive too —
|
|
4186
|
+
a Builder-UI condition and a UI name are not behaviour claims.
|
|
4187
|
+
|
|
4188
|
+
### Which build a claim was read on — `claims_verified_on` and `no_effect_unverified`
|
|
4189
|
+
|
|
4190
|
+
That evidence was read in **one** YOOtheme tree. A site running a different
|
|
4191
|
+
build is a site nobody has read, so the response says which is which:
|
|
4192
|
+
|
|
4193
|
+
`"claims_verified_on":{"build":"5.0.37","here":"4.5.33","status":"differs","keys":["rule_notes","no_effect_unverified"]}`
|
|
4194
|
+
|
|
4195
|
+
- **`status:"match"`** — the claims were read on the build you are running.
|
|
4196
|
+
Take them as fact.
|
|
4197
|
+
- **`status:"differs"`** — read on `build`, not re-checked on `here`.
|
|
4198
|
+
- **`status:"unknown"`** — the site's YOOtheme version could not be resolved
|
|
4199
|
+
(`here` is `null`). It never means "probably the same".
|
|
4200
|
+
|
|
4201
|
+
`here` is resolved per host — from YOOtheme's runtime on WordPress, from the
|
|
4202
|
+
installed `yootheme` template manifest on Joomla — and on both it is the SAME
|
|
4203
|
+
value `yootheme_builder_health` reports as `yootheme_version`. So the two must
|
|
4204
|
+
agree: **if health names a build while this record says `unknown`, that is a
|
|
4205
|
+
resolver bug, not a fact about your site.** Check health first.
|
|
4206
|
+
|
|
4207
|
+
Off a `match`, `no_effect` **is not emitted at all**. The finding ships under
|
|
4208
|
+
**`no_effect_unverified`** instead, naming both builds and ending in `UNKNOWN`,
|
|
4209
|
+
and `text`/`description` **stay** — those are YOOtheme's own words read from
|
|
4210
|
+
*your* build's live registry, so they are true for you, and only a verified
|
|
4211
|
+
deadness earns their removal. Every `rule_notes` entry is likewise prefixed
|
|
4212
|
+
`[Read in YOOtheme Pro <build>. … — unverified here.]`.
|
|
4213
|
+
|
|
4214
|
+
**`differs` does not mean the claim is false on your build** — it means nobody
|
|
4215
|
+
looked. Treat `no_effect_unverified` as a strong reason to check before relying
|
|
4216
|
+
on the prop, not as permission to assume it works.
|
|
4217
|
+
|
|
4218
|
+
**The record scopes exactly the keys it lists in `keys` — nothing else in the
|
|
4219
|
+
response.** It is emitted when, and only when, that list is non-empty, so scope
|
|
4220
|
+
and trigger are one thing and cannot drift apart. What can appear there:
|
|
4221
|
+
|
|
4222
|
+
| Key | Why it is build-scoped |
|
|
4223
|
+
|---|---|
|
|
4224
|
+
| `renders_only_if`, `blanked_by_parent`, `media_autoswap` | render gates read in one tree |
|
|
4225
|
+
| `rule_notes` | the shared render-rule texts, all read in that same tree |
|
|
4226
|
+
| `no_effect` / `no_effect_unverified` | whichever of the two actually shipped |
|
|
4227
|
+
| `value_hint` | **only** when the hint is one of the render-template reads: a value-FORMAT trap (`countdown.date`, `map_item.location`, `map.width`/`height`, `popover_item.position_x`/`_y`, `table.table_order`) or `image_expand` |
|
|
4228
|
+
|
|
4229
|
+
Everything else is **outside** it, whatever its own provenance — including
|
|
4230
|
+
`binding_contract`, whose `child_type` comes from a container/item table read on
|
|
4231
|
+
a *different* YOOtheme build, and `groups`, `enum_labels`, `semantic_role` and
|
|
4232
|
+
the descriptors themselves, which come from your own build's live registry.
|
|
4233
|
+
`placeholder_fallback` is outside for that second reason: it is derived from your
|
|
4234
|
+
own config, so it is true here by construction.
|
|
4235
|
+
|
|
4236
|
+
A `value_hint` that is **not** named in `keys` carries no single-build scope — the
|
|
4237
|
+
image/icon shape hints, the grid-layout string and the type scale state a
|
|
4238
|
+
convention rather than a render reading, and `title_element`'s per-type defaults
|
|
4239
|
+
were verified against **both** the 4.5.33 and the YT5 element sources. Do not read
|
|
4240
|
+
`keys` as "these are the only claims in the response"; read it as "these are the
|
|
4241
|
+
claims this record vouches for."
|
|
4242
|
+
|
|
4243
|
+
### Read what no field text can say — `value_hint`
|
|
4244
|
+
|
|
4245
|
+
`text` and `description` are YOOtheme's own words. `value_hint` is ours, and it
|
|
4246
|
+
carries only what lives in the element's **render template** — a rule no field
|
|
4247
|
+
definition can state. The equal-height option is the sharpest case, and it is
|
|
4248
|
+
**per element**:
|
|
4249
|
+
|
|
4250
|
+
- **A `grid` is ALREADY column-matched with no option set** — its container always
|
|
4251
|
+
renders `uk-grid-match` (sole exception: `grid_masonry`). Switching
|
|
4252
|
+
`image_expand` on to equalise the **columns** solves a solved problem: what it
|
|
4253
|
+
expands is the **IMAGE**. The media element takes `flex:1`, and because every
|
|
4254
|
+
`img` renders with `object-fit:cover`, the added height **crops** the picture.
|
|
4255
|
+
- **The order matters and the wrong order fails SILENTLY.** `image_expand` needs
|
|
4256
|
+
its gate set FIRST: `panel_match` on `panel-slider`, `height_expand` on
|
|
4257
|
+
`panel`, `grid_masonry` OFF on `grid` and `gallery`. Otherwise the element's
|
|
4258
|
+
template **blanks the value at RENDER** — the write succeeds and the value stays
|
|
4259
|
+
stored, so there is no error anywhere. **A field's `enable` condition does not
|
|
4260
|
+
protect you**: it is a Builder-UI condition only — no render path reads it, so it
|
|
4261
|
+
never guards an API write. Read the field's own `value_hint` for that element's exact condition
|
|
4262
|
+
(most gates have an `image_align: left`/`right` escape; `gallery` has none).
|
|
4263
|
+
- On the two sliders the render can also **FORCE it on**. With `slider_width`
|
|
4264
|
+
unset, `panel-slider` forces `panel_match` and `image_expand` to true, and
|
|
4265
|
+
`overlay-slider` forces `image_expand` to true — as it also does whenever
|
|
4266
|
+
`height_expand`, `slider_min_height` or `height_viewport` is set. There an
|
|
4267
|
+
`image_expand: false` write is accepted and stays stored, then replaced by true
|
|
4268
|
+
at **RENDER**: on those settings the option cannot be switched off at all.
|
|
4269
|
+
- **The fixed-size alternative:** set **BOTH** `image_width` and `image_height` —
|
|
4270
|
+
both numeric crops server-side to exactly that box, one alone only resizes
|
|
4271
|
+
proportionally.
|
|
4272
|
+
|
|
4273
|
+
`name_contains: "expand"` returns that one descriptor with the whole rule set.
|
|
4274
|
+
|
|
4275
|
+
### Will this element render at all? — `renders_only_if`, `placeholder_fallback` and `blanked_by_parent`
|
|
4276
|
+
|
|
4277
|
+
The schema root carries three arrays that decide whether a write is visible **at
|
|
4278
|
+
all**. They are element-level, so they are present on **every** read of that type
|
|
4279
|
+
— a `field_names` selection does not narrow them away.
|
|
4280
|
+
|
|
4281
|
+
**`renders_only_if` — an element with PRESENT-but-EMPTY required props is DELETED at render.**
|
|
4282
|
+
Not hidden, not empty: removed from the tree, with no error anywhere. It is a
|
|
4283
|
+
list of groups, and **ALL groups need at least one non-empty member**:
|
|
4284
|
+
|
|
4285
|
+
`"renders_only_if":[["title","meta","content","image","video","icon"]]` ← `grid_item`
|
|
4286
|
+
|
|
4287
|
+
`"renders_only_if":[["title"],["content","image","link"]]` ← `accordion_item`, which needs a title **and** one of the three
|
|
4288
|
+
|
|
4289
|
+
> **Read `placeholder_fallback` before concluding "nothing renders".** For props
|
|
4290
|
+
> you simply never wrote, the answer on **27 of the 28 gated element types** is
|
|
4291
|
+
> not deletion — see the next subsection. `renders_only_if` alone over-states it.
|
|
4292
|
+
|
|
4293
|
+
- **`link` is almost never a keeper.** It is absent from the gate of `grid_item`,
|
|
4294
|
+
`gallery_item`, `list_item`, `table_item`, `description_list_item` and `panel`,
|
|
4295
|
+
so a card carrying **only a link plus link text renders nothing at all**.
|
|
4296
|
+
`social_item` is the one element where a bare `link` is the whole gate — which
|
|
4297
|
+
is why this is a per-element read, never a blanket rule.
|
|
4298
|
+
- **The emptied parent goes too.** A container whose children all dropped is
|
|
4299
|
+
itself dropped, cascading column → row → section. So one unsatisfied gate on a
|
|
4300
|
+
repeated item can blank a whole section.
|
|
4301
|
+
- A member may be a `&&`-joined conjunction, and a `parent.` prefix addresses the
|
|
4302
|
+
**container's** prop: `switcher_item`'s second group lists
|
|
4303
|
+
`"title&&parent.show_title"`, i.e. with the container's `show_title` off a title
|
|
4304
|
+
alone does not satisfy it — meta, content or image is then required.
|
|
4305
|
+
- Media-only elements are the mirror image: `overlay`, `overlay-slider_item`,
|
|
4306
|
+
`slideshow_item` and `gallery_item` need an image or video and drop a text-only
|
|
4307
|
+
item.
|
|
4308
|
+
|
|
4309
|
+
**`placeholder_fallback` — an UNSET prop does not delete the element; it publishes
|
|
4310
|
+
YOOtheme's placeholder to the live page.** This is the difference between
|
|
4311
|
+
*absent* and *present-but-empty*, and absent is the normal shape of an MCP write:
|
|
4312
|
+
|
|
4313
|
+
`"placeholder_fallback":{"suppressed_by":["title","meta","content","image","video","icon","hover_image","hover_video"],"fills":["title","content"]}` ← `grid_item`
|
|
4314
|
+
|
|
4315
|
+
- **`fills` is what your visitors see.** YOOtheme merges the element type's own
|
|
4316
|
+
placeholder props *before* the gate is evaluated, on the **front end** as well
|
|
4317
|
+
as in the customizer, so the gate is satisfied by content you never wrote:
|
|
4318
|
+
lorem ipsum, the literal string `"Title"`, `"Headline"`, the icon `"star"`, a
|
|
4319
|
+
demo date, or `assets/images/element-image-placeholder.png`. On `overlay`, for
|
|
4320
|
+
instance, YOOtheme injects **both** its placeholder image **and** the literal
|
|
4321
|
+
text `"Title"` into the published page.
|
|
4322
|
+
- **`suppressed_by` is your off switch.** Write **any** key in that list —
|
|
4323
|
+
including to `""` — and the merge is skipped entirely for the whole element.
|
|
4324
|
+
Only then does `renders_only_if` delete. It is all-or-nothing: one written key
|
|
4325
|
+
suppresses every fill, not just its own.
|
|
4326
|
+
- **So there are three outcomes, not two.** Leave the props unset → placeholder
|
|
4327
|
+
content ships. Write one gate prop with real content → your content ships.
|
|
4328
|
+
Write a gate prop as `""` → the element is deleted. If you want an element
|
|
4329
|
+
gone, delete it; do not rely on leaving props unset.
|
|
4330
|
+
- **`social_item` is the one gated type with no placeholder at all**, so no key
|
|
4331
|
+
is present and an absent `link` really does drop the node. Absence of
|
|
4332
|
+
`placeholder_fallback` therefore means the gate is literal.
|
|
4333
|
+
|
|
4334
|
+
**`blanked_by_parent` — a container's `show_*` flag is NOT CSS visibility, and it
|
|
4335
|
+
is NOT a write either.** Both halves matter, and a reader who takes only one acts
|
|
4336
|
+
wrongly. It **empties the child's prop in the render transform**, so the value is
|
|
4337
|
+
gone from the **rendered HTML** while **the stored layout still holds it,
|
|
4338
|
+
unchanged** — flip the flag back and the render returns. The emptied value then
|
|
4339
|
+
feeds the child's own `renders_only_if`, in the same function, so clearing one
|
|
4340
|
+
flag can delete the **entire item**, not just the field you meant to hide:
|
|
4341
|
+
|
|
4342
|
+
`"blanked_by_parent":{"show_title":["title"],"show_image":["image","icon"],"show_link":["link"], …}`
|
|
4343
|
+
|
|
4344
|
+
- **MEASURED, YOOtheme Pro 5.0.37** — a two-item `grid`, written with
|
|
4345
|
+
`element_update_settings` + `page_publish`, read back with `page_get_layout`
|
|
4346
|
+
and from the published page. With `show_title: false` on the grid: rendered
|
|
4347
|
+
`.el-title` **2 → 0**, neither title string anywhere in the section HTML, and
|
|
4348
|
+
the grid's stored children subtree **byte-identical** before and after (equal
|
|
4349
|
+
sha256, 699 B, `diff` exit 0) — the whole-layout diff was **one added line**,
|
|
4350
|
+
`"show_title": false`, on the GRID node and nothing on any child. Setting it
|
|
4351
|
+
back to `true` restored the byte-identical baseline render. So **re-writing the
|
|
4352
|
+
props is never the fix; flipping the flag back is.**
|
|
4353
|
+
- **The whole item really does go — and takes its container.** In the state where
|
|
4354
|
+
the blanking left only `link` (which is not in `grid_item`'s gate), `.el-item`
|
|
4355
|
+
went **2 → 0** and the `.uk-grid` container with it (**2 → 1**; section HTML
|
|
4356
|
+
2221 → 609 chars) — while the read-back still carried `link` and `link_text` in
|
|
4357
|
+
full. Nothing rendered; nothing lost.
|
|
4358
|
+
- **A source-bound item behaves the same — measured, not assumed.** With
|
|
4359
|
+
`grid_item[0]` bound to a native source and `grid_item[1]` left literal,
|
|
4360
|
+
`show_title: false` blanked **both** (`.el-title` 4 → 0), and the read-back
|
|
4361
|
+
still showed the binding intact (`source.props.title`) on the bound item and
|
|
4362
|
+
the literal string on the static one. For a bound prop the surviving stored
|
|
4363
|
+
value is the **binding**, and it survives exactly as a literal does. Measured
|
|
4364
|
+
on the REST write path and the published page only — the customizer's own
|
|
4365
|
+
tick-and-save path was not measured, and nothing here is claimed about it.
|
|
4366
|
+
- **Intersect the two arrays to see the blast radius.** Where a blanked prop also
|
|
4367
|
+
appears in the gate, the item can vanish — true for nine of the thirteen item
|
|
4368
|
+
types that blank. In `nav_item`, `subnav_item`, `slideshow_item` and `map_item`
|
|
4369
|
+
it cannot: their gate is `content` / `content` / `image`+`video` / `location`,
|
|
4370
|
+
which no `show_*` blanks.
|
|
4371
|
+
- **`show_image` also wipes `icon`** on `grid_item`, `panel-slider_item`,
|
|
4372
|
+
`list_item`, `nav_item` and `subnav_item` — the array shows it, so read the
|
|
4373
|
+
values rather than assuming one flag clears one prop.
|
|
4374
|
+
- **Some flags blank from the TEMPLATE, not the transform** — `map_item`'s five
|
|
4375
|
+
and `switcher_item`'s `show_title`, which is why `switcher_item` carries seven
|
|
4376
|
+
flags and not the six its transform loop enumerates. These empty the value for
|
|
4377
|
+
**rendering** but do not feed the element's own gate, so do **not** intersect
|
|
4378
|
+
them with `renders_only_if`: `switcher_item`'s gate reads `title` raw in its
|
|
4379
|
+
first group and consults `show_title` itself in the second
|
|
4380
|
+
(`"title&&parent.show_title"`), and `map_item`'s gate is `location`, which no
|
|
4381
|
+
flag touches.
|
|
4382
|
+
- `table_item`, `social_item` and `button_item` have no blanking at all. They
|
|
4383
|
+
still have gates.
|
|
4384
|
+
- On `description_list_item`, `link` renders **only** as a wrapper around
|
|
4385
|
+
`content`, so losing `content` loses the link too — by a different route than
|
|
4386
|
+
the flag.
|
|
4387
|
+
|
|
4388
|
+
### `""` means INHERIT, not off — `empty_means_inherit`
|
|
4389
|
+
|
|
4390
|
+
A descriptor with `"empty_means_inherit":true` merges over its **container's**
|
|
4391
|
+
value. Because the empty string is falsy, writing it — which is what the
|
|
4392
|
+
Builder's **"None"** option stores — **re-inherits the parent** instead of
|
|
4393
|
+
switching the feature off. Item overrides are therefore **add-only**: a single
|
|
4394
|
+
item cannot be forced back to plain inside a styled container. **To make one item
|
|
4395
|
+
plain you must restyle the container, not the item.**
|
|
4396
|
+
|
|
4397
|
+
It is on **43 fields across 12 element types**, so read the flag rather than
|
|
4398
|
+
guessing the family: the styling props (`panel_style`, `*_text_color`, `media_*`)
|
|
4399
|
+
on `grid_item`, `gallery_item`, `panel-slider_item`, `overlay-slider_item` and
|
|
4400
|
+
`slideshow_item`, but also `link_text` on **ten** item types and
|
|
4401
|
+
`link_aria_label` on **eight** — **eleven** types carry one or the other,
|
|
4402
|
+
including `accordion_item`, `switcher_item` and `table_item` (`link_text` only)
|
|
4403
|
+
and `social_item` (`link_aria_label` only) — plus `list_item`'s `icon` and
|
|
4404
|
+
`image_svg_color` and `popover_item`'s `drop_position`. An empty `link_text` on a
|
|
4405
|
+
`table_item` shows the TABLE's link text, not no text.
|
|
4406
|
+
|
|
4407
|
+
### Which media actually renders — `media_autoswap`
|
|
4408
|
+
|
|
4409
|
+
Image/video collisions are resolved **per element, and YOOtheme is not
|
|
4410
|
+
consistent about it**. Read the key; there is no default to assume.
|
|
4411
|
+
|
|
4412
|
+
`"media_autoswap":{"precedence":["video","image","icon"],"sniffed":["image","video","hover_image","hover_video"]}`
|
|
4413
|
+
|
|
4414
|
+
- **`precedence`** — the first non-empty prop renders; the losers are simply not
|
|
4415
|
+
drawn. All seven builder media elements (`grid_item`, `panel`,
|
|
4416
|
+
`panel-slider_item`, `gallery_item`, `overlay`, `overlay-slider_item`,
|
|
4417
|
+
`slideshow_item`) are **video-first**, so setting both image and video shows the
|
|
4418
|
+
video.
|
|
4419
|
+
- **`sniffed`** — these props are re-classified by URL sniffing and the value is
|
|
4420
|
+
**MOVED** between `image` and `video`, nulling the other. **The field you wrote
|
|
4421
|
+
may not be the field that renders**: an `.mp4` in `image` ends up in `video`.
|
|
4422
|
+
- **`clears_on_conflict`** — `section` and `column` **invert** the precedence
|
|
4423
|
+
(image-first) **and** are destructive: with an image set, `video` is set to
|
|
4424
|
+
`false`, so anything derived from it downstream sees no video. These two are the
|
|
4425
|
+
only elements that do this.
|
|
4426
|
+
|
|
4427
|
+
### `rules` + `rule_notes` — the rules that need a sentence
|
|
4428
|
+
|
|
4429
|
+
The four keys above are arrays you can evaluate. Nine more rules have no array
|
|
4430
|
+
form, so they ship as prose — **stated once per response and referenced by id**,
|
|
4431
|
+
never repeated per prop:
|
|
4432
|
+
|
|
4433
|
+
```json
|
|
4434
|
+
{ "name": "slideshow",
|
|
4435
|
+
"rules": ["expand_model", "positional", "enable_is_ui_only"],
|
|
4436
|
+
"rule_notes": { "expand_model": "…", "positional": "…", "enable_is_ui_only": "…" },
|
|
4437
|
+
"fields": [ { "name": "slideshow_max_height", "enable": "!height_viewport",
|
|
4438
|
+
"rules": ["slideshow_max_height_vs_render"] } ] }
|
|
4439
|
+
```
|
|
4440
|
+
|
|
4441
|
+
- `rules` on the **element** = rules about the container's own template. They
|
|
4442
|
+
survive a `field_names` / `name_contains` selection, because they are true of
|
|
4443
|
+
the element whichever descriptors you asked for.
|
|
4444
|
+
- `rules` on a **field** = rules about that prop. Their notes are narrowed away
|
|
4445
|
+
when the field is not returned, so a two-descriptor read pays for two.
|
|
4446
|
+
- `rule_notes` resolves every id the response still references, and only those.
|
|
4447
|
+
|
|
4448
|
+
**`enable` and `show` are Builder-UI conditions only — they are not enforced.**
|
|
4449
|
+
In YOOtheme 5.0.37 the word `enable` appears **0×** in the theme's
|
|
4450
|
+
`packages/builder/src`, so no PHP render path reads either key and neither one
|
|
4451
|
+
guards an API write. They are still projected verbatim (they are the best
|
|
4452
|
+
statement of intent available), but they are wrong in **both** directions: a
|
|
4453
|
+
value the UI would refuse to let you edit is still stored and can still take
|
|
4454
|
+
effect, and a value whose `enable` was satisfied can still be dropped or
|
|
4455
|
+
overridden at render. Every prop where the render is known to disagree carries
|
|
4456
|
+
its own rule id — `slideshow_max_height_vs_render`, `thumbnav_ui_locked`,
|
|
4457
|
+
`nav_grid_needs_two`, `overlay_slider_two_positions`,
|
|
4458
|
+
`panel_slider_show_hover_video_dead` — plus the five-way `image_expand` split,
|
|
4459
|
+
which is in that field's `value_hint`.
|
|
4460
|
+
|
|
4461
|
+
That last one is the shape to expect from the rest: on `panel-slider`,
|
|
4462
|
+
`show_hover_video` is a **dead switch**. Its three sibling containers (`grid`,
|
|
4463
|
+
`gallery`, `overlay-slider`) blank the item's `hover_video` when it is off;
|
|
4464
|
+
`panel-slider`'s item lists `hover_image` twice and `hover_video` not at all, so
|
|
4465
|
+
the hover video keeps rendering. Prop, label and default are identical to the
|
|
4466
|
+
three that work — nothing in the descriptor can reveal it. Clear the item's
|
|
4467
|
+
`hover_video` prop instead.
|
|
4468
|
+
|
|
4469
|
+
Four rules to know before you write anything:
|
|
4470
|
+
|
|
4471
|
+
- **Equal height is not one switch** (`expand_model`). A `grid` already matches
|
|
4472
|
+
its column heights with nothing set; a `row` **never** does; a `column` matches
|
|
4473
|
+
only under a specific prop combination, and setting a row height **removes**
|
|
4474
|
+
matching from every column. `image_expand` expands the image, not the layout —
|
|
4475
|
+
the content-expand lever is a `*_margin_auto` checkbox.
|
|
4476
|
+
- **A link toggle silently strips HTML** (`link_striptags`). With `panel_link` /
|
|
4477
|
+
`overlay_link` / `card_link` on and a `link` present, YOOtheme's own renderer
|
|
4478
|
+
runs `striptags()` over `title`, `meta` **and** `content`. The stored value
|
|
4479
|
+
keeps the markup, so the loss is invisible in a read-back. This is YOOtheme's
|
|
4480
|
+
renderer, not this server's, and it cannot be switched off.
|
|
4481
|
+
**`list_item` is worse and gets its own id** (`link_striptags_ungated`): there
|
|
4482
|
+
is **no checkbox** to turn off — as soon as the item has a `link`, `content` is
|
|
4483
|
+
stripped, on both the with-image and the without-image render branch. To keep
|
|
4484
|
+
markup there, leave `link` empty or move the markup to an element that is not
|
|
4485
|
+
link-wrapped.
|
|
4486
|
+
- **A lightbox or modal drops the four link attributes**
|
|
4487
|
+
(`modal_drops_link_attrs`). `link_target`, `link_download`,
|
|
4488
|
+
`link_rel_nofollow` and `link_rel_noreferrer` are emitted only in the
|
|
4489
|
+
plain-link render branch. With `lightbox` on and a link present, an earlier
|
|
4490
|
+
branch wins and all four vanish — on `image`, and on `grid` and `gallery`
|
|
4491
|
+
(whose item templates have the identical shape; the four props are declared on
|
|
4492
|
+
the container). `button_item` does the same for a `dialog` with
|
|
4493
|
+
`dialog_layout` `modal`/`offcanvas` and no `link`. Open-in-new-tab, download
|
|
4494
|
+
and rel hints cannot be combined with a lightbox.
|
|
4495
|
+
- **An emptied container disappears with it** (`collapse_cascade`). A container
|
|
4496
|
+
that HAD children and ends up with none is removed — the mark is keyed on the
|
|
4497
|
+
element type's own `container` flag, and **21 element types** declare it in
|
|
4498
|
+
5.0.37, so `grid`, `gallery`, `slideshow` and the rest go exactly the way a
|
|
4499
|
+
`column` does. Items dropped by their own render gates therefore cascade
|
|
4500
|
+
upwards and a page can lose a whole section with no error anywhere. Only
|
|
4501
|
+
`column` offers `prevent_collapse`.
|
|
4502
|
+
|
|
4503
|
+
A field's **value format** is stated on the field itself, in `value_hint` — a
|
|
4504
|
+
value that does not parse is usually removed or defaulted silently, never
|
|
4505
|
+
rejected. `countdown.date` must be `strtotime()`-parseable or the element
|
|
4506
|
+
disappears; `map_item.location` must be numeric `"lat,lng"` or the marker is
|
|
4507
|
+
skipped (no geocoding); an empty `map.height` becomes 300; a non-numeric
|
|
4508
|
+
`popover_item.position_x`/`_y` becomes 50 %.
|
|
4509
|
+
|
|
2090
4510
|
## Dynamize an existing section (designed layout → live data, one call)
|
|
2091
4511
|
|
|
2092
4512
|
**Goal:** Take a section you (or the customer) already designed by hand
|
|
2093
4513
|
and make it data-driven — repeating over a source's items — WITHOUT
|
|
2094
4514
|
rebuilding it node by node.
|
|
2095
4515
|
|
|
2096
|
-
Use `
|
|
2097
|
-
byte-identically and swaps ONLY what you map, so the design
|
|
2098
|
-
styles, the `#parent` bindings) survives untouched.
|
|
4516
|
+
Use `yootheme_builder_page_generate_dynamic`. It copies the referenced
|
|
4517
|
+
layout byte-identically and swaps ONLY what you map, so the design
|
|
4518
|
+
(spacing, styles, the `#parent` bindings) survives untouched. It always
|
|
4519
|
+
creates a NEW, unpublished, unlinked page from the reference — the
|
|
4520
|
+
reference itself is never modified.
|
|
2099
4521
|
|
|
2100
4522
|
**Canonical tool-call sequence (snake_case):**
|
|
2101
4523
|
|
|
2102
4524
|
1. `yootheme_builder_get_etag()`: current lock.
|
|
2103
|
-
2. `yootheme_builder_template_summary({
|
|
2104
|
-
`yootheme_builder_page_get_layout({
|
|
4525
|
+
2. `yootheme_builder_template_summary({ layout_id })` (or
|
|
4526
|
+
`yootheme_builder_page_get_layout({ layout_id, mode: "outline" })`):
|
|
2105
4527
|
find the REFERENCE you will dynamize — a `section_name` (a named
|
|
2106
4528
|
section from the summary) or an `element_path`.
|
|
2107
4529
|
3. `yootheme_builder_sources_list()`: get the `list_source_name` to bind
|
|
2108
4530
|
(the published source that yields the items).
|
|
2109
|
-
4. `
|
|
4531
|
+
4. `yootheme_builder_page_generate_dynamic({ layout_id, output: "new_page", section_name: "<name>", list_source_name: "<source>", leaf_map: { "<original-leaf-field>": "<flat-flow-field>" }, etag })`.
|
|
2110
4532
|
|
|
2111
4533
|
**How the mapping works:**
|
|
2112
4534
|
|
|
2113
4535
|
- `leaf_map` keys are the ORIGINAL leaf field-names in the designed
|
|
2114
4536
|
section; values are the flat flow field they should now read. The
|
|
2115
|
-
`#parent` binding and any tuned props are kept.
|
|
4537
|
+
`#parent` binding and any tuned props are kept. A KEPT (unmapped) leaf
|
|
4538
|
+
whose original name is not a field the swapped source emits surfaces in
|
|
4539
|
+
the response's `summary.unmapped_kept_fields_missing_in_target` — map
|
|
4540
|
+
it or accept the empty render.
|
|
2116
4541
|
- A section with SEVERAL source blocks dynamizes in ONE call via
|
|
2117
4542
|
`source_map` — an array of `{ match, source_name, leaf_map }`, one
|
|
2118
4543
|
entry per block.
|
|
2119
4544
|
- For sub-query panels, key a `leaf_map` entry by
|
|
2120
4545
|
`"<subquery>.<leaf>"`.
|
|
2121
|
-
- `
|
|
2122
|
-
|
|
2123
|
-
|
|
4546
|
+
- `output` is REQUIRED — "new_page" is the only value available today; a
|
|
4547
|
+
future "in_place" value (editing the reference directly) is not yet
|
|
4548
|
+
available.
|
|
4549
|
+
- `page_generate_dynamic` requires `page.title`. It returns the page plus
|
|
4550
|
+
a `summary`, and `changed_paths` + `new_etag` like every other write.
|
|
4551
|
+
- With `dry_run:true`, a `would_change:false` answer carries a `reason` —
|
|
4552
|
+
most commonly "no bound exemplar element under the reference": the
|
|
4553
|
+
reference has nothing to swap because none of its leaves are bound yet.
|
|
4554
|
+
Bind one element first (`element_bind_source`), then retry.
|
|
2124
4555
|
|
|
2125
4556
|
**Common pitfalls:**
|
|
2126
4557
|
|
|
2127
4558
|
- **Binding Multi-Items on the container.** Bind the source + Multi-Items
|
|
2128
4559
|
on the `*_item` CHILD, never on the grid/list container — see the
|
|
2129
|
-
Multi-Items renderer truth table above. `
|
|
2130
|
-
rule for you when you hand it the right reference.
|
|
4560
|
+
Multi-Items renderer truth table above. `page_generate_dynamic` follows
|
|
4561
|
+
that rule for you when you hand it the right reference.
|
|
2131
4562
|
- **Hand-editing the JSON instead.** Don't reconstruct the section with
|
|
2132
4563
|
`element_add` calls to "make it dynamic" — that loses the design.
|
|
2133
|
-
`
|
|
4564
|
+
`page_generate_dynamic` is the supported one-call path.
|
|
4565
|
+
- **Expecting the reference page to change.** It never does — the result
|
|
4566
|
+
is always a NEW page. Publish the new page or copy its layout onto the
|
|
4567
|
+
reference yourself if you actually want an in-place swap.
|
|
2134
4568
|
- **Reusing a stale etag.** Feed `new_etag` from the response into your
|
|
2135
4569
|
next write.
|
|
2136
4570
|
|
|
@@ -2170,12 +4604,71 @@ apikey). See "WootsUp AI Layouts" below.
|
|
|
2170
4604
|
Browse, get, AND insert against the remote catalog (`source: "catalog"`,
|
|
2171
4605
|
the default) all depend on a YOOtheme Pro Library API key configured on the
|
|
2172
4606
|
connected site (the customer's own theme `apikey`, set once in the YOOtheme
|
|
2173
|
-
Customizer
|
|
2174
|
-
`library_browse` does not error
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
4607
|
+
Customizer, not something this MCP server manages). If it's missing,
|
|
4608
|
+
`library_browse` does not error: it returns `{ items: [], total: 0 }` with
|
|
4609
|
+
`api_key_configured: false` and a `detail_note`. Read that boolean rather
|
|
4610
|
+
than guessing from the count. **This apikey requirement applies ONLY to the
|
|
4611
|
+
remote catalog.** The local AI-Layouts store (next subsection) needs no apikey
|
|
4612
|
+
at all.
|
|
4613
|
+
|
|
4614
|
+
**Do NOT tell the customer to check the apikey field until you know that is the
|
|
4615
|
+
problem.** `api_key_configured: false` means no key could be READ, and the
|
|
4616
|
+
reader answers that for five different situations. One of them is OUR lookup
|
|
4617
|
+
missing the row rather than anything the customer did, and it needs the
|
|
4618
|
+
opposite action. Run `yootheme_builder_diagnose` and read `library_key.state`:
|
|
4619
|
+
|
|
4620
|
+
| `state` | who has to act |
|
|
4621
|
+
|---|---|
|
|
4622
|
+
| `key_present` | nobody. The key is readable, so an empty catalog is a different problem. |
|
|
4623
|
+
| `update_site_without_key` (Joomla) | the customer, in the YOOtheme Customizer theme settings. |
|
|
4624
|
+
| `theme_config_empty` (WordPress) | the customer, same place. |
|
|
4625
|
+
| `theme_config_unavailable` (WordPress) | nobody yet: YOOtheme is not loaded at all. Check the theme is active, then run `yootheme_builder_health`. |
|
|
4626
|
+
| `update_site_not_found` (Joomla) | **nobody, in the key field.** No `pkg_yootheme` update site matched, so the Customizer would show that field empty too. Report the state instead of sending the customer to a field that cannot fix it. |
|
|
4627
|
+
|
|
4628
|
+
`library_key.hint` carries the same verdict as a sentence you can pass on, and
|
|
4629
|
+
`library_key.configured` is the boolean. The key itself is never in the payload.
|
|
4630
|
+
|
|
4631
|
+
**A REFUSED catalog fetch is NOT an empty catalog.** When the site HAS a key
|
|
4632
|
+
and the YOOtheme Pro Library rejects it (measured: HTTP 401 "Invalid API Key"),
|
|
4633
|
+
`library_browse` and `library_get` answer `error_code:
|
|
4634
|
+
"library_upstream_unavailable"` (HTTP 503) carrying `upstream_status` and
|
|
4635
|
+
`api_key_configured`. Do NOT respond to it by dropping filters, trying another
|
|
4636
|
+
id, or calling `library_get` to "verify" the id: nothing you send repairs a key
|
|
4637
|
+
the provider refuses, and `library_get` is refused by the same upstream. Only
|
|
4638
|
+
the site owner can take it up with YOOtheme. Meanwhile the keyless `source: "ai"`
|
|
4639
|
+
store and `page_get_layout` still work. A successful `total: 0` is a different
|
|
4640
|
+
thing entirely: the catalog was read and matched nothing, and that is now the
|
|
4641
|
+
only thing an empty page can mean.
|
|
4642
|
+
|
|
4643
|
+
**Browse can succeed while get and insert fail, and that is normal, not a
|
|
4644
|
+
bug.** The LIST endpoint the browse call uses answers even with NO key at all;
|
|
4645
|
+
the DETAIL endpoint (`/v1/library/layout/<id>`) that `library_get` and
|
|
4646
|
+
`library_insert` need CHECKS the key. Measured on two independent installs:
|
|
4647
|
+
`api.yootheme.com` refused those installs' configured key on every
|
|
4648
|
+
key-checking endpoint, the detail endpoint included, while browsing kept
|
|
4649
|
+
working normally. So "I can browse hundreds of layouts but every `library_get`
|
|
4650
|
+
fails" is a REFUSED KEY, not a bad id and not a broken plugin.
|
|
4651
|
+
|
|
4652
|
+
**Say what was measured, and stop there.** The refusal answers
|
|
4653
|
+
`library_upstream_unavailable` (HTTP 503) with `upstream_status: 401` and
|
|
4654
|
+
`api_key_configured: true`, and the honest report is exactly that: *the
|
|
4655
|
+
YOOtheme Pro Library refused this site's API key, so the call could not be
|
|
4656
|
+
made.* Then stop re-trying ids, because no id repairs a refused key.
|
|
4657
|
+
|
|
4658
|
+
**Do NOT tell the customer WHY it was refused.** You cannot see why. A 401 is
|
|
4659
|
+
the same 401 for a plan that does not cover the endpoint, an expired key, a
|
|
4660
|
+
key that was replaced, a domain or host restriction, and a change on
|
|
4661
|
+
YOOtheme's side. If the customer asks what to check, offer those as
|
|
4662
|
+
POSSIBILITIES and point them at their YOOtheme account; never present one as
|
|
4663
|
+
the diagnosis. Telling a paying customer their plan is the reason is a claim
|
|
4664
|
+
about their account that we have not established and cannot withdraw once the
|
|
4665
|
+
assistant has said it. Never phrase it as a defect in their site either.
|
|
4666
|
+
|
|
4667
|
+
**`library_item_unavailable` (404) is a DIFFERENT state.** It means the item
|
|
4668
|
+
could not be fetched at all: no Library API key is configured on the site, or
|
|
4669
|
+
the id is not in the catalog. A site whose key is configured and refused gets
|
|
4670
|
+
the 503 above, never this 404. Reading them as the same thing sends a customer
|
|
4671
|
+
who HAS a key off to configure one.
|
|
2179
4672
|
|
|
2180
4673
|
### WootsUp AI Layouts — your OWN local library (no apikey, save → reuse)
|
|
2181
4674
|
|
|
@@ -2449,8 +4942,16 @@ at fetch time, never persisted raw):
|
|
|
2449
4942
|
the rest of the insert.
|
|
2450
4943
|
|
|
2451
4944
|
The insert response's `images_resolved: { downloaded, placeholders }`
|
|
2452
|
-
tells you
|
|
2453
|
-
|
|
4945
|
+
tells you HOW MANY of each — check it rather than assuming every image
|
|
4946
|
+
round-tripped. It does not tell you WHICH, and a count on its own is not
|
|
4947
|
+
actionable: `image_import_failures` is. That field appears only when a
|
|
4948
|
+
curated insert actually failed to fetch something, and carries one entry
|
|
4949
|
+
per lost image with the `prop_path` inside the fetched item (e.g.
|
|
4950
|
+
`children[0].props.image`), the `exception_class`, and a `message`. Read it
|
|
4951
|
+
before reporting the insert as finished: a failure is often transient, so a
|
|
4952
|
+
second `library_insert` — or a searched-and-imported photo bound into that
|
|
4953
|
+
exact prop path — usually fixes it. Absent means nothing failed; on
|
|
4954
|
+
`images_mode: "as_saved"` or `"placeholder"` the curate step never ran.
|
|
2454
4955
|
|
|
2455
4956
|
### Fill image slots with real on-topic photos (search → import → bind)
|
|
2456
4957
|
|
|
@@ -2458,9 +4959,13 @@ The professionalism signal is on-brand IMAGERY (see "Make it the CUSTOMER's bran
|
|
|
2458
4959
|
|
|
2459
4960
|
**The flow (three tools):**
|
|
2460
4961
|
1. `yootheme_builder_image_search({ source, query, orientation })` returns candidate rows, each with a `full_url` (the image to import) plus dimensions and author. `source`: `unsplash` or `pexels` for real photography (the primary rich sources); `proimages` for YOOtheme's own curated decorative and background assets. Use `orientation: "landscape"` for heroes and banners, `"portrait"` for tall cards.
|
|
4962
|
+
A search that could NOT be carried out is an ERROR, not an empty shelf: it comes back with `error_code: "image_search_unavailable"` (HTTP 503). Do NOT respond to it by broadening the query. It means the provider refused the request or never answered, most often an HTTP 401 on the site's YOOtheme Pro Library API key (`upstream_status: 401`), which only the site owner can resolve. When that happens, stop searching, keep honest placeholders, and tell the user what `error` said. A SUCCESSFUL `total: 0` is a different thing entirely and DOES mean the query matched nothing; `api_key_configured` tells you whether the key gate was even open.
|
|
4963
|
+
**`api_key_configured: true` plus a 401 is a real, ordinary state: the site HAS a key and the upstream turned it down for this call.** That is the whole of what the two fields establish, and it is not a defect in the plugin or in the site. A YOOtheme Pro Library API key can be perfectly well configured and still be REFUSED. Measured on two independent installs: with such a key, EVERY key-checking endpoint at `api.yootheme.com` refused it, including `/v1/library/layout/<id>`, the DETAIL endpoint `library_get` and `library_insert` depend on. The LIST endpoint behind `library_browse` answers even WITHOUT a key, which is why browsing keeps working and only the detail reads and searches fail. So the honest reading of "browse fine, get and search refused" is: **the upstream refuses this site's key** for those calls. Report exactly that, and stop. **What you must NOT do is name the reason.** Whether it is the subscription behind the key, an expired or replaced key, a domain restriction, or something changed at YOOtheme, is not visible from here, and all of them look like the same 401. If the customer wants to act on it, list those as POSSIBILITIES to check in their YOOtheme account, never as a finding. Telling a paying customer their subscription is short when it is not is not a mistake you get to take back. Do NOT report it as a defect, do NOT retry with broader queries, and do NOT tell them the plugin is broken. Image search works on WordPress and Joomla alike.
|
|
2461
4964
|
2. JUDGE the candidates by FIT and TONE, exactly like you choose a section, and never grab the first result. Search deliberately by the brief's INDUSTRY plus TONE: a coffee roaster searches `"coffee roasting"` / `"espresso pour"` / `"cafe interior"` (warm, editorial); a law firm searches `"modern office architecture"` (clean, restrained). A generic `query: "business"` returns generic stock that reads as filler.
|
|
2462
|
-
3. `yootheme_builder_image_import({ url: "<full_url>" })` sideloads the chosen photo into the site's OWN Media Library and returns `{ local_url }`. Only Unsplash, Pexels, and YOOtheme image hosts can be imported (a security allowlist); other URLs are rejected.
|
|
2463
|
-
|
|
4965
|
+
3. `yootheme_builder_image_import({ url: "<full_url>" })` sideloads the chosen photo into the site's OWN Media Library and returns `{ local_url }`. The argument is also accepted under the name `full_url`, so a row from step 1 can be passed on unchanged — give ONE of the two, never both with different values. Only Unsplash, Pexels, and YOOtheme image hosts can be imported (a security allowlist); other URLs are rejected.
|
|
4966
|
+
**`local_url` has a PLATFORM-SPECIFIC shape, and both are correct.** WordPress returns a root-relative path WITH a leading slash (`/wp-content/uploads/2026/07/hero.jpg`); Joomla returns a JPATH_ROOT-relative path WITHOUT one (`images/yootheme/hero.jpg`, the same shape YOOtheme's own image dialog stores). Bind whatever came back VERBATIM. Adding a slash on Joomla or stripping one on WordPress produces a path the layout cannot resolve, and the element then renders empty with no error anywhere. Importing the same photo twice returns the FIRST attachment instead of creating a duplicate, as long as the earlier copy is still in the site's current upload folder (on WordPress that is `wp-content/uploads/YYYY/MM/`, so a copy imported in an earlier month is not matched and a second attachment is created).
|
|
4967
|
+
A failure never throws: it comes back as `{ imported: false, reason, failure }`. Read `failure` before deciding what to do next. `failure: "rejected"` means THIS url did not import (blocked host, upstream error, the CMS refused the file type), so picking a different candidate is the right move. `failure: "unavailable"` means the SITE cannot import any image right now (no media runtime, no HTTP transport), so stop importing, keep the placeholder, and tell the user what the `reason` said. Trying twenty more photos against an `unavailable` site only wastes the user's time.
|
|
4968
|
+
4. Bind it: `yootheme_builder_element_update_settings({ layout_id, element_path, props: { image: "<local_url>" }, merge: true })`.
|
|
2464
4969
|
|
|
2465
4970
|
**Real photo vs honest placeholder:**
|
|
2466
4971
|
- A real, ON-TOPIC photo beats a placeholder every time; it is the single biggest lift from "template" to "finished". Fill the hero and feature-section image slots with searched photos that match the brief.
|
|
@@ -2482,7 +4987,9 @@ The most common way an AI-built page falls short of a real Pro layout: it defaul
|
|
|
2482
4987
|
1. Compose from the real Library sections FIRST. The section corpus (`library_browse({kind:"section"})`) is about 2000 individually-designed, already-rich sections across every demo. Selecting well among them for fit, tone, and variety, and preserving their design, is the primary path to a varied Pro-quality page. Do NOT strip their rich features (below).
|
|
2483
4988
|
2. When you hand-build a section (no corpus section fits, or you build from native elements), reach into the palette below and pick the rich pattern that fits. Never the bare-minimum version. Vary your choices across the page.
|
|
2484
4989
|
|
|
2485
|
-
**The polish features Pro sections use constantly (these ARE what "rich" means): preserve them when composing, apply them when hand-building. The `panel` props named below are VERIFIED against the LIVE YOOtheme Pro 5.x `panel` schema (`element_type_get_schema("panel")` → 81 fields). `element_type_get_schema` is the source of truth — always confirm the exact key AND its enum values there before writing; a guessed key is
|
|
4990
|
+
**The polish features Pro sections use constantly (these ARE what "rich" means): preserve them when composing, apply them when hand-building. The `panel` props named below are VERIFIED against the LIVE YOOtheme Pro 5.x `panel` schema (`element_type_get_schema("panel")` → 81 fields). `element_type_get_schema` is the source of truth — always confirm the exact key AND its enum values there before writing; a guessed key is stored and reported back under `warnings` (`code:
|
|
4991
|
+
"unknown_prop_keys"`) rather than refused, and a guessed VALUE either
|
|
4992
|
+
no-ops silently or, on an enum field, is refused with 422.**
|
|
2486
4993
|
- **Card styling — THE single biggest "flat vs rich" lever: `panel_style` on a `panel`.** A bare `panel` (or a plain column of text) is what makes a section look flat; a `panel_style` turns it into a real styled CARD. Verified enum: `card-default` / `card-primary` / `card-secondary` / `card-hover` (hover-lift card) / `card-overlay` / `tile-default` / `tile-muted` / `tile-primary` / `tile-secondary`. A feature / team / pricing "card" is a `panel` WITH a `panel_style`, not three bare text columns.
|
|
2487
4994
|
- **Hover** (on a `panel`; the image-hover props require a `link` + `panel_link`/`image_link`, per the schema `enable` rules): `hover_image` (swap the image on hover), `image_transition: scale-up`|`scale-down` (image zoom on hover), `title_hover_style: reset`|`heading`. `panel_style: card-hover` also gives a hover lift on its own.
|
|
2488
4995
|
- **Depth**: `image_box_shadow` (`small`…`xlarge`) and `image_box_decoration` (`default`/`primary`/`secondary`/`shadow`/`mask`) — these apply to a panel image only when NO `panel_style` is set (they are mutually gated: use a `panel_style` OR the box-shadow/decoration, not both).
|
|
@@ -2516,7 +5023,7 @@ ones are legal depends on `kind`:
|
|
|
2516
5023
|
| `target` shape | Accepts `kind: "layout"` | Accepts `kind: "element"` (preset) |
|
|
2517
5024
|
|---|---|---|
|
|
2518
5025
|
| `{ new_page: { title, status? } }` | ✓ creates a page, layout's sections become its content | ✗ rejected — a preset is not a page |
|
|
2519
|
-
| `{
|
|
5026
|
+
| `{ layout_id, element_path? }` | ✓ ONLY the layout ROOT is section-accepting (`element_path: ""` or omitted) | ✓ ONLY a **Column** — every other node type is rejected |
|
|
2520
5027
|
| `{ sublayout: { template_id, element_path } }` | ✓ the fetched layout's sections are stripped to bare ROWS first (a sublayout/fragment accepts rows, not sections) | ✗ rejected — a sublayout is not a Column |
|
|
2521
5028
|
|
|
2522
5029
|
**A preset (`kind: "element"`) MUST target a Column. There is no
|
|
@@ -2531,12 +5038,13 @@ first, then point `element_path` at ONE of the returned `column_paths`:
|
|
|
2531
5038
|
yootheme_builder_library_insert({
|
|
2532
5039
|
id: "8cf55920",
|
|
2533
5040
|
kind: "element",
|
|
2534
|
-
target: { template_id: "home", element_path: "" }
|
|
5041
|
+
target: { template_id: "home", element_path: "" },
|
|
5042
|
+
etag: "<etag>"
|
|
2535
5043
|
})
|
|
2536
5044
|
// → 422 target_not_a_column: 'target at "/" is a "layout"'.
|
|
2537
5045
|
|
|
2538
5046
|
// Fix: scaffold a section->row->column hull, then target the new Column.
|
|
2539
|
-
yootheme_builder_scaffold_section({
|
|
5047
|
+
yootheme_builder_scaffold_section({ layout_id: "home", columns: 1, etag: "<etag>" })
|
|
2540
5048
|
// Response includes column_paths: ["/children/4/children/0/children/0"]
|
|
2541
5049
|
|
|
2542
5050
|
yootheme_builder_library_insert({
|
|
@@ -2555,6 +5063,63 @@ separate strip step. Point `element_path` at the actual sublayout/
|
|
|
2555
5063
|
fragment node itself, not its parent — pointing at the wrong level is a
|
|
2556
5064
|
`422 target_not_a_sublayout`.
|
|
2557
5065
|
|
|
5066
|
+
**The table above is about the TARGET. The ITEM is checked too.** The
|
|
5067
|
+
composed result — the preset, or the layout's sections, or the stripped
|
|
5068
|
+
rows — goes through the SAME layout grammar `page_save` runs, before any
|
|
5069
|
+
image is imported. Two refusals come out of that, and neither is about
|
|
5070
|
+
where you pointed:
|
|
5071
|
+
|
|
5072
|
+
- `422 layout_invalid_hierarchy` — what would be stored is a shape the
|
|
5073
|
+
Page Builder never builds. The common case is a spine node saved as a
|
|
5074
|
+
preset: a Row or a Column put into your own library with
|
|
5075
|
+
`library_save_as` comes back classified as an element, and a Row inside
|
|
5076
|
+
a Column is refused. So is a whole layout spliced into a Column, and so
|
|
5077
|
+
is a **sublayout fragment at a page or layout root** — 67 of the shipped
|
|
5078
|
+
catalog layouts are fragments (the dropdown, search and dialog groups),
|
|
5079
|
+
served under `kind: "layout"` like any other layout. **The one
|
|
5080
|
+
destination that accepts them is a `sublayout` target — never a
|
|
5081
|
+
Column.** The Column branch of the target table only ever opens for
|
|
5082
|
+
`kind: "element"` presets; pointed at a Column, one of these fails
|
|
5083
|
+
before the hierarchy check even runs, with
|
|
5084
|
+
`422 target_not_section_accepting`, and that answer's own hint suggests
|
|
5085
|
+
the layout root or a `new_page` target — both refuse it too, for the
|
|
5086
|
+
same `layout_invalid_hierarchy` reason as the root case above. For this
|
|
5087
|
+
one item type, skip that suggestion and go straight to
|
|
5088
|
+
`target: { sublayout: { template_id, element_path } }`.
|
|
5089
|
+
- `422 library_item_kind_mismatch` — the item carries a different `kind`
|
|
5090
|
+
than the call declared. The declared kind alone decides which branch
|
|
5091
|
+
composes the item, so a layout handed over as `kind: "element"` would go
|
|
5092
|
+
in through the preset branch. The answer names both kinds; retry with
|
|
5093
|
+
the one the item carries. Items from your own library carry their own
|
|
5094
|
+
classification; a catalog id is looked up per kind, so there a wrong
|
|
5095
|
+
`kind` is already a `404 library_item_unavailable`.
|
|
5096
|
+
|
|
5097
|
+
Both are raised BEFORE the write and before any image import, so a
|
|
5098
|
+
refused insert stores nothing and imports nothing.
|
|
5099
|
+
|
|
5100
|
+
### `insert_mode` — and the confirm gate that guards its default
|
|
5101
|
+
|
|
5102
|
+
An `existing`/`sublayout` target already has children whenever you insert
|
|
5103
|
+
into a page you (or a customer) already built. `insert_mode` decides what
|
|
5104
|
+
happens to them, mirroring the three choices the Library dialog itself
|
|
5105
|
+
offers:
|
|
5106
|
+
|
|
5107
|
+
| `insert_mode` | effect |
|
|
5108
|
+
|---|---|
|
|
5109
|
+
| `"replace"` (default) | DROPS the target's existing children; the composed content becomes the whole list |
|
|
5110
|
+
| `"top"` | prepends the composed content before the existing children |
|
|
5111
|
+
| `"bottom"` | appends the composed content after the existing children |
|
|
5112
|
+
|
|
5113
|
+
**`"replace"` on a non-empty target answers with a PREVIEW instead of
|
|
5114
|
+
writing, unless you pass `confirm: true`.** The preview reports
|
|
5115
|
+
`preview: true`, `applied: false`, `requires_confirmation: true`, and —
|
|
5116
|
+
so you know exactly what would be lost — `would_remove_count` (how many
|
|
5117
|
+
children) and `would_remove_types` (their `type`s, in order). Nothing is
|
|
5118
|
+
written on that call; re-send the SAME request with `confirm: true` to
|
|
5119
|
+
replace anyway, or switch to `insert_mode: "top"` / `"bottom"` to keep
|
|
5120
|
+
the existing content instead. An ALREADY-EMPTY target never needs
|
|
5121
|
+
`confirm`, whatever mode you pick — there is nothing there to lose.
|
|
5122
|
+
|
|
2558
5123
|
### Adapting a Library insert is STATIC content editing, not dynamizing
|
|
2559
5124
|
|
|
2560
5125
|
**Every Library layout and preset is authored STATIC** — no source
|
|
@@ -2568,7 +5133,7 @@ section.
|
|
|
2568
5133
|
an archive-style listing) is a deliberate, separate follow-up call, not
|
|
2569
5134
|
something insert does for you.** Use the existing dynamize workflow
|
|
2570
5135
|
documented in "Dynamize an existing section" just above this one
|
|
2571
|
-
(`
|
|
5136
|
+
(`yootheme_builder_page_generate_dynamic`) to re-target a Library-inserted
|
|
2572
5137
|
section's leaf fields onto a Source, or bind a Multi-Items container
|
|
2573
5138
|
directly with `yootheme_builder_element_bind_source` (Workflow 2) if the
|
|
2574
5139
|
inserted item already has a grid/list container you want to re-point at
|
|
@@ -2590,7 +5155,7 @@ live data instead of its demo items.
|
|
|
2590
5155
|
- **Stuffing placeholder copy into a field you have nothing for**, to
|
|
2591
5156
|
defeat collapsing. Leave it empty; collapsing is the intended behavior.
|
|
2592
5157
|
- **Expecting `library_insert` to make anything dynamic.** It composes
|
|
2593
|
-
STATIC content only — dynamizing is the separate `
|
|
5158
|
+
STATIC content only — dynamizing is the separate `page_generate_dynamic` /
|
|
2594
5159
|
`element_bind_source` call.
|
|
2595
5160
|
|
|
2596
5161
|
**Worked example (browse → get → insert → adapt):**
|
|
@@ -2620,7 +5185,7 @@ yootheme_builder_library_insert({
|
|
|
2620
5185
|
images: "placeholder",
|
|
2621
5186
|
etag: "<etag>"
|
|
2622
5187
|
})
|
|
2623
|
-
// Response: {
|
|
5188
|
+
// Response: { layout_id: "home", inserted_path: "/children/3", element_count: 12,
|
|
2624
5189
|
// images_mode: "placeholder", images_resolved: { downloaded: 0, placeholders: 6 },
|
|
2625
5190
|
// new_etag: "<fresh>", frontend_url: null }
|
|
2626
5191
|
|
|
@@ -2628,7 +5193,7 @@ yootheme_builder_library_insert({
|
|
|
2628
5193
|
// inserted section back first (page_get_layout / element_get) to find the
|
|
2629
5194
|
// real headline's element_path, then edit CONTENT only — style untouched.
|
|
2630
5195
|
yootheme_builder_element_update_settings({
|
|
2631
|
-
|
|
5196
|
+
layout_id: "home",
|
|
2632
5197
|
element_path: "/children/3/children/0/children/0",
|
|
2633
5198
|
props: { content: "<customer's real headline>" },
|
|
2634
5199
|
merge: true,
|
|
@@ -2705,58 +5270,69 @@ perform.
|
|
|
2705
5270
|
|
|
2706
5271
|
<!-- TOOL-CATALOG:BEGIN -->
|
|
2707
5272
|
|
|
2708
|
-
**
|
|
5273
|
+
**60 catalogued tools** plus the `yootheme_builder_advanced` (write) + `yootheme_builder_advanced_read` (read-only) gateways = **62 callable** end to end, of which **49 are advertised in `tools/list`** (45 L1 + 2 L3 + 2 gateways); the remaining 13 advanced tools are reachable only through the gateways. Generated by `scripts/extract-tools.mjs` from the compiled `buildAllTools()` registry. Do not hand-edit this section; re-run `npm run build && node scripts/extract-tools.mjs` after changing tool definitions.
|
|
2709
5274
|
|
|
2710
5275
|
| Tool | Kind | Input keys | Description |
|
|
2711
5276
|
| --- | --- | --- | --- |
|
|
2712
|
-
| `yootheme_builder_clean_implode_directives` | destructive | `confirm`, `element_path`, `etag`, `
|
|
2713
|
-
| `yootheme_builder_diagnose` | read+idempotent | `site_id` |
|
|
2714
|
-
| `yootheme_builder_element_add` |
|
|
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
|
|
2716
|
-
| `yootheme_builder_element_clone` |
|
|
2717
|
-
| `yootheme_builder_element_delete` | destructive | `confirm`, `element_path`, `etag`, `
|
|
2718
|
-
| `yootheme_builder_element_get` | read+idempotent | `element_path`, `fields`, `
|
|
2719
|
-
| `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `
|
|
2720
|
-
| `yootheme_builder_element_list` | read+idempotent | `cursor`, `depth`, `fields`, `limit`, `max_chars`, `root_path`, `site_id
|
|
2721
|
-
| `yootheme_builder_element_move` | idempotent | `element_path`, `etag`, `
|
|
2722
|
-
| `yootheme_builder_element_set_source` | idempotent | `element_path`, `etag`, `site_id`, `source`, `source_extended
|
|
2723
|
-
| `
|
|
2724
|
-
| `
|
|
2725
|
-
| `
|
|
2726
|
-
| `
|
|
2727
|
-
| `
|
|
2728
|
-
| `
|
|
2729
|
-
| `
|
|
2730
|
-
| `
|
|
2731
|
-
| `
|
|
2732
|
-
| `
|
|
2733
|
-
| `
|
|
2734
|
-
| `
|
|
2735
|
-
| `
|
|
2736
|
-
| `
|
|
2737
|
-
| `
|
|
2738
|
-
| `
|
|
2739
|
-
| `
|
|
2740
|
-
| `
|
|
2741
|
-
| `
|
|
2742
|
-
| `
|
|
2743
|
-
| `
|
|
2744
|
-
| `
|
|
2745
|
-
| `
|
|
2746
|
-
| `
|
|
2747
|
-
| `
|
|
2748
|
-
| `
|
|
2749
|
-
| `
|
|
2750
|
-
| `
|
|
2751
|
-
| `
|
|
2752
|
-
| `
|
|
2753
|
-
| `
|
|
2754
|
-
| `
|
|
5277
|
+
| `yootheme_builder_clean_implode_directives` | write+destructive+idempotent | `confirm`, `element_path`, `etag`, `layout_id`, `site_id` | Use when a Multi-Items element renders rows as ONE comma-joined string instead of N repeated items — stale `implode` directives left by a plugin upgrade (detect via inspect_multi_items_binding). Destructive: the binding can't be restored without re-mapping. The clean requires confirm:true + ETag; the dry run neither. Without `confirm` the call runs a server-side DRY RUN and reports `would_clean_count` while writing nothing. Live at once. |
|
|
5278
|
+
| `yootheme_builder_diagnose` | read+idempotent | `site_id` | Why a call was refused: `auth_bound` (key accepted), `etag_ok` (layout readable), `site_url` (WHICH install; a key valid elsewhere fails here), `capability_ok` + `capability_warning` (the CMS user has no `can_edit`, so no blind 403). Install facts: health. |
|
|
5279
|
+
| `yootheme_builder_element_add` | write+non-idempotent | `children`, `element_type`, `etag`, `index`, `layout_id`, `name`, `parent_path`, `position`, `props`, `site_id`, `type_name` | Add a new element to a template. Provide `parent_path` (or "" for root), `element_type`, and optional `props`/`children`. Content/leaf elements may only be added inside a `column`; at the root only `section` — else call scaffold_section for a valid column path. Inserting renumbers later siblings: `renumbered_siblings` names each moved sibling address (absent if none). Requires ETag. Live at once in the stored tree — already save-transformed (defaults filled, empty children removed). Stage hidden with element_set_status(status:false). Prop names are snake_case. Common traps: `content` (not `title`) on button_item/headline/list_item/quotation/text; check `element_type_get_schema` first. The answer carries no `props`, only the new address. The stored node also carries the element's defaults — read it back with element_get. |
|
|
5280
|
+
| `yootheme_builder_element_bind_source` | write+destructive+idempotent | `arguments`, `bindingLevel`, `confirm`, `directives`, `element_path`, `etag`, `field`, `field_mappings`, `layout_id`, `probe`, `query_arguments`, `relation`, `site_id`, `source_id`, `source_name` | Bind 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); leaf under a #parent fragment → INHERIT (`__node_item__`); a standalone query renders empty. Map: bind `map_item`, map `location` to a "lat,lng" field or no markers. Requires ETag. Live at once. CONVENIENCE half of a pair — plain field names, but it REPLACES `source.props` whole, never merges; element_set_source is the CAPABILITY half. A re-bind that would drop anything already bound answers with a preview until you repeat it with `confirm: true`. To remove a binding, call element_unbind_source. |
|
|
5281
|
+
| `yootheme_builder_element_clone` | write+non-idempotent | `element_path`, `etag`, `layout_id`, `site_id`, `to_index`, `to_parent_path` | Clone an element. Default: as a sibling right after the source; pass `to_parent_path` to drop the copy elsewhere in the SAME template instead. Renumbers later siblings: `renumbered_siblings` names each moved sibling address (absent if none). Requires ETag. Live at once — stage hidden with element_set_status(status:false). |
|
|
5282
|
+
| `yootheme_builder_element_delete` | write+destructive+non-idempotent | `confirm`, `element_path`, `etag`, `layout_id`, `site_id` | PERMANENTLY delete an element + children. Cannot be undone. Pass `confirm: true` AND a fresh ETag. Preview: neither. A real delete returns `deleted:true` and NO `requires_confirmation` key; a returned `requires_confirmation:true` means NOTHING was deleted. `renumbered_siblings` names each moved sibling address (absent if none). Live at once. Hide instead: element_set_status(status:false). |
|
|
5283
|
+
| `yootheme_builder_element_get` | read+idempotent | `element_path`, `fields`, `layout_id`, `max_chars`, `site_id` | Get the full element object at a JSON-Pointer path, including props and a `children_count` (not the children — use element_list with root_path). On a `module`/`module_position` element it also returns `resolved` (the referenced widget/module row) plus `resolved_reason` when it could not be resolved. `max_chars` bounds the text output. |
|
|
5284
|
+
| `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `layout_id`, `max_chars`, `site_id` | Read the source binding on an element. Returns {layout_id, element_path, binding, has_binding, bound_via_item, item_binding?}: on a multi-item container (grid/slideshow/accordion/...) the binding sits on the `*_item` CHILD, so `has_binding:false` there does NOT mean static — read `bound_via_item`/`item_binding` first. Site-wide REVERSE question (which elements hang on a source): yootheme_builder_layout_query (gateway-only), OMIT layout_id. To remove what this returns, call element_unbind_source. |
|
|
5285
|
+
| `yootheme_builder_element_list` | read+idempotent | `cursor`, `depth`, `fields`, `include_meta`, `layout_id`, `limit`, `max_chars`, `root_path`, `site_id` | List elements in a template as a flat array with JSON-Pointer paths + types + `label`. Scope with `root_path`/`depth`, paginate with `limit`/`cursor`. Rows carry BOTH addresses: `path` (absolute) and `rel_path` — `rel_path` is the one write calls take as `element_path`. To find WHERE a module/widget is used, call local_content_read (gateway-only), never loop this per page. |
|
|
5286
|
+
| `yootheme_builder_element_move` | write+idempotent | `element_path`, `etag`, `layout_id`, `site_id`, `to_index`, `to_parent_path` | Move an element to a new parent + index in the tree — reordering or reparenting. Renumbers siblings at BOTH ends: `renumbered_siblings` names each moved sibling address (absent if none). Requires ETag. Live at once. |
|
|
5287
|
+
| `yootheme_builder_element_set_source` | write+destructive+idempotent | `confirm`, `element_path`, `etag`, `layout_id`, `site_id`, `source`, `source_extended` | Surgically SET a precomposed `source` (+ optional `source_extended.props`) on ONE element at a pointer, ETag-locked. Use this — not element_bind_source — when you need `source_extended.props`, which bind_source cannot write. CAPABILITY half of a pair — it also REPLACES `source.props` whole, never merges; element_bind_source is the CONVENIENCE half. Element-prop keys the type does not render echo back as `unknown_prop_keys`. WHOLE-NODE write: whatever you do not send is REMOVED. Omitting `source_extended` clears it exactly like passing `null`, and a `source` without `props` drops every prop mapping. Re-send what you want to keep. A call that would drop existing mappings answers with a preview until you repeat it with `confirm: true`. Live at once. To remove a binding entirely, call element_unbind_source. |
|
|
5288
|
+
| `yootheme_builder_element_set_status` | write+idempotent | `element_path`, `etag`, `layout_id`, `site_id`, `status` | Enable or disable an element via its `props.status` field — the SAME field YOOtheme's own native "Disable element" checkbox writes. status:false excludes the element and its descendants from BOTH the live render and live findings; pages_audit (gateway-only) and template_summary content_nodes both honour it. Requires ETag. Live at once. |
|
|
5289
|
+
| `yootheme_builder_element_type_get_schema` | read+idempotent | `element_type`, `field_names`, `fields`, `include_fields`, `max_chars`, `name_contains`, `site_id`, `type_name` | Prop schema; read before element_add/element_update_settings: undeclared keys are STORED, not refused: warns unknown_prop_keys. NO element_type: lists the types. With one: head only — binding_contract (containers/items) + groups[] + field_count. include_fields:true adds every descriptor {name,type,label?,text?,description?,enum?,value_hint?,group?,no_effect?}; Select descriptors: field_names/name_contains, answered with field_count/returned_count. value_hint+enum first: some defaults are SEO-fatal (headline.title_element→h1); checkbox = boolean true/false; no_effect = read by nothing, never write it; group:"runtime-accepted" = honoured at render, hidden in UI. Bound size with fields[]/max_chars. |
|
|
5290
|
+
| `yootheme_builder_element_types_list` | read+idempotent | `fields`, `include_meta`, `site_id` | List element types registered on this site (built-ins + YOOessentials extras). Names feed `element_type` of element_add. "slider"/"carousel" → `slideshow`/`panel-slider`/`overlay-slider`. Prop keys differ per element — use element_type_get_schema before element_add/bind. |
|
|
5291
|
+
| `yootheme_builder_element_unbind_source` | write+destructive+idempotent | `confirm`, `element_path`, `etag`, `layout_id`, `site_id` | Remove the source binding from an element — the counterpart to element_bind_source. Clears the canonical top-level `source` key AND the legacy `props.source` — may break dynamic-content rendering, always ask the user to confirm. ETag on the unbind only. Live at once. |
|
|
5292
|
+
| `yootheme_builder_element_update_settings` | write+idempotent | `element_path`, `etag`, `layout_id`, `merge`, `name`, `props`, `site_id`, `unset` | Update `props` on an element. Default replaces all props; pass `merge:true` for server-side deep-merge (only request keys overwritten). Requires ETag. `unset:["key",…]` resets props to their YOOtheme default and is valid ON ITS OWN: sent without `props`, it removes only those keys and touches nothing else. Send `props`, `unset`, or both — a call with neither is refused. Live at once. |
|
|
5293
|
+
| `yootheme_builder_fetch_public_url` | read+idempotent+openWorld | `expect`, `max_excerpt_chars`, `site_id`, `url` | The independent proof a publish reached visitors: fetches a page of the CONFIGURED site over the public network as an ANONYMOUS visitor — no login/cookies/Bearer — and reports http_status, page_title, an EXCERPT (never the whole document), html_bytes, and whether a cache served it (cache_verdict; "unknown" means no header said, NOT that nothing cached, and no re-fetch settles it — purge the cache instead). With `expect` it answers expect_found true/false. Refuses any address outside the configured site and never follows a redirect off it — an unrestricted fetcher is an open proxy. Get the url from `frontend_url` (pages_list / page_publish). Searches the WHOLE page (header, menu, footer too); a hit is no proof of THIS layout. render_preview renders ONLY that layout tree, never the theme wrapper. |
|
|
5294
|
+
| `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current site-wide ETag for optimistic locking. ANY write to ANY template bumps it — a 412 means the site state advanced, not necessarily your template. Same token page_get_layout/element_list return. Pass back as `etag` on writes; on 412, re-read and retry. The ETag is SITE-wide, not per template — that is why this tool takes no `layout_id`. |
|
|
5295
|
+
| `yootheme_builder_get_skill` | read+idempotent | `site_id`, `topic` | Read a chapter of the full workflow guide. Without `topic`: the list of available topics, each with the SIZE of what it returns. With `topic`: that chapter. The connect-time `instructions` carry only a short preamble — this is how you reach the rest. A long chapter is served in PARTS: its row lists their slugs, and asking for the parent returns the chapter intro plus that list. A row carrying `oversized_note` is served whole and is large — read the note before asking for it. |
|
|
5296
|
+
| `yootheme_builder_header_transparency_set` | write+idempotent | `acknowledge_contrast_risk`, `dry_run`, `etag`, `layout_id`, `mode`, `pull_content_behind`, `site_id`, `text_color`, `transparent` | Inspect + repair the transparent header. Always targets the FIRST section — later ones are silently dead. dry_run defaults TRUE: returns current state + the theme-declared --uk-inverse, risk_class, recommendation. mode:"auto" writes the theme-declared colour; over a hero image/video it writes NONE and says LOOK. mode:"explicit" refuses a colour contradicting the theme (409) unless acknowledge_contrast_risk. transparent:false is always allowed. |
|
|
5297
|
+
| `yootheme_builder_heading_outline` | read+idempotent | `layout_id`, `max_chars`, `mode`, `site_id` | A real, doc-order H1-H6 outline of ONE template, each heading with its source element, plus alt-status of every image. Structure inspection, not findings — use pages_audit for site-wide SEO findings. One row per CARRIER: a container whose bound item repeats appears once, while template_summary.heading_levels counts it once per rendered row. |
|
|
5298
|
+
| `yootheme_builder_health` | read+idempotent | `site_id` | Check plugin installed/reachable. Returns plugin version, YT Pro version, REST endpoints, site_url, home_url on both platforms; wp_version + yooessentials_version are WordPress-only. available_endpoints is derived from the live route registration on both platforms; every path is request-ready as printed. served_areas lists the tool areas this server serves (was `capabilities` — kept one release as an alias). Multi-site: pass `site_id`, else this checks the active/default site. `write_rate_limit` = the write cap this site enforces, per BEARER KEY: everything sharing the key shares it. See diagnose for Bearer-validity + connectivity. |
|
|
5299
|
+
| `yootheme_builder_image_import` | write+idempotent | `full_url`, `site_id`, `url` | Sideload a photo into the site's Media Library. Only Unsplash, Pexels and the YOOtheme library CDN are accepted; other hosts are refused. Returns `{imported:true, local_url}`; shape is platform-specific: WordPress leads with a slash (/wp-content/uploads/...), Joomla does NOT (images/yootheme/...). Bind it VERBATIM via element_update_settings({props:{image: local_url}, merge:true}). Give the address as `url` OR as `full_url`, the field name an image_search row carries; one of the two is required. Identical re-imports reuse the existing file in the current upload folder. Failures return `{imported:false, reason, failure}`; `unavailable` = no url works now. |
|
|
5300
|
+
| `yootheme_builder_image_search` | read+idempotent | `color`, `include_meta`, `offset`, `orientation`, `per_page`, `query`, `site_id`, `source` | Search on-topic photos (Unsplash/Pexels), YOOtheme Pro Images, or (source:"local") this site's own media library. Rows carry `full_url` (feeds image_import) plus dimensions/author. `total:0` is the ONLY shape meaning the provider matched nothing; a search that never happened answers an `error_code` instead. No apikey gives `image_search_api_key_not_configured`; a provider that REFUSES gives `error_code: image_search_unavailable`, which no broader query can fix. A key can be CONFIGURED and still be REFUSED (401): report that the upstream refused it, never why (source:"local" is exempt — see its field description). Works on WordPress and Joomla. |
|
|
5301
|
+
| `yootheme_builder_inspect_multi_items_binding` | read+idempotent | `element_path`, `layout_id`, `site_id` | Reports Multi-Items binding state: container/item pair (grid↔grid_item, map↔map_item, …), binding level (none\|container\|item), recommended_fix. For map/map_item, a bound item without a `location` field is flagged as a blocking map_location_warning (0 markers). If rows render as ONE comma-joined string, see clean_implode_directives (gateway-only). |
|
|
5302
|
+
| `yootheme_builder_inspect_sublayout` | read+idempotent | `element_path`, `layout_id`, `max_chars`, `site_id` | Inspects a panel/grid_item `< SUBLAYOUT`: lists the `fragment` subtree bindable leaves and the render-time inner grid. Flags `ungated_grid_prop`: grid props set but the gate unsatisfied → silent no-op. For container↔item binding see inspect_multi_items_binding. |
|
|
5303
|
+
| `yootheme_builder_layout_query` | read+idempotent | `content_contains`, `cursor`, `element_type`, `fields`, `has_binding`, `has_implode_directives`, `include_meta`, `layout_id`, `limit`, `max_chars`, `max_depth`, `max_paths`, `name_matches`, `site_id`, `source_name`, `under_section` | TWO scopes. OMIT layout_id → SITE-WIDE: binding counts per `source_name` (COUNTED, never resolved — a source that no longer exists counts like a live one; pages_audit flags those as `dangling-source`, with paths), `max_paths` for individual paths, `has_implode_directives:true` to locate the comma-joining leftovers. WITH layout_id → ONE layout, AND-combined `element_type`, `name_matches`, `content_contains` (case-insensitive substring of `content`/`title`/`meta` props), `has_binding`, `under_section`; paginate `limit`/`cursor`. Mixing scope arguments is REFUSED, never dropped. To find WHERE a module/widget is used call local_content_read (kind:"modules"). On a module/module_position row `label` is the builder-assigned element name, NOT the module title: read element_get(.resolved). |
|
|
5304
|
+
| `yootheme_builder_library_browse` | read+idempotent | `cursor`, `element_type`, `has_effects`, `include_meta`, `kind`, `limit`, `purpose`, `query`, `role`, `site_id`, `source`, `topic`, `type`, `website` | Browse the YOOtheme Pro Library catalog: pre-designed page LAYOUTS, element PRESETS, or purpose-classified SECTIONS. Filter: `kind`, `topic`, `type`/`role`, `element_type`, `purpose`, `query`. A section id is the composite "<layoutId>#<sectionIndex>" — feeds library_get/insert. `source`: "catalog" (default) or "ai" — YOUR saved entries from library_save_as, which library_delete removes again — both gateway-only, call via yootheme_builder_advanced({tool, arguments}). `total:0` = the catalog matched nothing; a read with no data answers `library_upstream_unavailable` or `library_api_key_not_configured`, never an empty page. |
|
|
5305
|
+
| `yootheme_builder_library_delete` | write+destructive+non-idempotent | `confirm`, `id`, `site_id`, `source` | Delete ONE saved entry from your "WootsUp AI Layouts" store (library_save_as writes into it) — pass the `id` from a library_browse(source:"ai") row. `source` defaults to "ai", the ONLY deletable store; the curated library is read-only, source:"catalog" returns 403. IRREVERSIBLE — requires confirm:true. |
|
|
5306
|
+
| `yootheme_builder_library_get` | read+idempotent | `id`, `include_thumbnail`, `kind`, `max_chars`, `site_id`, `source` | Fetch ONE Library layout or element preset by `id` — call after library_browse (items[].id feeds this). `include_thumbnail:true` adds an image (costs tokens). This DETAIL endpoint checks the key, browse does not: no key gives 503 library_api_key_not_configured, a refused key 503 library_upstream_unavailable. Report the refusal, not a cause. `source`: "catalog" (default) or "ai" for your own library_save_as (gateway-only) entries. |
|
|
5307
|
+
| `yootheme_builder_library_insert` | write+destructive+non-idempotent | `confirm`, `etag`, `id`, `images`, `index`, `insert_mode`, `kind`, `section_index`, `site_id`, `source`, `target` | Insert a Library item onto a page — call after library_get/browse. `target`: new_page, {template_id,element_path?} (splices onto the layout root, or a preset onto a Column), or sublayout. A preset (kind:"element") MUST target a Column (422 otherwise) — scaffold one with scaffold_section. `images`: "placeholder" (default) or "curated" (downloads to Media Library). Placement: `insert_mode` for layouts, `index` for presets. `etag` locks (412 if stale). `source`: "catalog" (default) or "ai" to insert your own saved entry. |
|
|
5308
|
+
| `yootheme_builder_library_save_as` | write+non-idempotent | `destination`, `name`, `site_id`, `source` | Save a page or subtree into a reusable library — the REVERSE of 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 source:"ai") or "native" (YOOtheme's built-in My Layouts). |
|
|
5309
|
+
| `yootheme_builder_local_content_read` | read+idempotent+openWorld | `fields`, `include_children`, `include_meta`, `include_usage`, `kind`, `limit`, `max_chars`, `offset`, `parent_id`, `search`, `site_id`, `status`, `taxonomy`, `type`, `verify_on_urls` | Read native records for one `kind`: entities, terms (ids that archive-scope a Grid), users, fields, menus, media, or modules (Builder widgets). Paginated {items,total,next_offset}, clamped to what the key may see. A deleted page/article is not in the default list; `status` (kind:"entities" only) gives the exclusive trashed view; the response carries `trashed_total` unasked when one exists. Archive-scoping: read {kind:"terms",taxonomy:"category"}, pick a term id, bind a source with query_arguments:{id:<term_id>}. Call local_content_types first. |
|
|
5310
|
+
| `yootheme_builder_local_content_types` | read+idempotent | `fields`, `kind`, `max_chars`, `site_id` | Discover the site's NATIVE content landscape, native to WP/Joomla (no API Mapper) — {content_types, taxonomies, field_groups, capabilities, positions}; taxonomies include ACF/com_fields groups. Content-type and taxonomy rows carry `bindable` + `source_name` (null for native types — bind via element_bind_source by content-type name); field-group rows carry neither. `positions` (B23) lists every valid widget-area / theme-position value for modules_create/modules_update's `position` argument — read it here BEFORE a write, not from a 400 refusal after one; null when not determinable, never `[]` as a claim that none exist. Call first, then local_content_read for records. |
|
|
5311
|
+
| `yootheme_builder_modules_create` | write+non-idempotent | `base`, `cache`, `cache_time`, `cachemode`, `class_sfx`, `content`, `dry_run`, `endLevel`, `layout`, `menutype`, `moduleclass_sfx`, `placement`, `position`, `showAllChildren`, `showtitle`, `site_id`, `startLevel`, `tag_id`, `text`, `title`, `type`, `window_open` | Create a widget (WordPress: custom_html/text/block) or module (Joomla: mod_custom/mod_menu), exactly five known types. REQUIRES placement (position, module_element or position_element): a call without it creates nothing and returns ways (every way this platform offers) plus notes. `position` is validated server-side against what this site declares and must agree with placement. content/text stored byte-for-byte, never sanitized. dry_run defaults TRUE, creates nothing; a real create returns the full row PLUS next_step, the exact follow-up call for the placement you chose. Gateway-only, not in tools/list — call via yootheme_builder_advanced. |
|
|
5312
|
+
| `yootheme_builder_modules_delete` | write+destructive+non-idempotent | `acknowledge_element_refs`, `confirm`, `id`, `intent`, `site_id` | Delete a widget (WordPress) or module (Joomla) — the clean-up verb create/update lacked. Needs `intent`: call once without it and the server lists which are possible for this module's CURRENT state, each with its own effect. confirm defaults FALSE: returns the row plus usage.element_refs and changes NOTHING. A DIRECT layout reference blocks the irreversible step (409) until acknowledge_element_refs:true. |
|
|
5313
|
+
| `yootheme_builder_modules_update` | write+idempotent | `access`, `active`, `assignment`, `content`, `dry_run`, `id`, `ordering`, `position`, `showtitle`, `site_id`, `theme_overlay`, `title` | Update a widget/module's writable subset: title, content (byte-identical), position, active, theme_overlay; plus showtitle, ordering, access, assignment on Joomla. dry_run defaults TRUE, returns {before, after, changed, notes}, writes nothing. Refused: title on a WordPress block widget (it has none), a position this site does not declare (either platform, same error_code as modules_create), and active:true while a Joomla publish window leaves it scheduled/expired — there active means published AND that window. Joomla: active:true also lifts a module out of the trash, PUBLISHED not as it was. position:"" clears the position on Joomla, refused on WordPress (deactivate with active:false instead). Gateway-only, not in tools/list — call via yootheme_builder_advanced. |
|
|
5314
|
+
| `yootheme_builder_navigation` | read+idempotent | `menu`, `site_id` | The site's navigation menus as a parent→children TREE, not a flat list — "which page is under which menu item". Each item carries a `page_role` facet and, when derivable, a `target_page` ready for page_get_layout. |
|
|
5315
|
+
| `yootheme_builder_page_generate_dynamic` | write+non-idempotent | `conditions`, `dry_run`, `element_path`, `etag`, `layout_id`, `leaf_map`, `list_source_name`, `output`, `page`, `remove_leaves`, `section_name`, `site_id`, `source_map` | Copies a designed section byte-identically into a NEW, unpublished, unlinked page and swaps only what you map — it never edits the reference. Requires output:"new_page" (today's only value; "in_place" is not yet available). Give a REFERENCE (section_name or element_path) + list_source_name + a leaf_map. A section with several source blocks dynamizes in ONE call via source_map. Preview with dry_run:true (a would_change:false answer carries `reason`). Live at once — stage hidden with element_set_status(status:false). The response carries `cms_visibility.findable_by_visitor`: can a visitor FIND this one page via a navigation path? It reads the menu route AND the access level, which the per-row `visibility` of pages_list does not. Cannot make it the site front page (a CMS menu setting). |
|
|
5316
|
+
| `yootheme_builder_page_get_layout` | read+idempotent | `fields`, `flat`, `force`, `include_modules`, `layout_id`, `max_chars`, `mode`, `site_id`, `url` | Get the layout tree for one layout — address by `layout_id` or `url`. `mode:"bindings_only"` for dynamize/bind. Over `max_chars` whole entries DROP (`truncated`+`omitted_count`; outline cuts its text) and NOTHING pages the rest — no cursor in any mode. Narrow instead: `mode:"outline"` to map, then element_list(root_path). For a site-wide SEO/structure audit do NOT loop this tool page-by-page — call yootheme_builder_pages_audit (gateway-only). For ONE narrow question (e.g. "where is the binding" or "which elements have type X") this whole-template read is heavier than it needs to be — prefer yootheme_builder_layout_query (gateway-only; has_binding/element_type/name_matches filters) or element_list with `root_path` to scope the walk. |
|
|
5317
|
+
| `yootheme_builder_page_publish` | write+idempotent | `cms_status`, `etag`, `layout_id`, `menu_item`, `quick_check`, `site_id` | Publish a template's LAYOUT — persist state, flush the plugin's own caches (a page-cache/CDN plugin is NOT purged: purge it yourself, then verify with fetch_public_url), snapshot the ETag; "publish" ALSO means the CMS post status, and this tool does not change that, so a draft page stays invisible. Step (2) of the three page_save describes; pages_set_status is step (3). ETag required — 412 on conflict. Answers `cms_visibility.findable_by_visitor`: can a visitor FIND this one page via a navigation path? It reads the menu route AND the access level, which the per-row `visibility` of pages_list does not. Optional `cms_status` folds step (3) into this call for the common build-it-and-make-it-live case; without it nothing about the CMS status changes. |
|
|
5318
|
+
| `yootheme_builder_page_save` | write+idempotent | `dry_run`, `etag`, `layout`, `layout_id`, `quick_check`, `site_id` | Re-run save-transforms + flush the plugin's own caches. Optional whole-tree replace via `layout` — the INNER {type:"layout",children:[...]} tree, NOT page_get_layout's whole response (rejected 422). THREE separate steps, and this is the first: (1) page_save replaces or re-normalises the stored tree — every write, element or whole-tree, runs the same save-transforms; (2) page_publish snapshots the ETag and flushes them; (3) pages_set_status decides whether a visitor may see the page at all. This call does NOT do (2) or (3). Response `previous_layout` is the tree right before THIS call, not before your edits (element writes already landed) — identical to what you just wrote is normal when the transforms change nothing. `dry_run:true` preflights, no ETag. ETag on the save (428/412). |
|
|
5319
|
+
| `yootheme_builder_pages_audit` | read+idempotent | `checks`, `include_meta`, `include_modules`, `max_chars`, `mode`, `site_id`, `stale_after_months`, `target`, `thin_content_threshold`, `types` | One-call site-wide SEO / structure / content / links / design audit — severity-sorted findings with fix_hints (a props fragment to merge). Covers heading structure and accessibility (h1/heading-skip), image alt text, thin content, duplicate layouts, title collisions, broken in-page anchor links (same-page `#id` jumps only — no external link is ever fetched or resolved), and design consistency (markup outside the UIkit design system, header/navigation contrast). Only active (non-disabled) nodes produce findings. `target` scopes to ONE page for the full set. SEO-plugin meta (title/description/OG) NOT covered. |
|
|
5320
|
+
| `yootheme_builder_pages_create` | write+non-idempotent | `dry_run`, `layout`, `menu_item`, `published`, `site_id`, `slug`, `status`, `title` | Create a page (WordPress) or article (Joomla 5/6) with a per-page layout. Required: title. Optional: status (default draft), layout, menu_item, dry_run (preflight, no persist). Joomla: a published article has NO SEF route until a menu item points at it — pass menu_item for a navigable url. Cannot make it the site front page: which page answers / is a CMS menu setting this server never writes. pages_list marks the current one with is_public_homepage. |
|
|
5321
|
+
| `yootheme_builder_pages_delete` | write+destructive+non-idempotent | `confirm`, `intent`, `layout_id`, `site_id` | Delete a page (WordPress) or article (Joomla 5/6). OMIT confirm to preview: returns 200 with label, elements_count, what happens to media, and the intents this page's state allows, and deletes nothing. confirm:true + an intent carries it out. Already-trashed returns 200 {deleted:false, already_trashed:true}, not an error. |
|
|
5322
|
+
| `yootheme_builder_pages_list` | read+idempotent | `cursor`, `fields`, `include_meta`, `label_contains`, `limit`, `max_chars`, `site_id`, `sort`, `visibility` | List pages, templates and layouts. Each row returns `kind` (content \| theme_template) plus serves_context_kind, serves_context_ids and page_role — the template ROLE (WHICH objects it renders for), NOT a data binding; for who-binds-what call layout_query with no layout_id. The envelope returns `totals`. Call first for layout_id (page_get_layout/element_list). `type` = PAGE type, not element type (those filter on `element_type`). Rows with a CMS status carry `visibility`: public \| hidden \| undetermined, never empty. Reads STORED FIELDS only — status, plus WordPress password protection (`hidden`, with the reason). It does NOT read the menu route, the access level, or whether the permalink resolves, so Joomla rows stay `undetermined`; for THAT verdict read `cms_visibility.findable_by_visitor` from pages_set_status / page_publish — a different, per-page question. Page contents (SEO/structure/links/modules): yootheme_builder_pages_audit. `is_public_homepage` marks the one row served at / (the site front page); no tool here moves it. |
|
|
5323
|
+
| `yootheme_builder_pages_resolve` | read+idempotent | `site_id`, `target`, `url` | Resolve a page/article/archive URL to the layout_id that renders it. SEF/pretty permalinks resolve on both WordPress and Joomla. A category/term resolves to the SPECIFICALLY-ASSIGNED template (honoring include-child-categories), not a generic one. Returns {layout_id, post_id, type, frontend_url, context, resolved_via}. |
|
|
5324
|
+
| `yootheme_builder_pages_restore` | write+non-idempotent | `layout_id`, `site_id` | Restore a TRASHED page (WordPress) or article (Joomla) — the one correct way back (pages_set_status refuses a trashed page with 409 naming this tool). WordPress: reinstates the EXACT pre-trash status (core defaults restores to draft since 5.6) and attempts to reclaim the pre-trash slug — a hand-rolled write leaves the "__trashed" suffix instead; a slug another page has since taken comes back different — check the response's frontend_url. Joomla stores no pre-trash status, so this can only set state to unpublished (visible to editors, not visitors); call yootheme_builder_pages_set_status with status:"published" afterwards to make it visible. Not currently trashed → refused (409). Find a trashed id with yootheme_builder_local_content_read({kind:"entities", status:"trash"}). |
|
|
5325
|
+
| `yootheme_builder_pages_set_status` | write+idempotent | `layout_id`, `site_id`, `status` | Change the CMS status of an EXISTING page (WordPress) or article (Joomla) — keeps its id and URL. Step (3) of the three page_save describes. Use YOUR platform's own words: WordPress publish, draft, pending, private, future; Joomla published, unpublished, archived — the other platform's word is refused (422). Status is RE-READ after the write. To trash or delete use yootheme_builder_pages_delete; a page already IN the trash is refused (409) — use yootheme_builder_pages_restore first. The response carries `cms_visibility.findable_by_visitor`: can a visitor FIND this one page via a navigation path? It reads the menu route AND the access level, which the per-row `visibility` of pages_list does not. |
|
|
5326
|
+
| `yootheme_builder_render_preview` | read+idempotent | `expect`, `layout_id`, `site_id`, `url` | Render-check a SAVED layout. Address by `layout_id` or `url`. rendered_item_wrapper_count counts the repeat-item wrappers rendered, BOUND OR NOT, so a 0 is not evidence a binding failed (per-source rows: `source_item_counts`). Distinct from pages_list `elements_count` (all tree nodes). A source the registry cannot resolve is named in `unresolved_sources`, not just a smaller count. Renders ONLY the page's own layout tree, never the theme wrapper: a MISS with `expect` proves nothing beyond the tree (adds {matched, snippet, match_conclusive, match_note, mismatch_hints, stored_but_not_rendered — the last names elements that DO store the text but rendered no rows}). A Joomla module/module_position element sharpens this — EXCLUDED (fallback_used:true) or silently empty (dropped_count:0) — but match_conclusive:false marks EVERY inconclusive miss. Reports CMS status in `cms_status`/`cms_status_note` (absent for a theme template); pages_set_status remains the tool that changes it. |
|
|
5327
|
+
| `yootheme_builder_repair_source_references` | write+idempotent | `allow_unregistered`, `dry_run`, `etag`, `field_map`, `from`, `layout_id`, `site_id`, `source_map`, `to` | Repairs binding REFERENCES to a source/type/field name in ONE template, in one ETag-locked write, after that name changed OUTSIDE the builder (a taxonomy/CPT rename, a re-published flow). Does NOT rename the source itself — sources_list stays unchanged. Pass `from`/`to` for one token, or `source_map`/`field_map` for several. Preview with dry_run:true. |
|
|
5328
|
+
| `yootheme_builder_scaffold_section` | write+non-idempotent | `column_widths`, `columns`, `etag`, `layout_id`, `site_id` | Create a VALID section → row → column hull in ONE call; returns the column path(s) for content. Only `section` may sit at the layout root; leaf/content elements are rejected anywhere except inside a `column`. Pass `columns` (1-12) or `column_widths` for an asymmetric row. Requires ETag. |
|
|
5329
|
+
| `yootheme_builder_sites_list` | read+idempotent | `include_meta`, `site_id` | List all sites configured in this multi-site MCP installation — site_id, URL, platform, DEFAULT + ACTIVE flag per row. Call first to discover site_ids. A call omitting site_id goes to `active_site_id` (set by use_site, process-wide) else to `default_site_id`. Pass `site_id` to FILTER to one site; omit it for all. IDs from any tool (layout_id, element ids, …) are PER-SITE, never global: the same id can exist on a different site naming something else entirely. Always keep an id paired with the site_id it came from. |
|
|
5330
|
+
| `yootheme_builder_sites_overview` | read+idempotent | `include_meta`, `include_pages`, `max_chars`, `site_id` | Every configured site in ONE call: reachability, Bearer validity and page counts per site, probed in parallel. Replaces sites_list plus one sites_test and one pages_list per site. A site that is down becomes a row with an `error`, never a failed call. Pass `site_id` to narrow to one site; `include_pages:false` skips the page read (one HTTP request per site less). The content/theme_template split is absent on hosts that do not report it — never reported as zero. |
|
|
2755
5331
|
| `yootheme_builder_sites_test` | read+idempotent | `site_id` | Verify connectivity to ONE site: probes /health (no auth) + /etag (auth) in parallel; returns plugin_reachable + bearer_valid. `site_id` is REQUIRED. Use sites_list to find IDs. |
|
|
2756
|
-
| `yootheme_builder_source_get_schema` | read+idempotent | `max_chars`, `name`, `site_id` | Introspect ONE data source:
|
|
2757
|
-
| `yootheme_builder_sources_list` | read+idempotent | `cursor`, `fields`, `group`, `kind`, `limit`, `max_chars`, `name_contains`, `site_id` | List
|
|
2758
|
-
| `yootheme_builder_template_set_type` | idempotent | `dry_run`, `etag`, `
|
|
2759
|
-
| `yootheme_builder_template_summary` | read+idempotent | `
|
|
2760
|
-
| `yootheme_builder_use_site` | idempotent | `probe`, `site_id` | Switch the ACTIVE site for this
|
|
5332
|
+
| `yootheme_builder_source_get_schema` | read+idempotent | `max_chars`, `name`, `site_id`, `source_name` | Introspect ONE data source: query methods + arguments + RELATIONS (e.g. Term→Posts: categoryFaqs.taxonomyCategoryFaq → field "faq"). `is_archive_context_provider` says whether the source CAN follow the current archive term; it does NOT say whether the source returns rows here — only a render answers that (element_bind_source with probe:true, or render_preview). Some arguments carry `enum_values` — pick from those when present, never invent values. Call before binding on an archive/category template. Get `name` from sources_list. |
|
|
5333
|
+
| `yootheme_builder_sources_list` | read+idempotent | `cursor`, `fields`, `group`, `include_meta`, `kind`, `limit`, `max_chars`, `name_contains`, `site_id` | List data sources/feeds available in the builder — name + label + origin + `group` per source. Call before binding an element with element_bind_source. `group` is what a source answers FROM: a "Page"-group LIST source serves the CURRENT page/archive context, so a standalone page gets zero rows from it. Each row says so in `context_note` when it applies. A long list narrows with `group` (the ORIGIN bucket: apimapper \| wordpress \| joomla \| essentials — not the per-row `group` column above), `name_contains`, or `kind`. Reach for one before paging. WHERE a source is used, site-wide: yootheme_builder_layout_query (gateway-only), OMIT layout_id. |
|
|
5334
|
+
| `yootheme_builder_template_set_type` | write+idempotent | `dry_run`, `etag`, `layout_id`, `site_id`, `type` | Reassign which archive/single a YOOtheme THEME template applies to — set its `type` condition in ONE ETag-locked write. Use after a taxonomy/CPT rename orphans a template. Site-wide + immediate — preview with dry_run:true, reversible via previous_type. THEME templates only: page:/article: refused (422). |
|
|
5335
|
+
| `yootheme_builder_template_summary` | read+idempotent | `cursor`, `include_nodes`, `layout_id`, `layout_ids`, `limit`, `site_id` | Token-efficient template overview: element counts by type, bindings, max depth, named sections — computed server-side in one call. Use before element_list/page_get_layout on a large template. `include_nodes:true` adds a `content_nodes` per-node breakdown (status/effective_visible/headings/alts), pageable via `limit`/`cursor`. `layout_ids:[...]` batches several: response carries `summaries`, `not_found` and `forbidden`. Example: yootheme_builder_template_summary({ layout_id: "default" }). |
|
|
5336
|
+
| `yootheme_builder_use_site` | write+idempotent | `probe`, `site_id` | Switch the ACTIVE site for this session — subsequent calls that omit `site_id` target it. NOTE: this default is PROCESS-WIDE — concurrent clients sharing this MCP server inherit it; pass `site_id` per call for isolation. Runs a /health + /etag probe by default; `probe:false` switches offline. `site_id` is REQUIRED. |
|
|
2761
5337
|
|
|
2762
5338
|
<!-- TOOL-CATALOG:END -->
|