-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Server Side Render component. #5602
Conversation
Question: at this moment the change in attributes is detected by using |
...attributes, | ||
_wpnonce: wpApiSettings.nonce, | ||
} ); | ||
return window.fetch( apiURL, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May want to use wp.apiRequest
for consistency. See #5253.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think more of a guide on using would help with that. I was working on another PR that used wp.apiRequest
and it was leading to test failures (maybe it was how #4710 used in redux connect
) so I removed, tests started passing and I could move forward. The only reason my modifications caused issues using wp.apiRequest
as far as I could tell was that the tests themselves were not being passed enough context.
Render core/latest-posts preview. | ||
```jsx | ||
<ServerSideRender | ||
block="core/latest-posts" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably register another block for something that makes more sense server-rendered (maybe tag cloud widget?) to use as the example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could add Archives
block from #5495. It's otherwise ready except for the preview which was waiting for SSR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mtias For usage example I merged the code from this PR to #5495 which adds Archives
block -- thought that this way this PR wouldn't have to wait for potential fixes for the Archives
block.
So basically #5495 now includes adding Archives block + SSR component with the idea that SSR component could be merged separately first.
Let me know if that's OK or if you'd prefer having the Archives block added here under this PR.
cc @westonruter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mtias Updated the description with added screenshots of Archives block based on SSR. Let me know if there's anything else you see in the PR that requires addressing.
* | ||
* @see WP_REST_Controller | ||
*/ | ||
class WP_REST_Blocks_Renderer_Controller extends WP_REST_Controller { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be singular? Block_Renderer...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yah, same with the endpoint route.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in c4abc69
return new WP_Error( 'rest_block_invalid_name', __( 'Invalid block name.', 'gutenberg' ), array( 'status' => 404 ) ); | ||
} | ||
|
||
$atts = $this->prepare_attributes( $request->get_params() ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we have $block
here, this means that we have $block->attributes
which can be used with rest_validate_value_from_schema()
and rest_sanitize_value_from_schema()
. For example:
$atts = array();
$schema = $block->attributes;
foreach ( $request->get_params() as $key => $value ) {
if ( ! isset( $schema[ $key ] ) ) {
return new WP_Error( 'rest_unrecognized_block_attribute' /* ... */ );
}
$validity = rest_validate_value_from_schema( $value, $schema[ $key ], $key );
if ( is_wp_error( $validity ) ) {
return $validity;
}
$atts[ $key ] = rest_sanitize_value_from_schema( $value, $schema[ $key ] );
}
This should eliminate the need for prepare_attributes
entirely.
public function register_routes() { | ||
|
||
// @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. | ||
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<name>[\w-]+\/[\w-]+)', array( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One idea: instead of letting name
be a parameter, we could register over all of the registered blocks and create an endpoint for each one. This could then allow for the block's attributes schema to be used as an attributes
object arg in the args
here, and it would eliminate the need for prepare_attributes
logic in get_item_output
. (See https://core.trac.wordpress.org/ticket/38583)
This would allow for a list of all the server-side renderable blocks to be discovered by looking at the REST API schema. Only the blocks returned by get_dynamic_block_names()
would be included.
'title' => 'blocks-renderer', | ||
'type' => 'object', | ||
'properties' => array( | ||
'output' => array( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the rendered
name would be more in-line with the rest of the REST API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in c4abc69
} | ||
|
||
return ( | ||
<RawHTML key="html">{ response }</RawHTML> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use SandBox
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess not because then styles won't be applied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@westonruter Sandbox doesn't stop styles being applied (If I understand correctly it ensures no css / js bleeds into admin experience). It's used in #4710. Should work on such block-specifics defer to this once it's released?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean the CSS from the editor. Otherwise, the CSS has to be re-printed in the iframe.
} | ||
|
||
componentWillReceiveProps( nextProps ) { | ||
if ( JSON.stringify( nextProps ) !== JSON.stringify( this.props ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about using isEqual
from lodash?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
I was going to comment that there was room to figure out how aggressive this component should be with refetches and ask if a shallow equality check could make any sense. However, by design, block attributes should (until abused) retain a very simple shape.
isEqual
should do it. -
More importantly, it's not clear to me why there is state synchronization at all. Perhaps this is a relic of a previous solution?
response
appears to be the only datum requiring local state. Component props should be fed directly intogetOutput
. If the goal is to trigger a refetch and a rerender, the following should be enough:
componentWillReceiveProps( nextProps ) {
if ( ! isEqual( nextProps, this.props ) ) {
this.getOutput();
}
}
…t of the REST API * Singularize "blocks renderer" to "block renderer" * Rename 'output' property to 'rendered' property. * Re-use get_item_permissions_check and get_item method names.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. | ||
*/ | ||
public function get_item( $request ) { | ||
if ( ! isset( $request['name'] ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex for name
in the register_rest_route()
call means $request['name']
should never be not set.
I'm working on refactoring how the routes are registered. Will be pushing up within the hour. |
* Supply block attributes schema as endpoint schema. * Introduce attributes endpoint property and let REST API schema validate and sanitize them. * Ensure that attribute values are sanitized in addition to validated.
$registry = WP_Block_Type_Registry::get_instance(); | ||
$block = $registry->get_registered( $request['name'] ); | ||
$data = array( | ||
'rendered' => $block->render( $request->get_param( 'attributes' ) ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@westonruter This isn't working with the current SSR component code since the attributes are all sent as separate params and not as one attributes
array:
const apiURL = addQueryArgs( '/gutenberg/v1/block-renderer/' + block, {
...attributes,
_wpnonce: wpApiSettings.nonce,
} );
Just sending attributes: attributes
as param would fail the request, is there an existing method that creates an acceptable query string for the API (e.g. ?attributes[key1]=value1&attributes[key2]=value2...
etc? If not then perhaps we could still pass all the attributes as separate params and modify the endpoint accordingly. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question. I think it's important for attributes
to be under an attributes
query parameter because that will prevent conflicts with block attributes. Like if a block has a name
or a context
these will conflict with the REST API args. It seems like this should be allowed:
const apiURL = addQueryArgs( '/gutenberg/v1/block-renderer/' + block, {
attributes,
_wpnonce: wpApiSettings.nonce,
} );
In other words, I think addQueryArgs
needs to be able to properly convert arg values that are objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't find an existing method at this moment, apparently query-string is intentionally not supporting nested attributes and suggests sending the object as a JSON string.
Added a custom method for now to the class to put together the query string supporting objects (ServerSideRender.getQueryUrlFromObject
).
|
||
ServerSideRender component is used for server-side rendering preview in Gutenberg editor, specifically for dynamic blocks. Server-side rendering in a block's `edit` function should be limited for blocks which are heavily dependent on (existing) PHP rendering logic that is heavily intertwined with data, such as when there are no endpoints available. | ||
|
||
New blocks should be built in conjunction with any necessary REST API endpoints so that JavaScript can be used for rendering client-side in the `edit` function for the best user experience, instead of relying using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint so that both the client-side JS and server-side PHP logic should require a mininal amount of differences. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/relying using/relying on/
ServerSideRender | ||
======= | ||
|
||
ServerSideRender component is used for server-side rendering preview in Gutenberg editor, specifically for dynamic blocks. Server-side rendering in a block's `edit` function should be limited for blocks which are heavily dependent on (existing) PHP rendering logic that is heavily intertwined with data, such as when there are no endpoints available. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's important to stress two other conditions to prefer SSR, more significant, I dare say, than the logic/data entanglement, since that isn't of itself insurmountable:
- A lack of potential to take some existing widget-like functionality and improve its user-facing editing experience. For instance, even Archive could benefit from a full JS-driven editing mode: similarly to how Latest Posts offers presentation tweaks (e.g., whether to display as a grid, to show publishing date) or beyond (e.g., allowing tweaking an entry's title, changing its date or otherwise moving it out of that particular archive view).
- An unwillingness to create a full JS experience for a block considered legacy.
The ultimate point, perhaps, is that—however useful—SSR should be regarded as a fallback.
|
||
## Usage | ||
|
||
Render core/latest-posts preview. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/latest-posts/archives/
|
||
## Output | ||
|
||
Output is using the `render_callback` set when defining the block. For example if `block="core/latest-posts"` as in the example then the output will match `render_callback` output of that block. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/latest-posts/archives/
} | ||
|
||
componentWillReceiveProps( nextProps ) { | ||
if ( JSON.stringify( nextProps ) !== JSON.stringify( this.props ) ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
I was going to comment that there was room to figure out how aggressive this component should be with refetches and ask if a shallow equality check could make any sense. However, by design, block attributes should (until abused) retain a very simple shape.
isEqual
should do it. -
More importantly, it's not clear to me why there is state synchronization at all. Perhaps this is a relic of a previous solution?
response
appears to be the only datum requiring local state. Component props should be fed directly intogetOutput
. If the goal is to trigger a refetch and a rerender, the following should be enough:
componentWillReceiveProps( nextProps ) {
if ( ! isEqual( nextProps, this.props ) ) {
this.getOutput();
}
}
} | ||
} | ||
|
||
getOutput() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is a name like fetch
more apt?
constructor( props ) { | ||
super( props ); | ||
this.state = { | ||
response: {}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the empty object as a representation of missing state a pattern from somewhere else? I'd expect null
or undefined
to be a better fit. This works well with the fact that the request resolution already checks for response truthiness:
if ( response && response.output ) {
Meaning that we can easily distinguish the three possible states, e.g.:
- no content:
response === null
- content missing:
response !== null && isEmpty( response )
- count found:
! isEmpty( response )
|
||
/** | ||
* NA. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd drop the placeholder comments, since they can throw our linter off and later slip through review.
* @access public | ||
*/ | ||
public function __construct() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per the project's style guide, function bodies shouldn't start with an empty line. The same applies to other occurrences in the diff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Looking pretty good on my end. Left some minor nit picks and then I think we're good for merge.
* @access public | ||
*/ | ||
public function __construct() { | ||
// @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we ignore this in the ruleset?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an exception for this file within 6d1ee65.
continue; | ||
} | ||
|
||
// @codingStandardsIgnoreLine - PHPCS mistakes $this->namespace for the namespace keyword. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added an exception for this file within 6d1ee65.
public function get_item( $request ) { | ||
global $post; | ||
|
||
$post_ID = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change the casing of $post_ID
to $post_id
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed within 6d1ee65.
} | ||
|
||
$data = array( | ||
'rendered' => $block->render( $request->get_param( 'attributes' ) ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may also be passing the full block content to the render callback in #6239
Rather than needing to remember passing all supported arguments to the render()
callback, it would be useful to have a helper function for this.
$response = $this->server->dispatch( $request ); | ||
|
||
$this->assertErrorResponse( 'gutenberg_block_cannot_read', $response, rest_authorization_required_code() ); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we include additional tests for test_get_item_without_permissions_invalid_post()
and test_get_item_without_permissions_cannot_edit_post()
, just to make sure we have those code paths covered?
Re-requesting a review from @mcsf, because I'd like to make sure his prior comments were addressed to his satisfaction. I've created a new issue for batch previews in #6381 @westonruter Can you speak briefly to how server block validation (#4063) relates to this endpoint? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good to go. I'll merge it shortly.
const path = '/gutenberg/v1/block-renderer/' + block + '?context=edit&' + this.getQueryUrlFromObject( { attributes } ); | ||
|
||
return wp.apiRequest( { path: path } ).then( ( response ) => { | ||
if ( response && response.rendered ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's nothing to say that the component is still mounted by the time this response is received, so attempting to setState
will result in errors. If anything asynchronous is to occur in a component, it must be complemented by a cancellation in componentWillUnmount
. It's a bit burdensome, but also speaks to the bigger concern that handling asynchronous behaviors in components is knowingly hard to maintain (and why extracting these behaviors out into e.g. data
/core-data
avoids this burden being placed on the component author and enables reuse)..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed within #6571.
const response = this.state.response; | ||
if ( ! response || ! response.length ) { | ||
return ( | ||
<div key="loading" className="wp-block-embed is-loading"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For what reason do we need a key
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced with Placeholder
within #6571.
return ( | ||
<div key="loading" className="wp-block-embed is-loading"> | ||
|
||
<p>{ __( 'Loading...' ) }</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need both a div
and a p
, or would one or the other have been sufficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replaced with Placeholder
within #6571.
getQueryUrlFromObject( obj, prefix ) { | ||
return map( obj, ( paramValue, paramName ) => { | ||
const key = prefix ? prefix + '[' + paramName + ']' : paramName, | ||
value = obj[ paramName ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are paramValue
and value
the same?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed within #6571.
return map( obj, ( paramValue, paramName ) => { | ||
const key = prefix ? prefix + '[' + paramName + ']' : paramName, | ||
value = obj[ paramName ]; | ||
return isObject( paramValue ) ? this.getQueryUrlFromObject( value, key ) : |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe not intentional:
n_ > _.isObject( [] )
true
n_ > _.isObject( function() {} )
true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed within #6571.
} | ||
|
||
fetch( props ) { | ||
this.setState( { response: null } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should check if this.state.response
is already null
here, as it will be when this function is called by componentDidMount
and we'll wastefully cause an immediate re-render (setState
always incurs a render unless prevented by shouldComponentUpdate
).
Calling setState() in [componentDidMount] will trigger an extra rendering, but it will happen before the browser updates the screen. This guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state. Use this pattern with caution because it often causes performance issues. It can, however, be necessary for cases like modals and tooltips when you need to measure a DOM node before rendering something that depends on its size or position.
https://reactjs.org/docs/react-component.html#componentdidmount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed within #6571.
|
||
const path = '/gutenberg/v1/block-renderer/' + block + '?context=edit&' + this.getQueryUrlFromObject( { attributes } ); | ||
|
||
return wp.apiRequest( { path: path } ).then( ( response ) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is available a mapping to @wordpress/api-request
as a dependency to avoid referencing the global here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed within #6571.
|
||
render() { | ||
const response = this.state.response; | ||
if ( ! response || ! response.length ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we check response.length
? Is it desirable that if an empty response was received from the server, we show a loading indicator forever?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed within #6571, let me know if it looks OK now.
} ); | ||
} | ||
|
||
getQueryUrlFromObject( obj, prefix ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds like a wordpress/url
package utility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds like a wordpress/url package utility.
In fact, it could probably be built-in as the default behavior for the existing addQueryArgs
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addQueryArgs
works well but not for nested attributes, thus the need for creating a custom method. Couldn't find an existing utility that would put together URL in case of nested params, if you happen to know any, happy to change it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the behavior is needed, it could be added to the existing function via pull request to the packages repository.
const response = this.state.response; | ||
if ( ! response || ! response.length ) { | ||
return ( | ||
<div key="loading" className="wp-block-embed is-loading"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't reuse classes but components. If there's something in the way the embed blocks handles the loading state that is useful, it should be extracted in a separate UI component.
They could be, yes. My hope would be that the feedback wouldn't simply be ignored by the original participants though. |
@aduth in my experience, many comments post-merge get lost. So let's capture actionable feedback and get that into a new issue so someone can pick up that work. Feel free to emoji-react on certain comments that are actionable and I can go through and pull all those into a new issue if you'd like. |
@jeffpaul @aduth Looks like @westonruter found a potential bug with SSR component when using the Columns block. Perhaps the post-merge feedback could be addressed together with that. |
Yes, I don't think an issue is needed in this particular case as the comments with the code are easier to follow — however you prefer, @miina. |
Description
Fixes #780: Adds
ServerSideRender
component and/gutenberg/v1/blocks-renderer/:block
API endpoints for dynamic blocks which together enable server side rendering for dynamic blocks in Gutenberg editor. API endpoint is leveraging the already existingrender_callback
of the block and returns identical HTML output.Types of changes
ServerSideRender
component which can be used for dynamic blocks for server side render. Usage example:<ServerSideRender block="core/latest-posts" attributes={ attributes } />
./gutenberg/v1/blocks-renderer/:block
API endpoints, expects the block to be in the format ofnamespace/block-name
.How Has This Been Tested?
Has been tested functionally with
core/archives
and alsophpunit
.Screenshots:
Archives block with
ServerSideRender
component:Checklist:
Thoughts are welcome!