Repository: Automattic/jetpack
Pull request #42549: Boost: Cache Preloading force rebuilding of cache files
Author: login: LiamSarsfield
Labels: name: [Plugin] Boost
description: A feature to speed up the site and improve performance.
name: [Tests] Includes Tests
name: [Boost Feature] Page Cache
Created: 2025-03-18T17:37:47Z | Merged: 2025-03-24T09:33:28Z

> [!NOTE]
> Merge this PR after https://github.com/Automattic/jetpack/pull/42528 is merged.

<!--- Provide a general summary of your changes in the Title above -->
<!-- Would you like this feature to be tested by Beta testers?
Please add testing instructions to projects/plugins/jetpack/to-test.md in a new commit as part of your PR. -->
<!-- a12s: If you have an expected version that you're aiming for the PR to add, please use the Milestone field to communicate it. If you leave it blank, that indicates there isn't a preference. -->

Resolves HOG-2

## Proposed changes:
<!--- Explain what functional changes your PR includes -->
* Updates what is considered a Preload within Boost's context, by first doing a Cache Rebuild for Cornerstone pages, and then immediately calling each Cornerstone Page's URL so the cache is rebuilt in the preloaded request.

### Other information:

- [x] Have you written new tests for your changes, if applicable?
- [ ] Have you checked the E2E test CI results, and verified that your changes do not break them?
- [ ] Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

## Jetpack product discussion
<!-- If you're an Automattician, include a shortlink to the p2 discussion with Jetpack Product here. -->
<!-- Make sure any changes to existing products have been discussed and agreed upon -->
pc9hqz-3nV-p2

## Does this pull request change what data or activity we track or use?
<!--- If so, please add the "[Status] Needs Privacy Updates" label and explain what changes there are. -->
<!--- Check existing Jetpack support documents for a preview of the information we need. -->
N/A

## Testing instructions:

### Prerequisites
- Ensure that the constant JETPACK_BOOST_ALPHA_FEATURES is defined as true either in an mu-plugin or wp-config.php.
- If testing on a Jurassic Ninja site, ensure Edge Caching is disabled in Settings -> Edge Cache -> Disable Edge Cache
- Bypassing Edge Cache is out of scope for this PR.

### 1. Updating a Cornerstone Page
<details>
<summary>Instructions</summary>

1. Go to Boost Settings page.
2. Ensure Boost Premium is on the WordPress site.
3. Enable Cache Site Pages (Page Cache).
4. Add at least 2 valid Cornerstone Pages.
5. Clear the Page Cache through the wp-admin UI
6. There should now be cache files present for each Cornerstone page, thanks to #41988
7. Within the WordPress site, head to `./wp-content/boost-cache/cache/(domain)/(Cornerstone Page)`
8. Note down when the file was created via `stat -c '%w %n' *`
9. Edit the Cornerstone Page within the WP Admin UI
10. Verify using `stat -c '%w %n' *` that the file creation date is now different. If the file ends in `.rebuild.html`, wait a moment, and it should change back to `.html` once the cache has been built from the preload.

</details>

### 2. Updating Cornerstone Page List
<details>
<summary>Instructions</summary>

1. Go to Boost Settings page.
2. Ensure Boost Premium is on the WordPress site.
3. Enable Cache Site Pages (Page Cache).
4. Add at least 2 valid Cornerstone Pages.
5. Within the WordPress site, head to `./wp-content/boost-cache/cache/(domain)`
6. Verify that page caching files are present.
7. Note down when a Cornerstone Page cache file was created via `stat -c '%w %n' *`.
8. Add a third page to the Cornerstone Page List.
9. Verify using `stat -c '%w %n' *` that the file creation date is now different. If the file ends in `.rebuild.html`, wait a moment, and it should change back to `.html` once the cache has been built from the preload.
</details>

### 3. Feature Flag Testing
<details>
<summary>Instructions</summary>

1. Remove the constant `JETPACK_BOOST_ALPHA_FEATURES` added previously, or set it to false.
2. Redo the testing for Updating Cornerstone Page List.
3. When running `stat -c '%w %n' *`, the cache files date time will **not** be different upon updating the Cornerstone Page List.
</details>


