diff --git a/src/Console/Commands/StaticWarm.php b/src/Console/Commands/StaticWarm.php index b40908cbe5f..4ff482b5b74 100644 --- a/src/Console/Commands/StaticWarm.php +++ b/src/Console/Commands/StaticWarm.php @@ -84,8 +84,6 @@ public function handle() private function warm(): void { - $client = new Client($this->clientConfig()); - $this->output->newLine(); $this->line('Compiling URLs...'); @@ -109,7 +107,7 @@ private function warm(): void } else { $this->line('Visiting '.count($requests).' URLs...'); - $pool = new Pool($client, $requests, [ + $pool = new Pool($this->client(), $requests, [ 'concurrency' => $this->concurrency(), 'fulfilled' => [$this, 'outputSuccessLine'], 'rejected' => [$this, 'outputFailureLine'], @@ -121,6 +119,33 @@ private function warm(): void } } + private function warmPaginatedPages(string $url, int $currentPage, int $totalPages, string $pageName): void + { + $urls = collect(range($currentPage, $totalPages))->map(function ($page) use ($url, $pageName) { + $url = "{$url}?{$pageName}={$page}"; + + if (config('statamic.static_caching.background_recache', false)) { + $url = RecacheToken::addToUrl($url); + } + + return $url; + }); + + $requests = $urls->map(fn (string $url) => new Request('GET', $url))->all(); + + $pool = new Pool($this->client(), $requests, [ + 'concurrency' => $this->concurrency(), + 'fulfilled' => function (Response $response, $index) use ($urls) { + $this->components->twoColumnDetail($this->getRelativeUri($urls->get($index)), '✓ Cached'); + }, + 'rejected' => [$this, 'outputFailureLine'], + ]); + + $promise = $pool->promise(); + + $promise->wait(); + } + private function concurrency(): int { $strategy = config('statamic.static_caching.strategy'); @@ -128,6 +153,11 @@ private function concurrency(): int return config("statamic.static_caching.strategies.$strategy.warm_concurrency", 25); } + private function client(): Client + { + return new Client($this->clientConfig()); + } + private function clientConfig(): array { return [ @@ -140,12 +170,18 @@ private function clientConfig(): array public function outputSuccessLine(Response $response, $index): void { - $this->components->twoColumnDetail($this->getRelativeUri($index), '✓ Cached'); + $this->components->twoColumnDetail($this->getRelativeUri($this->uris()->get($index)), '✓ Cached'); + + if ($response->hasHeader('X-Statamic-Pagination')) { + [$currentPage, $totalPages, $pageName] = $response->getHeader('X-Statamic-Pagination'); + + $this->warmPaginatedPages($this->uris()->get($index), $currentPage, $totalPages, $pageName); + } } public function outputFailureLine($exception, $index): void { - $uri = $this->getRelativeUri($index); + $uri = $this->getRelativeUri($this->uris()->get($index)); if ($exception instanceof RequestException && $exception->hasResponse()) { $response = $exception->getResponse(); @@ -162,9 +198,9 @@ public function outputFailureLine($exception, $index): void $this->components->twoColumnDetail($uri, "$message"); } - private function getRelativeUri(int $index): string + private function getRelativeUri(string $uri): string { - return Str::start(Str::after($this->uris()->get($index), config('app.url')), '/'); + return Str::start(Str::after($uri, config('app.url')), '/'); } private function requests() diff --git a/src/Console/Commands/StaticWarmJob.php b/src/Console/Commands/StaticWarmJob.php index 4ae92b1b429..31298d97e4a 100644 --- a/src/Console/Commands/StaticWarmJob.php +++ b/src/Console/Commands/StaticWarmJob.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Psr\Http\Message\ResponseInterface; class StaticWarmJob implements ShouldBeUnique, ShouldQueue { @@ -24,6 +25,36 @@ public function __construct(public Request $request, public array $clientConfig) public function handle() { - (new Client($this->clientConfig))->send($this->request); + $response = (new Client($this->clientConfig))->send($this->request); + + if ($this->shouldWarmPaginatedPages($response)) { + [$currentPage, $totalPages, $pageName] = $response->getHeader('X-Statamic-Pagination'); + + collect(range($currentPage, $totalPages)) + ->map(function (int $page) use ($pageName): string { + $url = $this->request->getUri(); + + return implode('', [ + $url, + str_contains($url, '?') ? '&' : '?', + "{$pageName}={$page}", + ]); + }) + ->each(fn (string $uri) => StaticWarmJob::dispatch( + new Request('GET', $uri), + $this->clientConfig + )); + } + } + + private function shouldWarmPaginatedPages(ResponseInterface $response): bool + { + if (! $response->hasHeader('X-Statamic-Pagination')) { + return false; + } + + [$currentPage, $totalPages, $pageName] = $response->getHeader('X-Statamic-Pagination'); + + return ! str_contains($this->request->getUri()->getQuery(), "{$pageName}="); } } diff --git a/src/StaticCaching/Middleware/Cache.php b/src/StaticCaching/Middleware/Cache.php index ddbe646e4e0..2323462120c 100644 --- a/src/StaticCaching/Middleware/Cache.php +++ b/src/StaticCaching/Middleware/Cache.php @@ -10,6 +10,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache as AppCache; use Illuminate\Support\Facades\Log; +use Statamic\Facades\Blink; use Statamic\Facades\StaticCache; use Statamic\Statamic; use Statamic\StaticCaching\Cacher; @@ -83,6 +84,16 @@ private function handleRequest($request, Closure $next) $this->makeReplacementsAndCacheResponse($request, $response); $this->nocache->write(); + + if ($paginator = Blink::get('tag-paginator')) { + if ($paginator->hasMorePages()) { + $response->headers->set('X-Statamic-Pagination', [ + 'current' => $paginator->currentPage(), + 'total' => $paginator->lastPage(), + 'name' => $paginator->getPageName(), + ]); + } + } } elseif (! $response->isRedirect()) { $this->makeReplacements($response); } diff --git a/tests/Console/Commands/StaticWarmJobTest.php b/tests/Console/Commands/StaticWarmJobTest.php index b92c0e2941c..ab2695e61a8 100644 --- a/tests/Console/Commands/StaticWarmJobTest.php +++ b/tests/Console/Commands/StaticWarmJobTest.php @@ -6,6 +6,7 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; +use Illuminate\Support\Facades\Queue; use PHPUnit\Framework\Attributes\Test; use Statamic\Console\Commands\StaticWarmJob; use Tests\TestCase; @@ -27,4 +28,66 @@ public function it_sends_a_get_request() $this->assertEquals('/about', $mock->getLastRequest()->getUri()->getPath()); } + + #[Test] + public function it_sends_a_get_request_and_dispatches_static_warm_job_for_page_with_pagination() + { + Queue::fake(); + + $mock = new MockHandler([ + (new Response(200))->withHeader('X-Statamic-Pagination', [ + 'current' => 1, + 'total' => 3, + 'name' => 'page', + ]), + ]); + + $handlerStack = HandlerStack::create($mock); + + $job = new StaticWarmJob(new Request('GET', '/blog'), ['handler' => $handlerStack]); + + $job->handle(); + + $this->assertEquals('/blog', $mock->getLastRequest()->getUri()->getPath()); + + Queue::assertPushed(StaticWarmJob::class, function (StaticWarmJob $job) { + return $job->request->getUri()->getPath() === '/blog' + && $job->request->getUri()->getQuery() === 'page=1'; + }); + + Queue::assertPushed(StaticWarmJob::class, function (StaticWarmJob $job) { + return $job->request->getUri()->getPath() === '/blog' + && $job->request->getUri()->getQuery() === 'page=2'; + }); + + Queue::assertPushed(StaticWarmJob::class, function (StaticWarmJob $job) { + return $job->request->getUri()->getPath() === '/blog' + && $job->request->getUri()->getQuery() === 'page=3'; + }); + } + + #[Test] + public function subsequent_paginated_pages_dont_dispatch_static_warm_jobs() + { + Queue::fake(); + + $mock = new MockHandler([ + (new Response(200))->withHeader('X-Statamic-Pagination', [ + 'current' => 2, + 'total' => 3, + 'name' => 'page', + ]), + ]); + + $handlerStack = HandlerStack::create($mock); + + $job = new StaticWarmJob(new Request('GET', '/blog?page=2'), ['handler' => $handlerStack]); + + $job->handle(); + + $this->assertEquals('/blog', $mock->getLastRequest()->getUri()->getPath()); + + // The first page is responsible for dispatchin jobs. Not subsequent pages. + Queue::assertNothingPushed(); + } }