Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Page Cache Laravel Package

silber/page-cache

Silber/page-cache gives Laravel apps fast full-page caching by saving rendered responses as static HTML files that the web server serves before the framework boots. Includes cache tags, automatic invalidation, middleware support, and configurable storage for dramatically faster page loads.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish the config

composer require silber/page-cache
php artisan vendor:publish --tag=page-cache-config

2. Register the middleware. The package ships Silber\PageCache\Middleware\CacheResponse, which writes the finished response body to a static file.

Laravel 11+/12/13 — bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(prepend: [
        \Silber\PageCache\Middleware\CacheResponse::class,
    ]);
})

Laravel 10 and below — app/Http/Kernel.php, top of $middlewareGroups['web']. Prefer putting it early in the group (outermost) so it captures the body after every inner middleware has finished transforming it.

3. Teach the web server to serve the files before PHP. This is the whole point — a cache hit must never boot Laravel. Copy the Nginx/Apache blocks from the README rather than hand-rolling; the sketch is roughly:

location = / {
    try_files /page-cache/index.html @laravel;
}
location / {
    try_files /page-cache$uri.html /page-cache$uri/index.html $uri @laravel;
}

4. Clear on demand

php artisan page-cache:clear          # everything
php artisan page-cache:clear about    # a single path

Where to look first: config/page-cache.php (the cache_path key is the one you'll actually touch), then read CacheResponse and Silber\PageCache\PageCache — both are small, and knowing exactly what shouldCacheRequest() / shouldCacheResponse() check saves you hours of guessing why a page didn't cache.

First use case: anonymous, GET-only, high-traffic marketing pages — homepage, landing pages, blog posts, category listings. That's where the win is (Laravel boot skipped entirely, sub-10ms TTFB) and where the risk of leaking per-user state is lowest.

Implementation Patterns

Scope the cache with a route group. Rather than the global web group, create an explicit group and attach it only to pages you've vetted:

Route::middleware('page-cache')->group(function () {
    Route::get('/', [HomeController::class, 'index']);
    Route::get('/blog/{post:slug}', [PostController::class, 'show']);
});

This keeps authenticated dashboards, search results, and anything session-dependent out of the cache by default — opt in, never opt out.

Invalidate from your models, not your memory. The command is the API; call it from observers when content changes:

class PostObserver
{
    public function saved(Post $post): void
    {
        Artisan::queue('page-cache:clear', ['path' => "blog/{$post->slug}"]);
    }
}

Use queue after commit so a rollback doesn't nuke a good cache. For whole-site purges, hook the deploy script instead (see Gotchas).

Customize rules by rebinding the service. The middleware resolves Silber\PageCache\PageCache from the container, so subclass it and swap the binding in a service provider:

class AppPageCache extends \Silber\PageCache\PageCache
{
    public function shouldCacheRequest(Request $request): bool
    {
        return parent::shouldCacheRequest($request)
            && ! $request->hasAny(['q', 'page', 'utm_source'])
            && ! Auth::check();
    }
}

$this->app->singleton(PageCache::class, AppPageCache::class);

This is the cleanest extension point; check the parent method signatures in the installed version before overriding.

Warm the cache after a clear. A purge drops you to an all-miss state right when traffic arrives. Fire a handful of Http::get() calls from a queued job (or your deploy script) for your top URLs.

Test it without hitting the web server. In a feature test, point the cache somewhere disposable and assert the file exists:

config(['page-cache.cache_path' => storage_path('framework/testing/page-cache')]);

$this->get('/blog/hello')->assertOk();
$this->assertFileExists(storage_path('framework/testing/page-cache/blog/hello.html'));

That test is your regression guard against someone accidentally caching a personalized page.

Gotchas and Tips

A cache hit is not a Laravel request. No middleware, no session, no auth, no logging, no view counters, no Model::increment. Any analytics must be client-side (JS), and anything that must run per-request must not be on a cached page.

Stale CSRF tokens will silently break forms. The token is baked into the HTML at cache time and belongs to a session the visitor doesn't have. Either keep forms off cached pages, hand the token to the page via JS from a non-cached endpoint, or exclude the route entirely. This is the single most common production surprise with this package.

Never cache personalized output. Greetings, carts, flash messages, @auth blocks, locale-switched strings, A/B-test variants. The package has no Vary support — Accept-Language, cookies, and device type are all invisible to it. If a page varies, it isn't a caching candidate.

Response headers set in Laravel don't reach the browser. Cache-Control, security headers, X-Frame-Options for cached pages must be configured in Nginx/Apache for the page-cache location. Same for gzip/brotli — the web server handles it, which is fine, but verify.

Query strings collide with $uri matching. Nginx matches on the path only, so /search?q=shoes can be served the cached /search file. Don't cache routes that read query parameters, and add a guard in your shouldCacheRequest() override.

Trailing slashes create duplicates. /about and /about/ produce different files and two cache entries. Pick one form, enforce it with a canonical redirect at the web server.

Stale files outlive deleted routes. The web server checks disk before Laravel, so removing a route doesn't remove its cached HTML — you'll keep serving a 200 for a page that no longer exists. Clear the cache on every deploy, and add public/page-cache to .gitignore while you're at it. On atomic/symlink deploys, remember the cache lives inside the release directory unless you repoint cache_path at shared storage; either clear after the symlink swaps or relocate the path.

Permissions. The directory must be writable by the PHP-FPM user. If pages mysteriously don't cache, check storage/logs/laravel.log and the directory owner first — the middleware swallows failures more quietly than you'd like.

Debugging a miss: curl -I https://site/blog/hello and look for a header your app sets (e.g. X-Request-Id). Present → you hit PHP, so either the file isn't there or the try_files rule is wrong. ls -R public/page-cache and compare mtimes against your last deploy.

Only 200/HTML responses are worth writing. Redirects, 404s, and JSON either aren't cached or shouldn't be — if you need cached 301s or error pages, handle them at the web server layer instead.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo