Skip to content

Commit 42bb977

Browse files
westonruterswissspidyfelixarntz
authored
Merge pull request #2097 from WordPress/add/authenticated-speculative-loading
Add Speculative Loading opt-in for authenticated requests Co-authored-by: westonruter <[email protected]> Co-authored-by: swissspidy <[email protected]> Co-authored-by: felixarntz <[email protected]>
2 parents 347a3de + ac30308 commit 42bb977

File tree

8 files changed

+360
-123
lines changed

8 files changed

+360
-123
lines changed

plugins/speculation-rules/hooks.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,53 @@
1212
}
1313
// @codeCoverageIgnoreEnd
1414

15+
/**
16+
* Determines whether Speculative Loading is enabled.
17+
*
18+
* @since n.e.x.t
19+
*
20+
* @return bool Whether enabled.
21+
*/
22+
function plsr_is_speculative_loading_enabled(): bool {
23+
$option = plsr_get_stored_setting_value();
24+
25+
// Disabled if the user is logged in, unless the setting explicitly allows the current user's role.
26+
if (
27+
is_user_logged_in()
28+
&&
29+
'any' !== $option['authentication']
30+
&&
31+
( ! current_user_can( 'manage_options' ) || 'logged_out_and_admins' !== $option['authentication'] )
32+
) {
33+
return false;
34+
}
35+
36+
// Disable if pretty permalinks are not enabled, unless explicitly overridden by the filter.
37+
if (
38+
! (bool) get_option( 'permalink_structure' )
39+
&&
40+
/**
41+
* Filters whether speculative loading should be enabled even though the site does not use pretty permalinks.
42+
*
43+
* Since query parameters are commonly used by plugins for dynamic behavior that can change state, ideally any
44+
* such URLs are excluded from speculative loading. If the site does not use pretty permalinks though, they are
45+
* impossible to recognize. Therefore, speculative loading is disabled by default for those sites.
46+
*
47+
* For site owners of sites without pretty permalinks that are certain their site is not using such a pattern,
48+
* this filter can be used to still enable speculative loading at their own risk.
49+
*
50+
* @since 1.4.0
51+
*
52+
* @param bool $enabled Whether speculative loading is enabled even without pretty permalinks.
53+
*/
54+
! apply_filters( 'plsr_enabled_without_pretty_permalinks', false )
55+
) {
56+
return false;
57+
}
58+
59+
return true;
60+
}
61+
1562
// Conditionally use either the WordPress Core API, or load the plugin's API implementation otherwise.
1663
if ( function_exists( 'wp_get_speculation_rules_configuration' ) ) {
1764
require_once __DIR__ . '/wp-core-api.php';

plugins/speculation-rules/plugin-api.php

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -130,34 +130,10 @@ static function ( string $href_exclude_path ) use ( $prefixer ): string {
130130
* @since 1.0.0
131131
*/
132132
function plsr_print_speculation_rules(): void {
133-
// Skip speculative loading for logged-in users.
134-
if ( is_user_logged_in() ) {
133+
if ( ! plsr_is_speculative_loading_enabled() ) {
135134
return;
136135
}
137136

138-
// Skip speculative loading for sites without pretty permalinks, unless explicitly enabled.
139-
if ( ! (bool) get_option( 'permalink_structure' ) ) {
140-
/**
141-
* Filters whether speculative loading should be enabled even though the site does not use pretty permalinks.
142-
*
143-
* Since query parameters are commonly used by plugins for dynamic behavior that can change state, ideally any
144-
* such URLs are excluded from speculative loading. If the site does not use pretty permalinks though, they are
145-
* impossible to recognize. Therefore speculative loading is disabled by default for those sites.
146-
*
147-
* For site owners of sites without pretty permalinks that are certain their site is not using such a pattern,
148-
* this filter can be used to still enable speculative loading at their own risk.
149-
*
150-
* @since 1.4.0
151-
*
152-
* @param bool $enabled Whether speculative loading is enabled even without pretty permalinks.
153-
*/
154-
$enabled = (bool) apply_filters( 'plsr_enabled_without_pretty_permalinks', false );
155-
156-
if ( ! $enabled ) {
157-
return;
158-
}
159-
}
160-
161137
wp_print_inline_script_tag(
162138
(string) wp_json_encode( plsr_get_speculation_rules() ),
163139
array( 'type' => 'speculationrules' )

plugins/speculation-rules/readme.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Enables browsers to speculatively prerender or prefetch pages to achieve near-in
1313

1414
This plugin adds support for the [Speculation Rules API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API), which allows defining rules by which certain URLs are dynamically prefetched or prerendered. This core Speculative Loading functionality was [merged into WordPress 6.8](https://make.wordpress.org/core/2025/03/06/speculative-loading-in-6-8/), but it only prefetches with conservative eagerness by default. In contrast, this plugin defaults to prerendering with moderate eagerness (i.e. when interacting with a link), and it provides a user interface to customize the mode and eagerness via the "Speculative Loading" section on the _Settings > Reading_ admin screen.
1515

16+
By default, speculative loading is only enabled for logged-out users, since unauthenticated pages are typically only eligible for caching and so more efficient to prefetch/prerender. This means that sites with frequent logged-in users on the frontend—such as e-commerce, forums, or membership sites—will not benefit from the feature. If your server can handle the additional load (for example, with persistent object caching), you can opt in to enable speculative loading for all logged-in users or for administrators only. This setting exclusively affects frontend pages; admin screens are always excluded.
17+
1618
A filter can be used to exclude certain URL paths from being eligible for prefetching and prerendering (see FAQ section). Alternatively, you can add the `no-prerender` CSS class to any link (`<a>` tag) that should not be prerendered. See FAQ for more information.
1719

1820
= Browser support =

plugins/speculation-rules/settings.php

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,39 @@ function plsr_get_eagerness_labels(): array {
4141
);
4242
}
4343

44+
/**
45+
* Returns the available options for the Speculative Loading authentication and their labels.
46+
*
47+
* @since n.e.x.t
48+
*
49+
* @return array{ logged_out: string, logged_out_and_admins: string, any: string } Associative array of `$authentication => $label` pairs.
50+
*/
51+
function plsr_get_authentication_labels(): array {
52+
return array(
53+
'logged_out' => _x( 'Logged-out visitors only (default)', 'setting label', 'speculation-rules' ),
54+
'logged_out_and_admins' => _x( 'Administrators and logged-out visitors', 'setting label', 'speculation-rules' ),
55+
'any' => _x( 'Any user (logged-in or logged-out)', 'setting label', 'speculation-rules' ),
56+
);
57+
}
58+
4459
/**
4560
* Returns the default setting value for Speculative Loading configuration.
4661
*
4762
* @since 1.0.0
4863
*
49-
* @return array{ mode: 'prerender', eagerness: 'moderate' } {
64+
* @return array{ mode: 'prerender', eagerness: 'moderate', authentication: 'logged_out' } {
5065
* Default setting value.
5166
*
52-
* @type string $mode Mode.
53-
* @type string $eagerness Eagerness.
67+
* @type string $mode Mode.
68+
* @type string $eagerness Eagerness.
69+
* @type string $authentication Authentication.
5470
* }
5571
*/
5672
function plsr_get_setting_default(): array {
5773
return array(
58-
'mode' => 'prerender',
59-
'eagerness' => 'moderate',
74+
'mode' => 'prerender',
75+
'eagerness' => 'moderate',
76+
'authentication' => 'logged_out',
6077
);
6178
}
6279

@@ -65,11 +82,12 @@ function plsr_get_setting_default(): array {
6582
*
6683
* @since 1.4.0
6784
*
68-
* @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } {
85+
* @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager', authentication: 'logged_out'|'logged_out_and_admins'|'any' } {
6986
* Stored setting value.
7087
*
71-
* @type string $mode Mode.
72-
* @type string $eagerness Eagerness.
88+
* @type string $mode Mode.
89+
* @type string $eagerness Eagerness.
90+
* @type string $authentication Authentication.
7391
* }
7492
*/
7593
function plsr_get_stored_setting_value(): array {
@@ -83,11 +101,12 @@ function plsr_get_stored_setting_value(): array {
83101
* @todo Consider whether the JSON schema for the setting could be reused here.
84102
*
85103
* @param mixed $input Setting to sanitize.
86-
* @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager' } {
104+
* @return array{ mode: 'prefetch'|'prerender', eagerness: 'conservative'|'moderate'|'eager', authentication: 'logged_out'|'logged_out_and_admins'|'any' } {
87105
* Sanitized setting.
88106
*
89-
* @type string $mode Mode.
90-
* @type string $eagerness Eagerness.
107+
* @type string $mode Mode.
108+
* @type string $eagerness Eagerness.
109+
* @type string $authentication Authentication.
91110
* }
92111
*/
93112
function plsr_sanitize_setting( $input ): array {
@@ -107,6 +126,9 @@ function plsr_sanitize_setting( $input ): array {
107126
if ( ! in_array( $value['eagerness'], array_keys( plsr_get_eagerness_labels() ), true ) ) {
108127
$value['eagerness'] = $default_value['eagerness'];
109128
}
129+
if ( ! in_array( $value['authentication'], array_keys( plsr_get_authentication_labels() ), true ) ) {
130+
$value['authentication'] = $default_value['authentication'];
131+
}
110132

111133
return $value;
112134
}
@@ -130,16 +152,21 @@ function plsr_register_setting(): void {
130152
'schema' => array(
131153
'type' => 'object',
132154
'properties' => array(
133-
'mode' => array(
155+
'mode' => array(
134156
'description' => __( 'Whether to prefetch or prerender URLs.', 'speculation-rules' ),
135157
'type' => 'string',
136158
'enum' => array_keys( plsr_get_mode_labels() ),
137159
),
138-
'eagerness' => array(
160+
'eagerness' => array(
139161
'description' => __( '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' ),
140162
'type' => 'string',
141163
'enum' => array_keys( plsr_get_eagerness_labels() ),
142164
),
165+
'authentication' => array(
166+
'description' => __( 'Only unauthenticated pages are typically served from cache. So in order to reduce load on the server, speculative loading is not enabled by default for logged-in users. If your server can handle the additional load, you can opt in to speculative loading for all logged-in users or just administrator users only. This only applies to pages on frontend; admin screens remain excluded.', 'speculation-rules' ),
167+
'type' => 'string',
168+
'enum' => array_keys( plsr_get_authentication_labels() ),
169+
),
143170
),
144171
'additionalProperties' => false,
145172
),
@@ -174,14 +201,18 @@ static function (): void {
174201
);
175202

176203
$fields = array(
177-
'mode' => array(
204+
'mode' => array(
178205
'title' => __( 'Speculation Mode', 'speculation-rules' ),
179206
'description' => __( 'Prerendering will lead to faster load times than prefetching. However, in case of interactive content, prefetching may be a safer choice.', 'speculation-rules' ),
180207
),
181-
'eagerness' => array(
208+
'eagerness' => array(
182209
'title' => __( 'Eagerness', 'speculation-rules' ),
183210
'description' => __( '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' ),
184211
),
212+
'authentication' => array(
213+
'title' => __( 'User Authentication Status', 'speculation-rules' ),
214+
'description' => __( 'Only unauthenticated pages are typically served from cache. So in order to reduce load on the server, speculative loading is not enabled by default for logged-in users. If your server can handle the additional load, you can opt in to speculative loading for all logged-in users or just administrator users only. This only applies to pages on frontend; admin screens remain excluded.', 'speculation-rules' ),
215+
),
185216
);
186217
foreach ( $fields as $slug => $args ) {
187218
add_settings_field(
@@ -205,7 +236,7 @@ static function (): void {
205236
* @since 1.0.0
206237
* @access private
207238
*
208-
* @param array{ field: 'mode'|'eagerness', title: non-empty-string, description: non-empty-string } $args {
239+
* @param array{ field: 'mode'|'eagerness'|'authentication', title: non-empty-string, description: non-empty-string } $args {
209240
* Associative array of arguments.
210241
*
211242
* @type string $field The slug of the sub setting controlled by the field.
@@ -223,6 +254,9 @@ function plsr_render_settings_field( array $args ): void {
223254
case 'eagerness':
224255
$choices = plsr_get_eagerness_labels();
225256
break;
257+
case 'authentication':
258+
$choices = plsr_get_authentication_labels();
259+
break;
226260
default:
227261
// Invalid (and this case should never occur).
228262
return; // @codeCoverageIgnore

0 commit comments

Comments
 (0)