From fcc0058a0a31d849bd4b4eed0fda23a773ac96aa Mon Sep 17 00:00:00 2001 From: Pavol Caban Date: Tue, 4 Aug 2026 11:15:38 +0200 Subject: [PATCH] feat(core): Pattern Overrides support for Webentor blocks (release 0.15.7) WP 7.0 opened Pattern Overrides to custom blocks through a single server-side opt-in: any attribute listed in `block_bindings_supported_attributes` becomes overridable per synced-pattern instance. That one list drives the "Enable overrides" button, the Overrides panel, the editor read/write plumbing, the server-side substitution in WP_Block::render() and the contentOnly/disabled locking of instances -- so no JS was needed, core handles it generically. Opts in the content attributes of e-button (button), e-accordion and e-tab-container (title), e-image (imgId, link), e-svg (imgId), e-icon-picker (icon) and e-gallery (images), behind a new `webentor/block_bindings_supported_attributes` filter so projects can extend the map or opt their own blocks in. Also fixes a latent issue this exposed: the block render_callback discarded its $attributes argument -- the value WP guarantees carries resolved binding values for dynamic blocks -- and re-read $block->attributes instead. It now passes them into render_block_blade(), which takes an optional third argument. Two things explored and deliberately not shipped: - A `pattern/overrides` usesContext filter. Unnecessary: WP_Block::process_block_bindings() injects the source's own uses_context before calling get_value(). - An apply_block_bindings() resolver for the recursive Blade renderer, on the assumption nested blocks would miss bindings. Verified by stubbing it out that nested overrides still render correctly: render() merges bound values into $this->attributes in place and WP_Block_List caches instances, so the merge has already happened by the time our recursion reads them. Verified on test-site (WP 7.0): pattern editor controls, contentOnly/disabled modes, read and write paths (typed in-browser -- overrides land on the instance's content keyed by metadata.name, pattern original untouched), frontend after save including nested l-section > e-button and the tab nav in get_tabs_nav_data(), e-image swapping per instance, and 26/26 blocks valid with no new console errors via scripts/check-blocks-console.mjs. Documented caveat: WP keys bindings by top-level attribute name only, so e-button's single `button` object is overridden as a whole. An overridden instance keeps its own variant/size/icon and stops inheriting later design changes from the pattern -- confirmed empirically. Container blocks are overridden through their inner blocks; per-instance structure is not possible (core/block stores only ref + content, and instances serialize self-closing). Claude-Session: https://claude.ai/code/session_015gnVTZywLgQYkM6Epr2uW2 --- docs/src/.vitepress/config.ts | 1 + docs/src/compatibility-matrix.md | 1 + docs/src/guides/pattern-overrides.md | 168 ++++++++++++++++++ docs/src/reference/php-api.md | 20 +++ packages/webentor-core/CHANGELOG.md | 5 + .../Providers/WebentorCoreServiceProvider.php | 1 + packages/webentor-core/app/blocks-init.php | 24 ++- .../app/blocks-pattern-overrides.php | 66 +++++++ packages/webentor-core/composer.json | 2 +- packages/webentor-core/package.json | 2 +- ...cEs1sHu.json => editor.deps-Bd1lgXQE.json} | 4 +- .../webentor-core/public/build/manifest.json | 2 +- 12 files changed, 282 insertions(+), 14 deletions(-) create mode 100644 docs/src/guides/pattern-overrides.md create mode 100644 packages/webentor-core/app/blocks-pattern-overrides.php rename packages/webentor-core/public/build/assets/{editor.deps-mcEs1sHu.json => editor.deps-Bd1lgXQE.json} (100%) diff --git a/docs/src/.vitepress/config.ts b/docs/src/.vitepress/config.ts index 8767b18..a9869ab 100644 --- a/docs/src/.vitepress/config.ts +++ b/docs/src/.vitepress/config.ts @@ -46,6 +46,7 @@ export default defineConfig({ { text: 'Editor Components', link: '/guides/editor-components' }, { text: 'Extend Button', link: '/guides/extend-e-button' }, { text: 'Extend Section Background', link: '/guides/extend-l-section' }, + { text: 'Pattern Overrides', link: '/guides/pattern-overrides' }, { text: 'Core Frontend Components', link: '/guides/core-frontend-components' }, { text: 'Images', link: '/guides/images' }, { text: '1Password Setup', link: '/guides/1password-setup' }, diff --git a/docs/src/compatibility-matrix.md b/docs/src/compatibility-matrix.md index d9b4c34..5325710 100644 --- a/docs/src/compatibility-matrix.md +++ b/docs/src/compatibility-matrix.md @@ -4,6 +4,7 @@ | starterVersion | themeVersion | coreVersion | configsVersion | setupCliVersion | php | node | | --- | --- | --- | --- | --- | --- | --- | +| 2.1.2 | 2.1.2 | 0.15.7 | 1.1.0 | 1.1.0 | >=8.3 | >=20 | | 2.1.2 | 2.1.2 | 0.15.6 | 1.1.0 | 1.1.0 | >=8.3 | >=20 | | 2.1.2 | 2.1.2 | 0.15.5 | 1.1.0 | 1.1.0 | >=8.3 | >=20 | | 2.1.2 | 2.1.2 | 0.15.4 | 1.1.0 | 1.1.0 | >=8.3 | >=20 | diff --git a/docs/src/guides/pattern-overrides.md b/docs/src/guides/pattern-overrides.md new file mode 100644 index 0000000..6b92043 --- /dev/null +++ b/docs/src/guides/pattern-overrides.md @@ -0,0 +1,168 @@ +# Pattern Overrides + +WordPress 7.0 opened Pattern Overrides to custom blocks. A synced pattern can now +be reused across pages while individual instances override selected content — +without detaching the pattern, so design changes still propagate from the +original. + +Requires WordPress 7.0 or later. On older versions the controls simply don't +appear; nothing breaks. + +## Which blocks support it + +| Block | Overridable attributes | +| --- | --- | +| `e-button` | `button` | +| `e-accordion` | `title` | +| `e-tab-container` | `title` | +| `e-image` | `imgId`, `link` | +| `e-svg` | `imgId` | +| `e-icon-picker` | `icon` | +| `e-gallery` | `images` | + +Everything else — spacing, layout, responsive settings, query configuration — is +deliberately excluded. Those are design decisions that belong to the pattern, not +to the instance. + +Container blocks are absent from this table on purpose. Their content lives in +inner blocks, which are overridden individually — see +[Container blocks](#container-blocks-overriding-inner-content). + +## Using it + +1. Create a **synced** pattern containing one of the blocks above. +2. Select the block, open **Advanced** in the block sidebar, and click + **Enable overrides**. Give the block a name — this name is how each instance + identifies which block it is overriding, and it shows up in the pattern's + **Overrides** panel. +3. Insert the pattern into a page. Blocks with overrides enabled are editable; + every other block in the instance is locked. +4. The block toolbar gains a **Reset** action to drop an instance's override and + fall back to the pattern value. + +Override values are stored on the pattern instance (the `core/block` block's +`content` attribute), never on the pattern itself. + +## Container blocks: overriding inner content + +Blocks whose content lives in inner blocks — `e-accordion`, `e-tab-container`, +`e-accordion-group`, `e-tabs`, `l-section`, `l-flexible-container`, +`e-table-cell` — work differently, and this is the most common source of +confusion. + +**You do not enable overrides on the container.** You enable it on each block +*inside* it. The container needs no support of its own; only the leaf blocks that +actually hold content do. This mirrors core, where you override the paragraph +inside a `core/group`, never the group. + +So for an accordion panel containing a heading, a paragraph and a CTA: + +```html + + + +

