Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
1 change: 1 addition & 0 deletions docs/src/compatibility-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
168 changes: 168 additions & 0 deletions docs/src/guides/pattern-overrides.md
Original file line number Diff line number Diff line change
@@ -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
<!-- wp:webentor/e-accordion {"title":"FAQ","metadata":{"name":"faq-1-title",
"bindings":{"__default":{"source":"core/pattern-overrides"}}}} -->

<!-- wp:heading {"metadata":{"name":"faq-1-heading",
"bindings":{"__default":{"source":"core/pattern-overrides"}}}} -->
<h2>Heading</h2>
<!-- /wp:heading -->

<!-- wp:paragraph {"metadata":{"name":"faq-1-body",
"bindings":{"__default":{"source":"core/pattern-overrides"}}}} -->
<p>Body copy</p>
<!-- /wp:paragraph -->

<!-- wp:webentor/e-button {"metadata":{"name":"faq-1-cta",
"bindings":{"__default":{"source":"core/pattern-overrides"}}}} /-->

<!-- A block with no override stays identical in every instance. -->
<!-- wp:paragraph --><p>Fine print</p><!-- /wp:paragraph -->

<!-- /wp:webentor/e-accordion -->
```

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:
`<!-- wp:block {"ref":123,"content":{…}} /-->`. 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).
20 changes: 20 additions & 0 deletions docs/src/reference/php-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
5 changes: 5 additions & 0 deletions packages/webentor-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 `<video>` added in 0.15.5 rendered with no `title` or `aria-label` (`<video>` has no `alt`). It now labels itself from the attachment's alt text, falling back to the attachment title, and emits both; with neither set it is treated as decorative and gets `aria-hidden="true"`. New `webentor/l-section/video_label` filter overrides the label. Frontend template only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function boot(): void
require_once WEBENTOR_CORE_PHP_PATH . '/app/blocks-init.php';
require_once WEBENTOR_CORE_PHP_PATH . '/app/blocks-settings.php';
require_once WEBENTOR_CORE_PHP_PATH . '/app/blocks-migration.php';
require_once WEBENTOR_CORE_PHP_PATH . '/app/blocks-pattern-overrides.php';
require_once WEBENTOR_CORE_PHP_PATH . '/app/CloudinaryClient.php';
require_once WEBENTOR_CORE_PHP_PATH . '/app/i18n.php';
require_once WEBENTOR_CORE_PHP_PATH . '/app/images.php';
Expand Down
24 changes: 15 additions & 9 deletions packages/webentor-core/app/blocks-init.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ function register_block_from_filename($filename)

// Only add the render callback if the block has a file called markdown.php in it's directory
$block_options['render_callback'] = function ($attributes, $content, $block) {
return render_block_blade($block);
// Pass $attributes through rather than re-reading $block->attributes: this is
// the argument WP guarantees carries resolved Block Bindings values
// (Pattern Overrides, post meta, …) for dynamic blocks.
return render_block_blade($block, null, $attributes);
};

$registered_block = register_block_type_from_metadata($block_folder, $block_options);
Expand Down Expand Up @@ -181,11 +184,12 @@ function ($block_list, $block_editor_context) use ($block_slug, $registered_bloc
/**
* Render blade view from block object and also handle inner blocks.
*
* @param \WP_Block $block
* @param \WP_Block $parent_block
* @param \WP_Block $block
* @param \WP_Block $parent_block
* @param array|null $attributes Resolved attributes, defaults to the block's own.
* @return string
*/
function render_block_blade($block, $parent_block = null)
function render_block_blade($block, $parent_block = null, $attributes = null)
{
// We don't need to render blocks in admin or while saving, this will dramatically improve Gutenberg loading time
if (is_admin() || wp_doing_ajax() || (!empty($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && defined('REST_REQUEST'))) {
Expand All @@ -209,21 +213,23 @@ function render_block_blade($block, $parent_block = null)
}
}

$attributes = $attributes ?? $block->attributes;

// Create ID HTML attribute from anchor value
$anchor = '';
if (!empty($block->attributes['anchor'])) {
$anchor = 'id="' . esc_attr($block->attributes['anchor']) . '" ';
if (!empty($attributes['anchor'])) {
$anchor = 'id="' . esc_attr($attributes['anchor']) . '" ';
}

$block_name = $block->parsed_block['blockName']; // in format "namespace/block-name"
$block_slug = substr($block_name, strrpos($block_name, "/") + 1); // get only "block-name"

// Single call to avoid duplicate attribute iteration
$block_classes_result = prepareBlockClassesFromSettings($block->attributes, $block, $parent_block);
$block_classes_result = prepareBlockClassesFromSettings($attributes, $block, $parent_block);
$classes = $block_classes_result['classes'];
$classes_by_property = $block_classes_result['classes_by_property'];
$all_classes_by_property = $block_classes_result['all_classes_by_property'];
$bg_classes = prepareBgBlockClassesFromSettings($block->attributes);
$bg_classes = prepareBgBlockClassesFromSettings($attributes);

$additional_data = [];

Expand All @@ -249,7 +255,7 @@ function render_block_blade($block, $parent_block = null)
// check if file exists
if (\Roots\view()->exists($view_path)) {
$block_content = \Roots\view($view_path, [
'attributes' => $block->attributes,
'attributes' => $attributes,
'innerBlocksContent' => $inner_blocks_html,
'anchor' => $anchor,
'block_classes' => $classes,
Expand Down
66 changes: 66 additions & 0 deletions packages/webentor-core/app/blocks-pattern-overrides.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Webentor\Core;

/**
* Block Bindings / Pattern Overrides support for Webentor blocks.
*
* WP 7.0 made Pattern Overrides generic: an attribute listed in
* `block_bindings_supported_attributes` becomes overridable per synced-pattern
* instance, and bindable to any other registered source (post meta, term data,
* custom sources). Everything else follows from that list — the "Enable
* overrides" button, the editor read/write plumbing, and the server-side
* attribute substitution in WP_Block::render().
*
* @see https://make.wordpress.org/core/2026/03/16/pattern-overrides-in-wp-7-0-support-for-custom-blocks/
*/

/**
* Attributes each Webentor block exposes to Block Bindings.
*
* Only content attributes belong here.
*
* Bindings are keyed by top-level attribute name — WP has no sub-path support —
* so an object attribute is always overridden as a whole. For `e-button` that
* means an instance override stores the full `button` object, including the
* design props (variant/size/icon). Design changes made in the pattern original
* afterwards no longer reach that instance. Splitting `button` into flat
* attributes would avoid this, but would change the attribute contract every
* consumer theme's Blade views and filters build on.
*
* @return array<string, string[]> Block name => supported attribute names.
*/
function get_block_bindings_supported_attributes_map()
{
/**
* Filters which block attributes Webentor exposes to Block Bindings.
*
* @param array<string, string[]> $map Block name => supported attribute names.
*/
return apply_filters('webentor/block_bindings_supported_attributes', [
'webentor/e-accordion' => ['title'],
'webentor/e-button' => ['button'],
'webentor/e-gallery' => ['images'],
'webentor/e-icon-picker' => ['icon'],
'webentor/e-image' => ['imgId', 'link'],
'webentor/e-svg' => ['imgId'],
'webentor/e-tab-container' => ['title'],
]);
}

/**
* Opt Webentor blocks into Block Bindings / Pattern Overrides.
*
* @param string[] $attributes Attributes already supported for this block type.
* @param string $block_type Block name.
* @return string[]
*/
add_filter('block_bindings_supported_attributes', function ($attributes, $block_type) {
$map = get_block_bindings_supported_attributes_map();

if (empty($map[$block_type])) {
return $attributes;
}

return array_values(array_unique(array_merge((array) $attributes, $map[$block_type])));
}, 10, 2);
2 changes: 1 addition & 1 deletion packages/webentor-core/composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "webikon/webentor-core",
"version": "0.15.6",
"version": "0.15.7",
"license": "MIT",
"description": "Webentor Core package",
"homepage": "https://webikon.sk",
Expand Down
2 changes: 1 addition & 1 deletion packages/webentor-core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@webikon/webentor-core",
"homepage": "https://webikon.sk",
"version": "0.15.6",
"version": "0.15.7",
"description": "Core functionality and useful utilities for Webentor Stack",
"license": "MIT",
"author": "Webikon s.r.o.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
[
"wp-i18n",
"wp-block-editor",
"wp-blocks",
"wp-i18n",
"wp-components",
"wp-hooks",
"wp-compose",
"wp-data",
"wp-element",
"wp-hooks",
"wp-html-entities",
"wp-primitives"
]
Loading
Loading