diff --git a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php index 62bfc29cfc03f..344dc4209f50d 100644 --- a/src/wp-includes/interactivity-api/class-wp-interactivity-api.php +++ b/src/wp-includes/interactivity-api/class-wp-interactivity-api.php @@ -95,6 +95,43 @@ final class WP_Interactivity_API { */ private $has_processed_router_region = false; + /** + * Flag that indicates whether all the blocks rendered on the page support + * client-side navigation. + * + * It starts as `true` and it is set to `false` as soon as a block that does + * not declare support for client-side navigation is rendered. It is used to + * decide whether the server-generated style assets can be marked with the + * `data-wp-router-managed` attribute. + * + * @since 7.2.0 + * @var bool + */ + private $all_blocks_support_client_navigation = true; + + /** + * Flag that indicates whether the template enhancement output buffer started + * by core is active for the current request. + * + * It is set from the {@see 'wp_template_enhancement_output_buffer_started'} + * action. When it is `true`, the `data-wp-router-managed` attribute is added + * through the {@see 'wp_template_enhancement_output_buffer'} filter and no + * additional output buffer is needed. + * + * @since 7.2.0 + * @var bool + */ + private $template_output_buffer_started = false; + + /** + * Flag that indicates whether starting the fallback output buffer used to add + * the `data-wp-router-managed` attribute has already been considered. + * + * @since 7.2.0 + * @var bool + */ + private $router_managed_output_buffer_attempted = false; + /** * Set of script modules that can be loaded after client-side navigation. * @@ -399,11 +436,368 @@ public function register_script_modules() { * * @since 6.5.0 * @since 6.9.0 Adds support for client-side navigation in script modules. + * @since 7.2.0 Tracks the page-wide client-side navigation support and registers the hooks that decide how the + * `data-wp-router-managed` attribute is added to the server-generated style assets. */ public function add_hooks() { add_filter( 'script_module_data_@wordpress/interactivity', array( $this, 'filter_script_module_interactivity_data' ) ); add_filter( 'script_module_data_@wordpress/interactivity-router', array( $this, 'filter_script_module_interactivity_router_data' ) ); add_filter( 'wp_script_attributes', array( $this, 'add_load_on_client_navigation_attribute_to_script_modules' ) ); + + if ( ! is_admin() ) { + /* + * The tracked support is only read on the front end, so there is no + * need to inspect every rendered block in admin requests. + */ + add_filter( 'render_block_data', array( $this, 'filter_render_block_data_client_navigation_support' ) ); + + /* + * The `wp_template_enhancement_output_buffer` filter that adds the + * `data-wp-router-managed` attribute is not registered here on purpose. + * Merely having that filter registered when the template is included + * makes core start the template enhancement output buffer for every + * front-end request, which disables response streaming even on pages + * that never get the attribute. See + * `wp_should_output_buffer_template_for_enhancement()`. + * + * Instead, the filter is added from + * `WP_Interactivity_API::data_wp_router_region_processor()`, i.e. only + * once a router region has actually been processed, and + * `WP_Interactivity_API::maybe_start_router_managed_output_buffer()` + * starts a dedicated output buffer when core's one is not running. + */ + add_action( 'wp_template_enhancement_output_buffer_started', array( $this, 'mark_template_output_buffer_started' ) ); + + /* + * The lowest possible priority guarantees that nothing hooked to + * `wp_head` prints a style asset before the fallback output buffer + * starts. Marking only part of the style assets of a page would be + * worse than not marking any of them, because the router would treat + * the unmarked ones as client-injected and preserve them across every + * client-side navigation. + */ + add_action( 'wp_head', array( $this, 'maybe_start_router_managed_output_buffer' ), PHP_INT_MIN ); + } + } + + /** + * Records that the template enhancement output buffer started by core is + * active for the current request. + * + * This method is a {@see 'wp_template_enhancement_output_buffer_started'} + * action callback. + * + * @since 7.2.0 + */ + public function mark_template_output_buffer_started() { + $this->template_output_buffer_started = true; + } + + /** + * Tracks whether all the blocks rendered on the page support client-side + * navigation. + * + * This method is a `render_block_data` filter callback that only inspects the + * blocks being rendered; it always returns the parsed block unmodified. As + * soon as a block that does not declare support for client-side navigation is + * found, client-side navigation is considered unsupported for the whole page. + * + * The compatibility rules mirror the ones used by + * {@see block_core_query_disable_enhanced_pagination()}: blocks without a + * block name, i.e. freeform classic HTML, do not break compatibility, while + * named blocks require either `supports.interactivity` or + * `supports.interactivity.clientNavigation` to be `true`. + * + * @since 7.2.0 + * + * @param array $parsed_block The block being rendered. + * @return array Returns the parsed block, unmodified. + */ + public function filter_render_block_data_client_navigation_support( $parsed_block ) { + if ( ! $this->all_blocks_support_client_navigation ) { + return $parsed_block; + } + + if ( ! isset( $parsed_block['blockName'] ) ) { + return $parsed_block; + } + + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] ); + + /* + * Client side navigation can be true in two states: + * - supports.interactivity = true; + * - supports.interactivity.clientNavigation = true; + */ + $supports_client_navigation = ( isset( $block_type->supports['interactivity']['clientNavigation'] ) && true === $block_type->supports['interactivity']['clientNavigation'] ) + || ( isset( $block_type->supports['interactivity'] ) && true === $block_type->supports['interactivity'] ); + + if ( ! $supports_client_navigation ) { + $this->all_blocks_support_client_navigation = false; + } + + return $parsed_block; + } + + /** + * Adds the `data-wp-router-managed` attribute to all the style assets of the + * page. + * + * This method is a `wp_template_enhancement_output_buffer` filter callback, so + * it receives the complete server-generated markup. Working on the final + * buffer is what guarantees full-page coverage: every ` + + + +
+ + +