From f9a5fbe04abfa0dce52a215cd61e088b1a70f860 Mon Sep 17 00:00:00 2001 From: Daniel Paz Date: Thu, 4 Jun 2026 13:47:59 +0200 Subject: [PATCH] Add support for prerender_until_script mode and origin trial token --- plugins/speculation-rules/hooks.php | 13 +++ plugins/speculation-rules/plugin-api.php | 8 +- plugins/speculation-rules/settings.php | 87 +++++++++++++------ .../test-speculation-rules-plugin-api.php | 16 ++++ .../tests/test-speculation-rules-settings.php | 39 ++++++--- .../test-speculation-rules-wp-core-api.php | 15 ++++ .../tests/test-speculation-rules.php | 50 +++++++++++ plugins/speculation-rules/wp-core-api.php | 2 +- 8 files changed, 189 insertions(+), 41 deletions(-) diff --git a/plugins/speculation-rules/hooks.php b/plugins/speculation-rules/hooks.php index 200dba88a7..2e73c0acfc 100644 --- a/plugins/speculation-rules/hooks.php +++ b/plugins/speculation-rules/hooks.php @@ -86,3 +86,16 @@ function plsr_render_generator_meta_tag(): void { echo '' . "\n"; } add_action( 'wp_head', 'plsr_render_generator_meta_tag' ); + +/** + * Prints the Origin Trial meta tag if a token is configured. + * + * @since 1.7.0 + */ +function plsr_print_origin_trial_meta_tag(): void { + $option = plsr_get_stored_setting_value(); + if ( '' !== $option['origin_trial_token'] && 'prerender_until_script' === $option['mode'] && plsr_is_speculative_loading_enabled() ) { + echo '' . "\n"; + } +} +add_action( 'wp_head', 'plsr_print_origin_trial_meta_tag', 1 ); diff --git a/plugins/speculation-rules/plugin-api.php b/plugins/speculation-rules/plugin-api.php index 9c1fada0c7..88b0ee37fe 100644 --- a/plugins/speculation-rules/plugin-api.php +++ b/plugins/speculation-rules/plugin-api.php @@ -63,7 +63,7 @@ function plsr_get_speculation_rules(): array { * * @param string[] $href_exclude_paths Additional paths to disable speculative prerendering for. The base exclude paths, * such as for wp-admin, cannot be removed. - * @param string $mode Mode used to apply speculative prerendering. Either 'prefetch' or 'prerender'. + * @param string $mode Mode used to apply speculative prerendering. Either 'prefetch', 'prerender', or 'prerender_until_script'. */ $href_exclude_paths = (array) apply_filters( 'plsr_speculation_rules_href_exclude_paths', array(), $mode ); @@ -119,6 +119,12 @@ static function ( string $href_exclude_path ) use ( $prefixer ): string { 'selector_matches' => '.no-prerender', ), ); + } elseif ( 'prerender_until_script' === $mode ) { + $rules[0]['where']['and'][] = array( + 'not' => array( + 'selector_matches' => '.no-prerender, .no-prerender_until_script', + ), + ); } return array( $mode => $rules ); diff --git a/plugins/speculation-rules/settings.php b/plugins/speculation-rules/settings.php index 665de3746e..669a9109ae 100644 --- a/plugins/speculation-rules/settings.php +++ b/plugins/speculation-rules/settings.php @@ -19,12 +19,13 @@ * * @since 1.0.0 * - * @return array{ prefetch: string, prerender: string } Associative array of `$mode => $label` pairs. + * @return array{ prefetch: string, prerender: string, prerender_until_script: string } Associative array of `$mode => $label` pairs. */ function plsr_get_mode_labels(): array { return array( - 'prefetch' => _x( 'Prefetch', 'setting label', 'speculation-rules' ), - 'prerender' => _x( 'Prerender', 'setting label', 'speculation-rules' ), + 'prefetch' => _x( 'Prefetch', 'setting label', 'speculation-rules' ), + 'prerender' => _x( 'Prerender', 'setting label', 'speculation-rules' ), + 'prerender_until_script' => _x( 'Prerender until script', 'setting label', 'speculation-rules' ), ); } @@ -69,7 +70,7 @@ function plsr_get_authentication_labels(): array { */ function plsr_get_field_description( string $field ): string { $descriptions = array( - 'mode' => __( 'Prerendering will lead to faster load times than prefetching. However, in case of interactive content, prefetching may be a safer choice.', 'speculation-rules' ), + 'mode' => __( 'Prerendering will lead to faster load times than prefetching. Prerender until script starts rendering but pauses before executing script elements. Prefetching is the safest choice for interactive content.', 'speculation-rules' ), 'eagerness' => __( 'The eagerness setting defines the heuristics based on which the loading is triggered. "Eager" will have the minimum delay to start speculative loads, "Conservative" increases the chance that only URLs the user actually navigates to are loaded.', 'speculation-rules' ), 'authentication' => sprintf( /* translators: %s: URL to persistent object cache documentation */ @@ -85,19 +86,21 @@ function plsr_get_field_description( string $field ): string { * * @since 1.0.0 * - * @return array{ mode: 'prerender', eagerness: 'moderate', authentication: 'logged_out' } { + * @return array{ mode: 'prerender', eagerness: 'moderate', authentication: 'logged_out', origin_trial_token: string } { * Default setting value. * - * @type string $mode Mode. - * @type string $eagerness Eagerness. - * @type string $authentication Authentication. + * @type string $mode Mode. + * @type string $eagerness Eagerness. + * @type string $authentication Authentication. + * @type string $origin_trial_token Origin trial token. * } */ function plsr_get_setting_default(): array { return array( - 'mode' => 'prerender', - 'eagerness' => 'moderate', - 'authentication' => 'logged_out', + 'mode' => 'prerender', + 'eagerness' => 'moderate', + 'authentication' => 'logged_out', + 'origin_trial_token' => '', ); } @@ -106,12 +109,13 @@ function plsr_get_setting_default(): array { * * @since 1.4.0 * - * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager', authentication: 'logged_out'|'logged_out_and_admins'|'any' } { + * @return array{ mode: 'prefetch'|'prerender'|'prerender_until_script', eagerness: 'conservative'|'moderate'|'eager', authentication: 'logged_out'|'logged_out_and_admins'|'any', origin_trial_token: string } { * Stored setting value. * - * @type string $mode Mode. - * @type string $eagerness Eagerness. - * @type string $authentication Authentication. + * @type string $mode Mode. + * @type string $eagerness Eagerness. + * @type string $authentication Authentication. + * @type string $origin_trial_token Origin trial token. * } */ function plsr_get_stored_setting_value(): array { @@ -125,12 +129,13 @@ function plsr_get_stored_setting_value(): array { * @todo Consider whether the JSON schema for the setting could be reused here. * * @param mixed $input Setting to sanitize. - * @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager', authentication: 'logged_out'|'logged_out_and_admins'|'any' } { + * @return array{ mode: 'prefetch'|'prerender'|'prerender_until_script', eagerness: 'conservative'|'moderate'|'eager', authentication: 'logged_out'|'logged_out_and_admins'|'any', origin_trial_token: string } { * Sanitized setting. * - * @type string $mode Mode. - * @type string $eagerness Eagerness. - * @type string $authentication Authentication. + * @type string $mode Mode. + * @type string $eagerness Eagerness. + * @type string $authentication Authentication. + * @type string $origin_trial_token Origin trial token. * } */ function plsr_sanitize_setting( $input ): array { @@ -154,6 +159,8 @@ function plsr_sanitize_setting( $input ): array { $value['authentication'] = $default_value['authentication']; } + $value['origin_trial_token'] = sanitize_text_field( $value['origin_trial_token'] ); + return $value; } @@ -176,21 +183,25 @@ function plsr_register_setting(): void { 'schema' => array( 'type' => 'object', 'properties' => array( - 'mode' => array( + 'mode' => array( 'description' => wp_strip_all_tags( plsr_get_field_description( 'mode' ) ), 'type' => 'string', 'enum' => array_keys( plsr_get_mode_labels() ), ), - 'eagerness' => array( + 'eagerness' => array( 'description' => wp_strip_all_tags( plsr_get_field_description( 'eagerness' ) ), 'type' => 'string', 'enum' => array_keys( plsr_get_eagerness_labels() ), ), - 'authentication' => array( + 'authentication' => array( 'description' => wp_strip_all_tags( plsr_get_field_description( 'authentication' ) ), 'type' => 'string', 'enum' => array_keys( plsr_get_authentication_labels() ), ), + 'origin_trial_token' => array( + 'description' => __( 'Origin Trial Token for Prerender Until Script.', 'speculation-rules' ), + 'type' => 'string', + ), ), 'additionalProperties' => false, ), @@ -225,18 +236,22 @@ static function (): void { ); $fields = array( - 'mode' => array( + 'mode' => array( 'title' => __( 'Speculation Mode', 'speculation-rules' ), 'description' => plsr_get_field_description( 'mode' ), ), - 'eagerness' => array( + 'eagerness' => array( 'title' => __( 'Eagerness', 'speculation-rules' ), 'description' => plsr_get_field_description( 'eagerness' ), ), - 'authentication' => array( + 'authentication' => array( 'title' => __( 'User Authentication Status', 'speculation-rules' ), 'description' => plsr_get_field_description( 'authentication' ), ), + 'origin_trial_token' => array( + 'title' => __( 'Origin Trial Token', 'speculation-rules' ), + 'description' => __( 'If you are using the "Prerender until script" mode in production, you can register for the Chrome Origin Trial and paste your token here to enable it for visitors.', 'speculation-rules' ), + ), ); foreach ( $fields as $slug => $args ) { add_settings_field( @@ -260,7 +275,7 @@ static function (): void { * @since 1.0.0 * @access private * - * @param array{ field: 'mode'|'eagerness'|'authentication', title: non-empty-string, description: non-empty-string } $args { + * @param array{ field: 'mode'|'eagerness'|'authentication'|'origin_trial_token', title: non-empty-string, description: non-empty-string } $args { * Associative array of arguments. * * @type string $field The slug of the sub setting controlled by the field. @@ -281,6 +296,26 @@ function plsr_render_settings_field( array $args ): void { case 'authentication': $choices = plsr_get_authentication_labels(); break; + case 'origin_trial_token': + $value = $option['origin_trial_token']; + ?> +
+ +

+ +

+

+ +

+
+ assertCount( 4, $rules['prerender'][0]['where']['and'] ); } + /** + * @covers ::plsr_get_speculation_rules + */ + public function test_plsr_get_speculation_rules_prerender_until_script(): void { + update_option( 'plsr_speculation_rules', array( 'mode' => 'prerender_until_script' ) ); + + $rules = plsr_get_speculation_rules(); + + $this->assertArrayHasKey( 'prerender_until_script', $rules ); + $this->assertCount( 4, $rules['prerender_until_script'][0]['where']['and'] ); + $this->assertSame( + '.no-prerender, .no-prerender_until_script', + $rules['prerender_until_script'][0]['where']['and'][3]['not']['selector_matches'] + ); + } + /** * @covers ::plsr_get_speculation_rules */ diff --git a/plugins/speculation-rules/tests/test-speculation-rules-settings.php b/plugins/speculation-rules/tests/test-speculation-rules-settings.php index 3da5e1fcde..dc8a65597a 100644 --- a/plugins/speculation-rules/tests/test-speculation-rules-settings.php +++ b/plugins/speculation-rules/tests/test-speculation-rules-settings.php @@ -67,9 +67,10 @@ public function test_plsr_sanitize_setting( $input, array $expected ): void { /** @return array */ public function data_plsr_sanitize_setting(): array { $default_value = array( - 'mode' => 'prerender', - 'eagerness' => 'moderate', - 'authentication' => 'logged_out', + 'mode' => 'prerender', + 'eagerness' => 'moderate', + 'authentication' => 'logged_out', + 'origin_trial_token' => '', ); return array( @@ -215,17 +216,19 @@ public function test_get_stored_setting_value(): void { update_option( 'plsr_speculation_rules', array( - 'mode' => 'prefetch', - 'eagerness' => 'moderate', - 'authentication' => 'logged_out', + 'mode' => 'prefetch', + 'eagerness' => 'moderate', + 'authentication' => 'logged_out', + 'origin_trial_token' => '', ) ); $settings = plsr_get_stored_setting_value(); $this->assertEquals( array( - 'mode' => 'prefetch', - 'eagerness' => 'moderate', - 'authentication' => 'logged_out', + 'mode' => 'prefetch', + 'eagerness' => 'moderate', + 'authentication' => 'logged_out', + 'origin_trial_token' => '', ), $settings ); @@ -283,24 +286,30 @@ public function test_plsr_add_setting_ui(): void { */ public function data_provider_to_test_render_settings_field(): array { return array( - 'mode' => array( + 'mode' => array( 'field' => 'mode', 'value' => 'prefetch', 'title' => 'Speculation Mode', 'description' => 'The mode description', ), - 'eagerness' => array( + 'eagerness' => array( 'field' => 'eagerness', 'value' => 'moderate', 'title' => 'Eagerness', 'description' => 'The eagerness description', ), - 'authentication' => array( + 'authentication' => array( 'field' => 'authentication', 'value' => 'any', 'title' => 'Authentication', 'description' => 'The authentication description.', ), + 'origin_trial_token' => array( + 'field' => 'origin_trial_token', + 'value' => 'some_token', + 'title' => 'Origin Trial Token', + 'description' => 'The origin trial token description.', + ), ); } @@ -332,7 +341,11 @@ public function test_plsr_render_settings_field( string $field, string $value, s && $p->get_attribute( 'value' ) === $value ) { - $found = null !== $p->get_attribute( 'checked' ); + if ( 'origin_trial_token' === $field ) { + $found = true; + } else { + $found = null !== $p->get_attribute( 'checked' ); + } break; } } diff --git a/plugins/speculation-rules/tests/test-speculation-rules-wp-core-api.php b/plugins/speculation-rules/tests/test-speculation-rules-wp-core-api.php index 2fcdcbcac8..627359a29a 100644 --- a/plugins/speculation-rules/tests/test-speculation-rules-wp-core-api.php +++ b/plugins/speculation-rules/tests/test-speculation-rules-wp-core-api.php @@ -99,6 +99,21 @@ public function test_plsr_filter_speculation_rules_exclude_paths_with_invalid(): $this->assertSame( array( '/personalized/*' ), plsr_filter_speculation_rules_exclude_paths( '/personalized/*', 'prefetch' ) ); } + /** + * @covers ::plsr_filter_speculation_rules_configuration + */ + public function test_plsr_filter_speculation_rules_configuration_with_prerender_until_script(): void { + add_filter( 'plsr_enabled_without_pretty_permalinks', '__return_true' ); + update_option( 'plsr_speculation_rules', array( 'mode' => 'prerender_until_script' ) ); + $this->assertSame( + array( + 'mode' => 'prerender_until_script', + 'eagerness' => 'moderate', + ), + plsr_filter_speculation_rules_configuration( null ) + ); + } + private function disable_pretty_permalinks(): void { update_option( 'permalink_structure', '' ); } diff --git a/plugins/speculation-rules/tests/test-speculation-rules.php b/plugins/speculation-rules/tests/test-speculation-rules.php index 1fd8b48421..1069ff943b 100644 --- a/plugins/speculation-rules/tests/test-speculation-rules.php +++ b/plugins/speculation-rules/tests/test-speculation-rules.php @@ -128,6 +128,7 @@ public function test_hooks(): void { } $this->assertSame( 10, has_action( 'wp_head', 'plsr_render_generator_meta_tag' ) ); + $this->assertSame( 1, has_action( 'wp_head', 'plsr_print_origin_trial_meta_tag' ) ); } /** @@ -141,4 +142,53 @@ public function test_plsr_render_generator_meta_tag(): void { $this->assertStringContainsString( 'generator', $tag ); $this->assertStringContainsString( 'speculation-rules ' . SPECULATION_RULES_VERSION, $tag ); } + + /** + * Test printing the origin trial meta tag. + * + * @covers ::plsr_print_origin_trial_meta_tag + */ + public function test_plsr_print_origin_trial_meta_tag(): void { + // Ensure no user is logged in so that plsr_is_speculative_loading_enabled() returns true. + wp_set_current_user( 0 ); + + // Enable pretty permalinks to bypass speculative loading checks. + update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%hour%/%minute%/%second%' ); + + // Case 1: Empty token. + update_option( + 'plsr_speculation_rules', + array( + 'mode' => 'prerender_until_script', + 'origin_trial_token' => '', + ) + ); + $this->assertSame( '', get_echo( 'plsr_print_origin_trial_meta_tag' ) ); + + // Case 2: Mode is not prerender_until_script (e.g. prerender). + update_option( + 'plsr_speculation_rules', + array( + 'mode' => 'prerender', + 'origin_trial_token' => 'test-token', + ) + ); + $this->assertSame( '', get_echo( 'plsr_print_origin_trial_meta_tag' ) ); + + // Case 3: Token is set and mode is prerender_until_script. + update_option( + 'plsr_speculation_rules', + array( + 'mode' => 'prerender_until_script', + 'origin_trial_token' => 'test-token', + ) + ); + $tag = get_echo( 'plsr_print_origin_trial_meta_tag' ); + $this->assertStringStartsWith( 'assertStringContainsString( 'origin-trial', $tag ); + $this->assertStringContainsString( 'test-token', $tag ); + + // Clean up. + update_option( 'permalink_structure', '' ); + } } diff --git a/plugins/speculation-rules/wp-core-api.php b/plugins/speculation-rules/wp-core-api.php index a2fd603f88..f16244822b 100644 --- a/plugins/speculation-rules/wp-core-api.php +++ b/plugins/speculation-rules/wp-core-api.php @@ -49,7 +49,7 @@ function plsr_filter_speculation_rules_configuration( $config ): ?array { * @since 1.5.0 * * @param string[]|mixed $href_exclude_paths Additional path patterns to disable speculative loading for. - * @param string $mode Mode used to apply speculative loading. Either 'prefetch' or 'prerender'. + * @param string $mode Mode used to apply speculative loading. Either 'prefetch', 'prerender', or 'prerender_until_script'. * @return string[] Filtered $href_exclude_paths. */ function plsr_filter_speculation_rules_exclude_paths( $href_exclude_paths, string $mode ): array {