Diff:
diff --git a/projects/plugins/boost/app/modules/optimizations/page-cache/class-cache-preload.php b/projects/plugins/boost/app/modules/optimizations/page-cache/class-cache-preload.php
index cedcff520e5a..03b5fd12c142 100644
--- a/projects/plugins/boost/app/modules/optimizations/page-cache/class-cache-preload.php
+++ b/projects/plugins/boost/app/modules/optimizations/page-cache/class-cache-preload.php
@@ -6,14 +6,15 @@
 use Automattic\Jetpack_Boost\Contracts\Is_Always_On;
 use Automattic\Jetpack_Boost\Contracts\Pluggable;
 use Automattic\Jetpack_Boost\Lib\Cornerstone\Cornerstone_Utils;
+use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Boost_Cache;
 use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Filesystem_Utils;
 use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Logger;
 
 /**
  * Class Cache_Preload
  *
- * Handles the preloading of cache for pages, currently only for Cornerstone Pages.
- * This module automagically preloads the cache after cache invalidation events, or when
+ * Handles the rebuilding/preloading of cache for pages, currently only for Cornerstone Pages.
+ * This module automagically preloads the cache after cache invalidation events such as rebuilds, or when
  * Cornerstone Pages are updated, to ensure that important pages always have a cache.
  *
  * @since 3.11.0
@@ -25,8 +26,8 @@ class Cache_Preload implements Pluggable, Has_Activate, Is_Always_On {
 	 * @since 3.11.0
 	 */
 	public function setup() {
-		add_action( 'update_option_jetpack_boost_ds_cornerstone_pages_list', array( $this, 'schedule_cornerstone_preload' ) );
-		add_action( 'jetpack_boost_preload_pages', array( $this, 'preload_pages' ) );
+		add_action( 'update_option_jetpack_boost_ds_cornerstone_pages_list', array( $this, 'schedule_cornerstone' ) );
+		add_action( 'jetpack_boost_preload', array( $this, 'preload' ) );
 
 		add_action( 'post_updated', array( $this, 'handle_post_update' ), 10, 1 );
 		add_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ), 10, 2 );
@@ -62,55 +63,90 @@ public static function activate() {
 	}
 
 	/**
-	 * Schedule preload for all Cornerstone Pages.
+	 * Schedule rebuild for all Cornerstone Pages.
 	 *
 	 * This method is triggered when the Cornerstone Pages list is updated,
-	 * ensuring all Cornerstone Pages have their cache preloaded.
+	 * ensuring all Cornerstone Pages have their cache rebuilt, and hence preloaded.
 	 *
-	 * @since 3.11.0
+	 * @since $$next-version$$
 	 * @return void
 	 */
-	public function schedule_cornerstone_preload() {
-		$this->schedule_preload_cronjob( Cornerstone_Utils::get_list() );
+	public function schedule_cornerstone() {
+		$this->schedule( Cornerstone_Utils::get_list() );
+	}
+
+	/**
+	 * Schedule a rebuild for the given URLs.
+	 *
+	 * @since $$next-version$$
+	 * @param array $urls The URLs of the Cornerstone Pages to rebuild.
+	 * @return void
+	 */
+	public function schedule( array $urls ) {
+		Logger::debug( sprintf( 'Scheduling preload for %d pages', count( $urls ) ) );
+		wp_schedule_single_event( time(), 'jetpack_boost_preload', array( $urls ) );
+	}
+
+	/**
+	 * Rebuild the cache for the given URLs.
+	 *
+	 * @since $$next-version$$
+	 * @param array $urls The URLs of the Cornerstone Pages to preload.
+	 * @return void
+	 */
+	public function preload( array $urls ) {
+		Logger::debug( sprintf( 'Preload started for %d pages', count( $urls ) ) );
+
+		// Pause the hook here to avoid an infinite loop of: invalidation → preload → invalidation.
+		remove_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ) );
+		$boost_cache = new Boost_Cache();
+		foreach ( $urls as $url ) {
+			$boost_cache->invalidate_cache_for_url( $url, Filesystem_Utils::REBUILD_FILES );
+			$this->request_page( $url );
+		}
+		add_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ), 10, 2 );
+
+		Logger::debug( sprintf( 'Preload completed for %d pages', count( $urls ) ) );
 	}
 
 	/**
-	 * Schedules the preload cronjob, if not already scheduled
-	 * to execute within 10 minutes with the same arguments, per wp_schedule_single_event.
+	 * Schedule preload for all Cornerstone Pages.
+	 *
+	 * This method is triggered when the Cornerstone Pages list is updated,
+	 * ensuring all Cornerstone Pages have their cache preloaded.
 	 *
 	 * @since 3.11.0
-	 * @param array $posts The posts to preload.
 	 * @return void
 	 */