Heading

+ + + +

Body copy

+ + + + + +

Fine print

+ + +``` + +In practice you do this through the UI: expand the pattern in the List View, +select each inner block, and enable overrides on it. Every named block then shows +up in the pattern's **Overrides** panel and in the instance's **Content** panel. + +The accordion's own `title` is a separate override from its inner blocks — enable +both if editors should be able to change the panel label *and* its content. + +### What cannot be overridden: structure + +Per-instance **structure** is not possible, and this is a WordPress +architectural limit rather than something Webentor can add: + +- `core/block` has only two attributes, `ref` and `content` (a map of attribute + overrides). There is nowhere to store a per-instance block tree. +- A pattern instance serializes as a self-closing block with no inner markup: + ``. The inner blocks are always read + from the pattern post. +- Accordingly, the editor blocks insertion inside an instance and discards any + structural change. + +Concretely: one synced pattern **cannot** show five accordion items on one page +and two on another. Content varies per instance; the number, order and type of +blocks do not. + +If you need that, pick the option that matches the real requirement: + +| Need | Approach | +| --- | --- | +| Same items, different copy per page | Pattern overrides on inner blocks (above) | +| A fixed maximum, with some items hidden per page | Over-provision the pattern and expose a bindable hide flag on the item block. `e-button` already supports this via `button.showButton`, since the whole object is bound | +| Genuinely different structure per page | An **unsynced** pattern — full freedom, but design changes no longer propagate | +| Fixed structure, freely editable content, no central sync | A container with `templateLock: 'contentOnly'` in the page itself | +| A repeating list of records | Model it as content: a CPT with ACF fields plus `e-query-loop`. If the "pattern" is really data, this is the correct architecture and everything above is a workaround | + +## Caveat: object attributes are overridden as a whole + +WordPress keys bindings by top-level attribute name — there is no support for +sub-paths like `button.title`. Since `e-button` keeps all of its settings in a +single `button` object attribute, an override stores **the entire object**, +including the design props (`variant`, `size`, `icon`, `htmlElement`). + +The practical consequence: once an instance has overridden a button, later design +changes to that button in the pattern original no longer reach that instance. Its +title and URL are its own, and so is its variant. + +If a pattern's button styling needs to stay centrally controlled, keep overrides +off for that button and let editors change copy in the pattern instead — or use +separate patterns per variant. `e-accordion` and `e-tab-container` use flat string +attributes and are not affected. + +## Extending the supported attributes + +The list is a filter, so a project can expose more attributes on core blocks or +opt its own blocks in: + +```php +add_filter('webentor/block_bindings_supported_attributes', function (array $map): array { + // Let editors override the Section background image per instance. + $map['webentor/l-section'] = ['img']; + + // Opt a theme block in. + $map['webentor/my-testimonial'] = ['quote', 'author']; + + return $map; +}); +``` + +Only list content attributes, and prefer flat scalar attributes over objects for +anything new — see the caveat above. + +Because Pattern Overrides and Block Bindings share this list, every attribute +here also becomes connectable to other sources such as post meta via the block's +**Attributes** panel. That is usually a bonus, but it is a reason not to expose +object attributes that no other source could sensibly produce. + +## Notes for block authors + +Webentor blocks are dynamic (`save: () => null` plus a Blade `view.blade.php`), +which is the easy case for bindings: WordPress resolves bound values before +calling the render callback, so `$attributes` in your Blade view already holds +the override. No `WP_HTML_Tag_Processor` work and no selectors are needed. + +Do not add container blocks to the map. A block whose content is inner blocks has +no attribute worth binding — its children are overridden individually instead, and +adding it would only put a misleading entry in the **Attributes** panel. See +[Container blocks](#container-blocks-overriding-inner-content). diff --git a/docs/src/reference/php-api.md b/docs/src/reference/php-api.md index 95d2e34..616807c 100644 --- a/docs/src/reference/php-api.md +++ b/docs/src/reference/php-api.md @@ -255,6 +255,26 @@ get_msls_languages(): array ## Filters +### `webentor/block_bindings_supported_attributes` + +Controls which block attributes are exposed to Block Bindings, and therefore to +Pattern Overrides (WordPress 7.0+). Keys are block names, values are lists of +top-level attribute names. See [Pattern Overrides](/guides/pattern-overrides). + +```php +add_filter('webentor/block_bindings_supported_attributes', function (array $map): array { + $map['webentor/l-section'] = ['img']; // override Section background per instance + $map['webentor/my-block'] = ['quote']; // opt a theme block in + + return $map; +}); +``` + +List content attributes only. Object attributes are overridden as a whole, so +prefer flat scalar attributes for anything new. + +--- + ### `webentor/skip_render_block_blade` Allows skipping the Blade rendering pipeline for a specific block. Return `true` diff --git a/packages/webentor-core/CHANGELOG.md b/packages/webentor-core/CHANGELOG.md index 12a6073..1198ae1 100644 --- a/packages/webentor-core/CHANGELOG.md +++ b/packages/webentor-core/CHANGELOG.md @@ -1,5 +1,10 @@ # Webentor Core Changelog +## 0.15.7 + +- **Pattern Overrides support for Webentor blocks (WordPress 7.0).** WP 7.0 opened Pattern Overrides to custom blocks via a single server-side opt-in, so `e-button` (`button`), `e-accordion` and `e-tab-container` (`title`), `e-image` (`imgId`, `link`), `e-svg` (`imgId`), `e-icon-picker` (`icon`) and `e-gallery` (`images`) now expose their content attributes to Block Bindings — a synced pattern can be reused across pages with per-instance content while the design keeps propagating from the original. The list is filterable via the new `webentor/block_bindings_supported_attributes` filter, and because Pattern Overrides and Block Bindings share it, these attributes also become connectable to post meta. Container blocks are overridden through their inner blocks rather than the container itself; see the new Pattern Overrides guide in the docs. One caveat: WordPress keys bindings by top-level attribute name only, so `e-button`'s single `button` object is overridden as a whole — once an instance overrides a button it keeps its own `variant`/`size`/`icon` and stops inheriting later design changes from the pattern. +- **Pass resolved attributes into Blade views.** The block `render_callback` discarded its `$attributes` argument and re-read `$block->attributes` instead, bypassing the value WordPress guarantees carries resolved Block Bindings values for dynamic blocks. `render_block_blade()` now takes them as an optional third argument and the callback passes them through. No behavior change for blocks without bindings. + ## 0.15.6 - **Give the `l-section` background video an accessible name.** The `