Laravel Page Cache
#Laravel Page Cache
This Laravel package provides a full-page response cache middleware for stateless public GET pages. It caches 200 responses keyed by a version token, locale, and theme cookie, skips authenticated requests, exposes an X-Page-Cache HIT/MISS header, and offers a static flush() helper to invalidate every cached page at once.
#Installation
You can install the package via composer:
composer require jeffersongoncalves/laravel-page-cache
You can publish the config file with:
php artisan vendor:publish --tag="laravel-page-cache-config"
This is the contents of the published config file:
return [ 'enabled' => env('PAGE_CACHE_ENABLED', true), // A TTL of 0 (or below) means "cache forever". 'ttl' => (int) env('PAGE_CACHE_TTL', 3600), // Fold a normalized query string into the cache key (safe default). 'include_query_string' => env('PAGE_CACHE_INCLUDE_QUERY_STRING', true), 'key' => [ 'locale' => true, // Vary the cache on the negotiated content encoding. 'accept_encoding' => true, 'theme' => [ 'enabled' => true, 'cookie' => 'theme', ], ], ];
#Usage
Register CachePublicPage after StartSession and the authentication middleware — i.e. inside your web middleware group, not as the outermost middleware. The cache deliberately depends on a started session to tell guests from authenticated users, and registering it before StartSession would make it bail out on every request:
use Illuminate\Support\Facades\Route; use JeffersonGoncalves\PageCache\Middleware\CachePublicPage; Route::middleware(['web', CachePublicPage::class])->group(function () { Route::get('/', HomeController::class); Route::get('/{slug}', ShowController::class); });
The first request to a path is computed normally and stored with an X-Page-Cache: MISS header. Subsequent requests are served straight from the cache with an X-Page-Cache: HIT header. Only stateless GET requests that return a 200 response from a guest (unauthenticated) visitor are cached, and the full response header bag (minus Set-Cookie) is replayed on a cache hit.
#What is never cached (important)
To avoid leaking per-visitor state across visitors, the middleware passes the request through untouched (no read, no write) when any of the following is true:
- the request is authenticated (
$request->user()is notnull); - there is no started session (so register it after
StartSession); - the response sets its own cookies (
Set-Cookie); - the response sends
Cache-Control: no-store.
Cache-Control: no-store is the explicit opt-out you should reach for whenever a page renders per-visitor state. (Only no-store is honoured: Symfony stamps the default Cache-Control: no-cache, private on every response that does not set its own cache headers, so no-cache/private cannot be used as opt-out signals without disabling the cache for every page.)
Session/CSRF limitation — read this. Laravel flushes queued cookies (the session cookie,
XSRF-TOKEN, flash data) into the response after this middleware has already inspected it, so theSet-Cookieguard above cannot see them. A page that embeds a@csrftoken in a<form>therefore looks cacheable, and caching it would replay one visitor's CSRF token to everyone else. Any response that contains a CSRF token, a form, flash messages, or other guest-session content must mark itself withCache-Control: no-store(e.g.return response($html)->header('Cache-Control', 'no-store');) or be kept out of the cached route group entirely. There is no reliable way for the middleware to detect this for you.
#Invalidating the cache
Call CachePublicPage::flush() from your model observers to invalidate every cached page whenever the underlying content changes:
use JeffersonGoncalves\PageCache\Middleware\CachePublicPage; class ProjectObserver { public function saved(Project $project): void { CachePublicPage::flush(); } public function deleted(Project $project): void { CachePublicPage::flush(); } }
flush() bumps an internal version token, so every previously cached page is bypassed on the next request without touching individual cache keys.
#Cache key
By default the cache key is composed of the version token, the current locale, the negotiated Accept-Encoding, the theme cookie value, a hash of the request path, and a hash of a normalized (sorted) query string. You can disable the locale, accept_encoding, or theme segments — or change the theme cookie name — through the config file. The Accept-Encoding segment is normalized (tokens lowercased and sorted) so a body compressed for a gzip/br client is never replayed to a client that cannot decode it.
The path (not the full URL) is used for the path segment, and the query string is normalized and hashed separately so that ?b=2&a=1 and ?a=1&b=2 collapse to the same entry while /products?page=2 stays distinct from /products?page=1. If a route ignores the query string entirely you can drop it from the key by setting include_query_string to false.
#Testing
composer test
#Changelog
Please see CHANGELOG for more information on what has changed recently.
#Contributing
Please see CONTRIBUTING for details.
#Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
#Credits
#License
The MIT License (MIT). Please see License File for more information.