From 9082cff07eb0ee57ceebb2ed58f52b304e330512 Mon Sep 17 00:00:00 2001 From: Ryan Mitchell Date: Tue, 1 Sep 2026 14:20:26 +0100 Subject: [PATCH] Fix background recache clearing tags --- src/Http/Middleware/CacheTracker.php | 29 +++++++++---- src/Tracker/Manager.php | 20 ++++++--- tests/Unit/TrackerTest.php | 41 ++++++++++++++++++- .../resources/views/nav-default.antlers.html | 3 ++ .../resources/views/nav-test.antlers.html | 3 ++ 5 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 tests/__fixtures__/resources/views/nav-default.antlers.html create mode 100644 tests/__fixtures__/resources/views/nav-test.antlers.html diff --git a/src/Http/Middleware/CacheTracker.php b/src/Http/Middleware/CacheTracker.php index b1a0cdf..1d17405 100644 --- a/src/Http/Middleware/CacheTracker.php +++ b/src/Http/Middleware/CacheTracker.php @@ -10,9 +10,10 @@ use Statamic\Contracts\Assets\Asset; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Globals\Variables; +use Statamic\Facades\StaticCache; use Statamic\Facades\URL; use Statamic\Forms; -use Statamic\StaticCaching\Cacher; +use Statamic\StaticCaching\RecacheToken; use Statamic\Structures\Nav; use Statamic\Structures\Page; use Statamic\Support\Str; @@ -42,19 +43,16 @@ public function handle($request, Closure $next) return $next($request); } - $cacher = app(Cacher::class); - - if ($cacher && $cacher->hasCachedPage($request)) { - return $next($request); - } - $url = $this->url(); if (Str::endsWith($url, '/')) { $url = substr($url, 0, -1); } - if (Tracker::has($url)) { + // A background-recache request re-renders a page that is still cached, so + // let it through even when the URL is already tracked - this is how the + // stored tags get refreshed when a page's content dependencies change. + if (! $this->isRecacheRequest($request) && Tracker::has($url)) { return $next($request); } @@ -102,6 +100,15 @@ private function isEnabled($request) return $request->method() === 'GET' && ! Str::startsWith($request->path(), [config('statamic.routes.action', '!').'/', config('statamic.assets.image_manipulation.route')]); } + private function isRecacheRequest($request): bool + { + if (! $token = $request->input(StaticCache::recacheTokenParameter())) { + return false; + } + + return StaticCache::checkRecacheToken($token); + } + private function setupAdditionalTracking() { $pipelines = Tracker::getAdditionalTrackers(); @@ -206,6 +213,10 @@ private function setupTagHooks() private function url() { - return URL::makeAbsolute(class_exists(Livewire::class) ? Livewire::originalUrl() : URL::getCurrent()); + $url = URL::makeAbsolute(class_exists(Livewire::class) ? Livewire::originalUrl() : URL::getCurrent()); + + // Strip the background-recache token so a recache request keys the same + // entry as the original page rather than creating an orphaned one. + return RecacheToken::removeFromUrl($url); } } diff --git a/src/Tracker/Manager.php b/src/Tracker/Manager.php index 9b9939b..82b9ec6 100644 --- a/src/Tracker/Manager.php +++ b/src/Tracker/Manager.php @@ -76,20 +76,30 @@ public function invalidate(array $tags = []) { $storeData = $this->all(); + // With background recaching the page is refreshed in place rather than + // deleted, and the recache request re-tracks the URL with fresh tags. We + // keep the existing entry so that if the recache job fails or is dropped + // the URL stays tracked and future saves will retry it, instead of being + // orphaned in the static cache with no tags. + $keepEntries = config('statamic.static_caching.background_recache', false); + $urls = []; foreach ($storeData as $key => $data) { - $storeTags = $data['tags']; - $url = $data['url']; + if (! $this->tagsMatch($tags, $data['tags'])) { + continue; + } - if ($this->tagsMatch($tags, $storeTags)) { - $urls[] = $url; + $urls[] = $data['url']; + if (! $keepEntries) { unset($storeData[$key]); } } if (! empty($urls)) { - $this->cacheStore()->forever($this->cacheKey, $storeData); + if (! $keepEntries) { + $this->cacheStore()->forever($this->cacheKey, $storeData); + } $this->invalidateUrls($urls); } diff --git a/tests/Unit/TrackerTest.php b/tests/Unit/TrackerTest.php index fa1e38d..6940475 100644 --- a/tests/Unit/TrackerTest.php +++ b/tests/Unit/TrackerTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Console\Commands\StaticWarmJob; use Statamic\Events\UrlInvalidated; +use Statamic\Facades\StaticCache; use Thoughtco\StatamicCacheTracker\Events\ContentTracked; use Thoughtco\StatamicCacheTracker\Facades\Tracker; use Thoughtco\StatamicCacheTracker\Tests\TestCase; @@ -244,6 +245,44 @@ public function it_queues_a_recache_job_instead_of_invalidating_when_background_ Queue::assertPushed(StaticWarmJob::class); Event::assertNotDispatched(UrlInvalidated::class); - $this->assertCount(0, Tracker::all()); + + // The entry is retained so that if the recache job fails or is dropped + // the URL stays tracked and future saves will retry it, instead of being + // orphaned in the static cache with no tags. + $this->assertCount(1, Tracker::all()); + $this->assertSame(['pages:home', 'collection:pages'], collect(Tracker::all())->first()['tags']); + } + + #[Test] + public function it_refreshes_tags_for_an_already_tracked_url_on_a_recache_request() + { + $this->get('/'); + + $this->assertSame(['pages:home', 'collection:pages'], collect(Tracker::all())->first()['tags']); + + Tracker::addAdditionalTracker(function ($tracker, $next) { + $tracker->addContentTag('fresh::tag'); + }); + + $token = StaticCache::recacheTokenParameter().'='.StaticCache::recacheToken(); + + $this->get('/?'.$token); + + $this->assertCount(1, Tracker::all()); + $this->assertSame(['fresh::tag', 'pages:home', 'collection:pages'], collect(Tracker::all())->first()['tags']); + } + + #[Test] + public function it_does_not_retrack_an_already_tracked_url_on_a_normal_request() + { + $this->get('/'); + + Tracker::addAdditionalTracker(function ($tracker, $next) { + $tracker->addContentTag('fresh::tag'); + }); + + $this->get('/'); + + $this->assertSame(['pages:home', 'collection:pages'], collect(Tracker::all())->first()['tags']); } } diff --git a/tests/__fixtures__/resources/views/nav-default.antlers.html b/tests/__fixtures__/resources/views/nav-default.antlers.html new file mode 100644 index 0000000..b891584 --- /dev/null +++ b/tests/__fixtures__/resources/views/nav-default.antlers.html @@ -0,0 +1,3 @@ +{{ nav }} + {{ title }} +{{ /nav }} \ No newline at end of file diff --git a/tests/__fixtures__/resources/views/nav-test.antlers.html b/tests/__fixtures__/resources/views/nav-test.antlers.html new file mode 100644 index 0000000..346dc39 --- /dev/null +++ b/tests/__fixtures__/resources/views/nav-test.antlers.html @@ -0,0 +1,3 @@ +{{ nav :handle="my_nav" }} + {{ title }} +{{ /nav }} \ No newline at end of file