-	public function schedule_preload_cronjob( array $posts ) {
-		wp_schedule_single_event( time(), 'jetpack_boost_preload_pages', array( $posts ) );
+	public function schedule_cornerstone_preload() {
+		$this->schedule( Cornerstone_Utils::get_list() );
 	}
 
 	/**
-	 * Preloads the pages scheduled for preload.
+	 * Requests the pages scheduled for preload.
 	 *
 	 * @since 3.11.0
 	 * @param array $posts The posts to preload.
 	 * @return void
 	 */
-	public function preload_pages( $posts ) {
+	public function request_pages( $posts ) {
 		if ( empty( $posts ) ) {
 			return;
 		}
 
 		foreach ( $posts as $url ) {
-			$this->preload_page( $url );
+			$this->request_page( $url );
 		}
 	}
 
 	/**
-	 * Preload a single page. Makes an HTTP request to the specified URL to generate a fresh cache entry.
+	 * Make an HTTP request to the specified URL to generate a fresh cache entry.
 	 *
 	 * @since 3.11.0
 	 * @param string $page The URL of the page to preload.
 	 * @return void
 	 */
-	private function preload_page( string $page ) {
+	private function request_page( string $page ) {
 		// Add a cache-busting header to ensure our response is fresh.
 		$args = array(
 			'headers' => array(
@@ -142,7 +178,7 @@ private function preload_page( string $page ) {
 	 */
 	public function handle_post_update( int $post_id ) {
 		if ( Cornerstone_Utils::is_cornerstone_page( $post_id ) ) {
-			$this->schedule_preload_cronjob( array( get_permalink( $post_id ) ) );
+			$this->schedule( array( get_permalink( $post_id ) ) );
 		}
 	}
 
@@ -169,7 +205,7 @@ public function handle_cache_invalidation( string $path, string $type ) {
 		$cornerstone_pages = array_map( 'untrailingslashit', $cornerstone_pages );
 		// If the $path is in the Cornerstone Page list, add it to the preload list.
 		if ( in_array( untrailingslashit( $path ), $cornerstone_pages, true ) ) {
-			$this->schedule_preload_cronjob( array( $path ) );
+			$this->schedule( array( $path ) );
 		}
 	}
 }
diff --git a/projects/plugins/boost/changelog/add-boost-cache-preloading-force-rebuild b/projects/plugins/boost/changelog/add-boost-cache-preloading-force-rebuild
new file mode 100644
index 000000000000..93e4b7eb49b0
--- /dev/null
+++ b/projects/plugins/boost/changelog/add-boost-cache-preloading-force-rebuild
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Add Page Cache Preloading for Cornerstone Pages
diff --git a/projects/plugins/boost/tests/php/modules/optimizations/page-cache/Cache_Preload_Test.php b/projects/plugins/boost/tests/php/modules/optimizations/page-cache/Cache_Preload_Test.php
index b729d2d9808f..e53517942a7c 100644
--- a/projects/plugins/boost/tests/php/modules/optimizations/page-cache/Cache_Preload_Test.php
+++ b/projects/plugins/boost/tests/php/modules/optimizations/page-cache/Cache_Preload_Test.php
@@ -71,7 +71,7 @@ public function test_schedule_cornerstone_preload() {
 
 		Functions\expect( 'wp_schedule_single_event' )
 			->once()
-			->with( Mockery::type( 'int' ), 'jetpack_boost_preload_pages', array( $cornerstone_pages ) );
+			->with( Mockery::type( 'int' ), 'jetpack_boost_preload', array( $cornerstone_pages ) );
 
 		// Create a mock that doesn't call schedule_cornerstone_preload but verifies its behavior
 		$preload = new Cache_Preload();
@@ -81,34 +81,70 @@ public function test_schedule_cornerstone_preload() {
 	}
 
 	/**
-	 * Test schedule_preload_cronjob correctly schedules the cron event.
+	 * Test schedule correctly schedules the event.
 	 */
-	public function test_schedule_preload_cronjob() {
+	public function test_schedule() {
 		$posts = array( 'https://example.com', 'https://example.com/page' );
 		Functions\expect( 'wp_schedule_single_event' )
 			->once()
-			->with( Mockery::type( 'int' ), 'jetpack_boost_preload_pages', array( $posts ) );
+			->with( Mockery::type( 'int' ), 'jetpack_boost_preload', array( $posts ) );
 
 		$preload = new Cache_Preload();
-		$preload->schedule_preload_cronjob( $posts );
+		$preload->schedule( $posts );
 		$this->expectNotToPerformAssertions();
 	}
 
 	/**
-	 * Test preload_pages with an empty queue.
+	 * Test preload with an empty queue.
 	 */
-	public function test_preload_pages_empty_posts() {
+	public function test_preload_empty_posts() {
 		// Set up the mock
 		$preload = new Cache_Preload();
 
-		$preload->preload_pages( array() );
+		$preload->request_pages( array() );
 		$this->expectNotToPerformAssertions();
 	}
 
 	/**
-	 * Test preload_pages with posts in the queue.
+	 * Test preload method rebuilds cache for provided URLs.
 	 */
-	public function test_preload_pages_with_posts() {
+	public function test_preload() {
+		define( 'JETPACK_BOOST_CACHE_DURATION', 1 );
+		define( 'ABSPATH', '/pseudo' );
+
+		$urls = array( 'https://example.com', 'https://example.com/page' );
+
+		$preload = new Cache_Preload();
+		// Mock wp_remote_get
+		foreach ( $urls as $url ) {
+			Functions\expect( 'wp_remote_get' )
+				->once()
+				->with( $url, Mockery::type( 'array' ) );
+			Functions\expect( 'wp_remote_retrieve_response_code' )
+				->once()
+				->withAnyArgs()
+				->andReturn( 200 );
+		}
+
+		// Mock WordPress functions
+		Functions\expect( 'remove_action' )
+			->once()
+			->withArgs( array( 'jetpack_boost_invalidate_cache_success', array( $preload, 'handle_cache_invalidation' ) ) );
+
+		Functions\expect( 'add_action' )
+			->once()
+			->withArgs( array( 'jetpack_boost_invalidate_cache_success', array( $preload, 'handle_cache_invalidation' ), 10, 2 ) );
+
+		// Execute the method being tested
+		$preload->preload( $urls );
+
+		$this->expectNotToPerformAssertions();
+	}
+
+	/**
+	 * Test request_pages with posts in the queue.
+	 */
+	public function test_request_pages_with_posts() {
 		$posts = array( 'https://example.com', 'https://example.com/page' );
 
 		// Set up the mock
@@ -125,15 +161,15 @@ public function test_preload_pages_with_posts() {
 				->andReturn( 200 );
 		}
 
-		$preload->preload_pages( $posts );
+		$preload->request_pages( $posts );
 
 		$this->expectNotToPerformAssertions();
 	}
 
 	/**
-	 * Test schedule_preload_cronjob with a single URL.
+	 * Test schedule with a single URL.
 	 */
-	public function test_schedule_preload_single() {
+	public function test_schedule_single() {
 		$new_url = array( 'https://example.com/new' );
 
 		// Set up the mock
@@ -141,16 +177,16 @@ public function test_schedule_preload_single() {
 
 		Functions\expect( 'wp_schedule_single_event' )
 			->once()
-			->with( Mockery::type( 'int' ), 'jetpack_boost_preload_pages', array( $new_url ) );
+			->with( Mockery::type( 'int' ), 'jetpack_boost_preload', array( $new_url ) );
 
-		$preload->schedule_preload_cronjob( $new_url );
+		$preload->schedule( $new_url );
 		$this->expectNotToPerformAssertions();
 	}
 
 	/**
-	 * Test schedule_preload_cronjob with multiple URLs.
+	 * Test schedule with multiple URLs.
 	 */
-	public function test_schedule_preload_multiple() {
+	public function test_schedule_multiple() {
 		$new_urls = array( 'https://example.com/new1', 'https://example.com/new2' );
 
 		// Set up the mock
@@ -158,9 +194,9 @@ public function test_schedule_preload_multiple() {
 
 		Functions\expect( 'wp_schedule_single_event' )
 			->once()
-			->with( Mockery::type( 'int' ), 'jetpack_boost_preload_pages', array( $new_urls ) );
+			->with( Mockery::type( 'int' ), 'jetpack_boost_preload', array( $new_urls ) );
 
-		$preload->schedule_preload_cronjob( $new_urls );
+		$preload->schedule( $new_urls );
 		$this->expectNotToPerformAssertions();
 	}
 
@@ -185,7 +221,7 @@ public function test_handle_post_update_cornerstone() {
 
 		Functions\expect( 'wp_schedule_single_event' )
 			->once()
-			->with( Mockery::type( 'int' ), 'jetpack_boost_preload_pages', array( array( $permalink ) ) );
+			->with( Mockery::type( 'int' ), 'jetpack_boost_preload', array( array( $permalink ) ) );
 
 		$preload = new Cache_Preload();
 		$preload->handle_post_update( $post_id );
@@ -208,7 +244,7 @@ public function test_handle_post_update_not_cornerstone() {
 		// Set up the mock
 		$preload = new Cache_Preload();
 
-		// The schedule_preload_cronjob should never be called because the post is not a cornerstone page
+		// The schedule should never be called because the post is not a cornerstone page
 		Functions\expect( 'get_permalink' )->never();
 		Functions\expect( 'wp_schedule_single_event' )->never();
 
@@ -231,7 +267,7 @@ public function test_handle_cache_invalidation_all() {
 
 		Functions\expect( 'wp_schedule_single_event' )
 			->once()
-			->with( Mockery::type( 'int' ), 'jetpack_boost_preload_pages', array( $cornerstone_pages ) );
+			->with( Mockery::type( 'int' ), 'jetpack_boost_preload', array( $cornerstone_pages ) );
 
 		// Set up the mock
 		$preload = new Cache_Preload();
@@ -254,7 +290,7 @@ public function test_handle_cache_invalidation_specific_cornerstone() {
 
 		Functions\expect( 'wp_schedule_single_event' )
 			->once()
-			->with( Mockery::type( 'int' ), 'jetpack_boost_preload_pages', array( array( $path ) ) );
+			->with( Mockery::type( 'int' ), 'jetpack_boost_preload', array( array( $path ) ) );
 
 		$preload = new Cache_Preload();
 		$preload->handle_cache_invalidation( $path, Filesystem_Utils::DELETE_FILE );
@@ -292,7 +328,7 @@ public function test_setup() {
 
 		Functions\expect( 'add_action' )
 			->once()
-			->withArgs( array( 'jetpack_boost_preload_pages', Mockery::type( 'array' ) ) );
+			->withArgs( array( 'jetpack_boost_preload', Mockery::type( 'array' ) ) );
 
 		Functions\expect( 'add_action' )
 			->once()
@@ -306,4 +342,45 @@ public function test_setup() {
 		$preload->setup();
 		$this->expectNotToPerformAssertions();
 	}
+
+	/**
+	 * Test the activate method.
+	 */
+	public function test_activate() {
+		$cornerstone_pages = array( 'https://example.com', 'https://example.com/page' );
+
+		// Mock the Cornerstone_Utils::get_list method
+		$cornerstone_utils = Mockery::mock( 'alias:' . Cornerstone_Utils::class );
+		$cornerstone_utils->shouldReceive( 'get_list' )
+			->once()
+			->andReturn( $cornerstone_pages );
+
+		Functions\expect( 'wp_schedule_single_event' )
+			->once()
+			->with( Mockery::type( 'int' ), 'jetpack_boost_preload', array( $cornerstone_pages ) );
+
+		Cache_Preload::activate();
+		$this->expectNotToPerformAssertions();
+	}
+
+	/**
+	 * Test schedule_cornerstone method.
+	 */
+	public function test_schedule_cornerstone() {
+		$cornerstone_pages = array( 'https://example.com', 'https://example.com/page' );
+
+		// Mock the Cornerstone_Utils::get_list method
+		$cornerstone_utils = Mockery::mock( 'alias:' . Cornerstone_Utils::class );
+		$cornerstone_utils->shouldReceive( 'get_list' )
+			->once()
+			->andReturn( $cornerstone_pages );
+
+		Functions\expect( 'wp_schedule_single_event' )
+			->once()
+			->with( Mockery::type( 'int' ), 'jetpack_boost_preload', array( $cornerstone_pages ) );
+
+		$preload = new Cache_Preload();
+		$preload->schedule_cornerstone();
+		$this->expectNotToPerformAssertions();
+	}
 }


Reviews:
user: login: LiamSarsfield
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:18:45Z
user: login: haqadn
body: The implementation makes complete sense. I did catch a tiny issue where two methods were doing the same thing. I think we can remove one and merge this.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T18:55:06Z
user: login: LiamSarsfield
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T09:33:07Z

Review comments:
diff_hunk: @@ -51,14 +53,63 @@ public static function is_available() {
 		return false;
 	}
 
+	/**
+	 * Schedules a regular cronjob to rebuild the Cornerstone Pages every 30 minutes.
+	 *
+	 * @since $$next-version$$
+	 * @return void
+	 */
+	public function schedule_cornerstone_rebuild_cronjob() {
+		// TODO: Update this to run every 30 minutes.
+		wp_schedule_event( time(), 'hourly', 'jetpack_boost_rebuild', Cornerstone_Utils::get_list() );
path: projects/plugins/boost/app/modules/optimizations/page-cache/class-cache-preload.php
user: login: LiamSarsfield
body: Haven't updated this yet, but will in a bit.
created_at: 2025-03-19T13:18:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,55 +63,90 @@ public static function activate() {
 	}
 
 	/**
-	 * Schedule preload for all Cornerstone Pages.
+	 * Schedule rebuild for all Cornerstone Pages.
 	 *
 	 * This method is triggered when the Cornerstone Pages list is updated,
-	 * ensuring all Cornerstone Pages have their cache preloaded.
+	 * ensuring all Cornerstone Pages have their cache rebuilt, and hence preloaded.
 	 *
-	 * @since 3.11.0
+	 * @since $$next-version$$
 	 * @return void
 	 */
-	public function schedule_cornerstone_preload() {
-		$this->schedule_preload_cronjob( Cornerstone_Utils::get_list() );
+	public function schedule_cornerstone() {
+		$this->schedule( Cornerstone_Utils::get_list() );
+	}
+
+	/**
+	 * Schedule a rebuild for the given URLs.
+	 *
+	 * @since $$next-version$$
+	 * @param array $urls The URLs of the Cornerstone Pages to rebuild.
+	 * @return void
+	 */
+	public function schedule( array $urls ) {
+		Logger::debug( sprintf( 'Scheduling preload for %d pages', count( $urls ) ) );
+		wp_schedule_single_event( time(), 'jetpack_boost_preload', array( $urls ) );
+	}
+
+	/**
+	 * Rebuild the cache for the given URLs.
+	 *
+	 * @since $$next-version$$
+	 * @param array $urls The URLs of the Cornerstone Pages to preload.
+	 * @return void
+	 */
+	public function preload( array $urls ) {
+		Logger::debug( sprintf( 'Preload started for %d pages', count( $urls ) ) );
+
+		// Pause the hook here to avoid an infinite loop of: invalidation → preload → invalidation.
+		remove_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ) );
+		$boost_cache = new Boost_Cache();
+		foreach ( $urls as $url ) {
+			$boost_cache->invalidate_cache_for_url( $url, Filesystem_Utils::REBUILD_FILES );
+			$this->request_page( $url );
+		}
+		add_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ), 10, 2 );
+
+		Logger::debug( sprintf( 'Preload completed for %d pages', count( $urls ) ) );
 	}
 
 	/**
-	 * Schedules the preload cronjob, if not already scheduled
-	 * to execute within 10 minutes with the same arguments, per wp_schedule_single_event.
+	 * Schedule preload for all Cornerstone Pages.
+	 *
+	 * This method is triggered when the Cornerstone Pages list is updated,
+	 * ensuring all Cornerstone Pages have their cache preloaded.
 	 *
 	 * @since 3.11.0
-	 * @param array $posts The posts to preload.
 	 * @return void
 	 */
-	public function schedule_preload_cronjob( array $posts ) {
-		wp_schedule_single_event( time(), 'jetpack_boost_preload_pages', array( $posts ) );
+	public function schedule_cornerstone_preload() {
+		$this->schedule( Cornerstone_Utils::get_list() );
 	}
path: projects/plugins/boost/app/modules/optimizations/page-cache/class-cache-preload.php
user: login: haqadn
body: Seems like this is identical to the `schedule_cornerstone` method above.
created_at: 2025-03-21T18:50:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,55 +63,90 @@ public static function activate() {
 	}
 
 	/**
-	 * Schedule preload for all Cornerstone Pages.
+	 * Schedule rebuild for all Cornerstone Pages.
 	 *
 	 * This method is triggered when the Cornerstone Pages list is updated,
-	 * ensuring all Cornerstone Pages have their cache preloaded.
+	 * ensuring all Cornerstone Pages have their cache rebuilt, and hence preloaded.
 	 *
-	 * @since 3.11.0
+	 * @since $$next-version$$
 	 * @return void
 	 */
-	public function schedule_cornerstone_preload() {
-		$this->schedule_preload_cronjob( Cornerstone_Utils::get_list() );
+	public function schedule_cornerstone() {
+		$this->schedule( Cornerstone_Utils::get_list() );
+	}
+
+	/**
+	 * Schedule a rebuild for the given URLs.
+	 *
+	 * @since $$next-version$$
+	 * @param array $urls The URLs of the Cornerstone Pages to rebuild.
+	 * @return void
+	 */
+	public function schedule( array $urls ) {
+		Logger::debug( sprintf( 'Scheduling preload for %d pages', count( $urls ) ) );
+		wp_schedule_single_event( time(), 'jetpack_boost_preload', array( $urls ) );
+	}
+
+	/**
+	 * Rebuild the cache for the given URLs.
+	 *
+	 * @since $$next-version$$
+	 * @param array $urls The URLs of the Cornerstone Pages to preload.
+	 * @return void
+	 */
+	public function preload( array $urls ) {
+		Logger::debug( sprintf( 'Preload started for %d pages', count( $urls ) ) );
+
+		// Pause the hook here to avoid an infinite loop of: invalidation → preload → invalidation.
+		remove_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ) );
+		$boost_cache = new Boost_Cache();
+		foreach ( $urls as $url ) {
+			$boost_cache->invalidate_cache_for_url( $url, Filesystem_Utils::REBUILD_FILES );
+			$this->request_page( $url );
+		}
+		add_action( 'jetpack_boost_invalidate_cache_success', array( $this, 'handle_cache_invalidation' ), 10, 2 );
+
+		Logger::debug( sprintf( 'Preload completed for %d pages', count( $urls ) ) );
 	}
 
 	/**
-	 * Schedules the preload cronjob, if not already scheduled
-	 * to execute within 10 minutes with the same arguments, per wp_schedule_single_event.
+	 * Schedule preload for all Cornerstone Pages.
+	 *
+	 * This method is triggered when the Cornerstone Pages list is updated,
+	 * ensuring all Cornerstone Pages have their cache preloaded.
 	 *
 	 * @since 3.11.0
-	 * @param array $posts The posts to preload.
 	 * @return void
 	 */
-	public function schedule_preload_cronjob( array $posts ) {
-		wp_schedule_single_event( time(), 'jetpack_boost_preload_pages', array( $posts ) );
+	public function schedule_cornerstone_preload() {
+		$this->schedule( Cornerstone_Utils::get_list() );
 	}
path: projects/plugins/boost/app/modules/optimizations/page-cache/class-cache-preload.php
user: login: LiamSarsfield
body: Yep, you're right. I've addressed this in [#42611](https://github.com/Automattic/jetpack/pull/42611/files#diff-ed6265c8baf885fc295700dfaad03787da9a32f13914ab8d19d441b3367e3169L121)
created_at: 2025-03-24T09:33:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-18T17:38:53Z
body: **Thank you for your PR!**

When contributing to Jetpack, we have [a few suggestions](https://github.com/Automattic/jetpack/blob/trunk/.github/PULL_REQUEST_TEMPLATE.md) that can help us test and review your patch:<br>
- :white_check_mark: Include a description of your PR changes.<br>
- :white_check_mark: Add a "[Status]" label (In Progress, Needs Review, ...).<br>
- :white_check_mark: Add a "[Type]" label (Bug, Enhancement, Janitorial, Task).<br>
- :white_check_mark: Add testing instructions.<br>
- :white_check_mark: Specify whether this PR includes any changes to data or privacy.<br>
- :white_check_mark: Add changelog entries to affected projects<br>


This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation :robot:

******

**Follow this PR Review Process:**

1. Ensure all required checks appearing at the bottom of this PR are passing.
2. Make sure to test your changes on all platforms that it applies to. *You're responsible for the quality of the code you ship*.
3. You can use [GitHub's Reviewers functionality](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) to request a review.
4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!
******

**Boost plugin:**

- Next scheduled release: _April 2, 2025_


If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

author_association: CONTRIBUTOR
user: login: jp-launch-control[bot]
created_at: 2025-03-18T17:41:41Z
body: ### Code Coverage Summary

Coverage changed in 9 files. Only the first 5 are listed here.

| File | Coverage | Δ% | Δ Uncovered |
| :--- | :------: | :---: | :---: |
| projects/plugins/boost/app/modules/optimizations/page-cache/class-cache-preload.php | 45/49 (91.84%) | 7.22% | -2 💚 |
| projects/plugins/boost/app/modules/optimizations/page-cache/pre-wordpress/Boost&#95;Cache&#95;Error.php | 2/8 (25.00%) | 25.00% | -2 💚 |
| projects/plugins/boost/app/modules/optimizations/page-cache/pre-wordpress/Logger.php | 3/64 (4.69%) | 4.69% | -3 💚 |
| projects/plugins/boost/app/modules/optimizations/page-cache/pre-wordpress/Boost&#95;Cache&#95;Actions.php | 4/12 (33.33%) | 33.33% | -4 💚 |
| projects/plugins/boost/app/modules/optimizations/page-cache/pre-wordpress/storage/File&#95;Storage.php | 9/59 (15.25%) | 15.25% | -9 💚 |



[Full summary](https://jetpackcodecoverage.atomicsites.blog/prs/42549/) · [PHP report](https://jetpackcodecoverage.atomicsites.blog/prs/42549/php/) · [JS report](https://jetpackcodecoverage.atomicsites.blog/prs/42549/js/)
author_association: NONE
user: login: haqadn
created_at: 2025-03-21T19:10:46Z
body: I noticed something off in JN. I found that some URLs get stuck in the rebuild state.
author_association: CONTRIBUTOR
user: login: LiamSarsfield
created_at: 2025-03-21T19:28:59Z
body: Is that with Edge Caching disabled?

On Fri 21 Mar 2025, 19:11 Adnan Haque, ***@***.***> wrote:

> I noticed something off in JN. I found that some URLs get stuck in the
> rebuild state.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/Automattic/jetpack/pull/42549#issuecomment-2744238267>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AKLF5ZOWAYLWYULN66FCUBL2VRP57AVCNFSM6AAAAABZIZJP7SVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDONBUGIZTQMRWG4>
> .
> You are receiving this because you were assigned.Message ID:
> ***@***.***>
> [image: haqadn]*haqadn* left a comment (Automattic/jetpack#42549)
> <https://github.com/Automattic/jetpack/pull/42549#issuecomment-2744238267>
>
> I noticed something off in JN. I found that some URLs get stuck in the
> rebuild state.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/Automattic/jetpack/pull/42549#issuecomment-2744238267>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AKLF5ZOWAYLWYULN66FCUBL2VRP57AVCNFSM6AAAAABZIZJP7SVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDONBUGIZTQMRWG4>
> .
> You are receiving this because you were assigned.Message ID:
> ***@***.***>
>

author_association: CONTRIBUTOR
user: login: haqadn
created_at: 2025-03-21T20:32:11Z
body: > Is that with Edge Caching disabled?
> […](#)
> On Fri 21 Mar 2025, 19:11 Adnan Haque, ***@***.***> wrote: I noticed something off in JN. I found that some URLs get stuck in the rebuild state. — Reply to this email directly, view it on GitHub <[#42549 (comment)](https://github.com/Automattic/jetpack/pull/42549#issuecomment-2744238267)>, or unsubscribe <[github.com/notifications/unsubscribe-auth/AKLF5ZOWAYLWYULN66FCUBL2VRP57AVCNFSM6AAAAABZIZJP7SVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDONBUGIZTQMRWG4](https://github.com/notifications/unsubscribe-auth/AKLF5ZOWAYLWYULN66FCUBL2VRP57AVCNFSM6AAAAABZIZJP7SVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDONBUGIZTQMRWG4)> . You are receiving this because you were assigned.Message ID: ***@***.***> [image: haqadn]*haqadn* left a comment ([Automattic/jetpack#42549](https://github.com/Automattic/jetpack/pull/42549)) <[#42549 (comment)](https://github.com/Automattic/jetpack/pull/42549#issuecomment-2744238267)> I noticed something off in JN. I found that some URLs get stuck in the rebuild state. — Reply to this email directly, view it on GitHub <[#42549 (comment)](https://github.com/Automattic/jetpack/pull/42549#issuecomment-2744238267)>, or unsubscribe <[github.com/notifications/unsubscribe-auth/AKLF5ZOWAYLWYULN66FCUBL2VRP57AVCNFSM6AAAAABZIZJP7SVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDONBUGIZTQMRWG4](https://github.com/notifications/unsubscribe-auth/AKLF5ZOWAYLWYULN66FCUBL2VRP57AVCNFSM6AAAAABZIZJP7SVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDONBUGIZTQMRWG4)> . You are receiving this because you were assigned.Message ID: ***@***.***>

Ah! forgot to disable edge caching before testing.
author_association: CONTRIBUTOR