Page Cache
Evolve comes with full response caching for pages. This can greatly improve the performance of your website by serving static HTML files instead of booting up Laravel for every request.
How it works
Usualy when a request comes in, your webserver (Apache/Nginx) redirects everything to public/index.php which starts PHP and spins up a new Laravel instance. Starting a new PHP process, booting Laravel, making database calls and doing other logic in your application sometimes takes a lot of time.
The Evolve Page Cache will do this only one time for each route and then save the full response body to a static HTML or JSON file on your webserver.
For example: When you visit the route /en/contact, the page cache will create a new file in public/page-cache/en/contact/index.html. Same goes for AJAX calls which result in index.json files inside the page-cache directory.
Now we can serve the content directly from our page-cache directory without even having to start PHP or Laravel.
WARNING
Be aware of the fact that you can only cache pages with static content! Every user will get the exact same cached file.
INFO
Good to know: Only GET requests with a successful response code (HTTP 2xx) will be cached.
Setup
Enabling the page cache
Enable the page cache on the Evolve Settings Page or just set APP_PAGE_CACHE=true. Outgoing responses will now be cached to disk and ready to be served for the next request.
Serving cached content
To be able to serve the static files correctly from our page-cache directory we'll have to tell Apache about it. Place this config just before the FrontController handling:
# Serve Cached Page If Available...
# (Exclude URLs with query strings and requests carrying "Cache-Control: no-cache")
RewriteCond %{QUERY_STRING} ^$
RewriteCond %{REQUEST_URI} ^/?$
RewriteCond %{DOCUMENT_ROOT}/page-cache/index.html -f
RewriteCond %{HTTP:Cache-Control} !no-cache
RewriteRule .? page-cache/index.html [L]
RewriteCond %{QUERY_STRING} ^$
RewriteCond %{DOCUMENT_ROOT}/page-cache%{REQUEST_URI}/index.html -f
RewriteCond %{HTTP:Cache-Control} !no-cache
RewriteRule . page-cache%{REQUEST_URI}/index.html [L]
RewriteCond %{QUERY_STRING} ^$
RewriteCond %{DOCUMENT_ROOT}/page-cache%{REQUEST_URI}/index.json -f
RewriteCond %{HTTP:Cache-Control} !no-cache
RewriteRule . page-cache%{REQUEST_URI}/index.json [L]These are the rules the evolve-starter ships. Requests with a query string are never written to the cache, so the rules must not serve a cached file for them either.
Security headers on cached pages
A cache hit never reaches PHP, so it carries none of the headers the Evolve middleware adds. Add them in the .htaccess as described in Security Headers.
Workaround for CSRF Tokens
Because CSRF tokens are session based, we cannot use the token in the <meta> tag anymore for making POST requests. We need to fetch it with Javascript in the frontend.
Replace the CSRF token part in the theme's src/app.ts with this:
// Add Laravel CSRF token to axios defaults
export const csrfReady = Http.get('/api/csrf-token').then(response => {
Http.defaults.headers.common['X-CSRF-TOKEN'] = response.data
Http.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
})Now, when doing POST requests, be sure to wait for the csrfReady promise to be fulfilled.
import { csrfToken } from '@app/app'
csrfToken.then(() => {
// Ready to do POST requests
// Http.post(...)
})Controlling which pages get cached
Exclude routes
You can exclude somes routes from the page cache by adding the path to the page-cache.ignore array. Asterisks (*) may be used as wildcard values.
'page-cache' => [
'ignore' => [
'/de/fahrzeuge',
'/de/shop/*',
]
]Routeables
With Evolve, your pages are usually served via a model which implements the Routeable interface. You can specify which models should get cached by returning true/false from the isPageCachable public method.
public function isPageCachable(): bool
{
$allowPageCache = /* some custom logic */ ;
return $allowPageCache;
}What is never written
A cache entry is written by one request and then served by the web server to every later visitor. It may therefore only contain what an anonymous visitor gets. Besides the ignore list and isPageCachable(), the cache refuses to write when any of these holds:
| Condition | Why |
|---|---|
APP_PAGE_CACHE is off, or APP_ENV is not in page-cache.environments (production, staging) | cache disabled |
| A preview password is set on staging | the web server would serve cached pages without the basic auth check |
| The request is not a GET, has a query string, or the response is not 2xx | only plain successful page loads are static |
The request host is not the host of APP_URL | cached HTML carries absolute asset URLs of the host it was rendered for (since 2.10) |
The response contains a rendered Livewire component (wire:snapshot) | the snapshot carries a CSRF token and component state |
| The request is authenticated 2.12 | a user on the default guard (panel login, remember-me), a session holding a login_* key of any guard, or a remember_* cookie |
The response contains the info bar (id="evolve__infoBar") 2.12 | second line of defence for themes that render the bar under their own condition |
| The response sets a cookie of its own 2.12 | the page was rendered for one visitor's state. The session cookie, XSRF-TOKEN and the attribution tracking cookie do not count, they are set on every visit and do not change the body |
The three rules marked 2.12 close a gap that existed in every earlier release: when a logged-in panel user opened a frontend page that was not cached yet, that page was written and then served to everybody, with the panel info bar, the editor preview hooks in the head, the user's CSRF token and routes in languages not yet active for guests.
Upgrading from before 2.12
Entries written by earlier releases are not detected automatically. php artisan deploy clears the page cache of every new release before it goes live, so the first deploy with 2.12 removes them. A site updated any other way needs the clearing procedure once.
A project cookie set on every page (a consent or A/B cookie set in PHP middleware on the frontend route) now keeps every page out of the cache. Set such cookies client side, or queue them only on the requests that need them, and check with the X-Evolve-Page-Cache header (see Pre-Caching pages).
Clearing cached pages
Most models in Evolve are already set up to clear the related routes when changes are made. But sometimes you still need to clear the cached content by yourself. There's a few ways to do it:
Evolve Admin UI
You can clear all cached content with the cache clear button in the footer. Navigate to the caches page to forget specific pages. Since 2.12 these actions are POST requests with a CSRF token; an old GET link to /evolve/cache/... only opens the cache overview. See Security Headers.
Artisan command
# Flush all cached content
php artisan evolve:clear-page-cache
# Clear a specific page
php artisan evolve:clear-page-cache de/some-pageProgrammatically
use Racerfish\Evolve\Services\PageCache;
// Flush all cached content
PageCache::make()->flush();
// Clear a specific page
PageCache::make()->forget('de/some-page');
// Clear many pages
PageCache::make()->forget([
'de/some-page',
'de/another-page',
'de/full-paths/*',
]);Pre-Caching pages
Artisan command
You can pre-cache all Evolve routes with the evolve:rebuild-page-cache Artisan command.
php artisan evolve:rebuild-page-cacheProgrammatically
If you wish to do the pre-caching in code, you can do it by making a GET request with a HTTP client to a specific route. Make sure to make two calls: One requesting text/html, and another requesting application/json.
// Cache HTML version
Http::accept('text/html')->get('/en/contact');
// Cache JSON/API version
Http::accept('application/json')->get('/en/contact');Evolve will return a X-Evolve-Page-Cache header with a value of either cached or none, depending on if the page was cached or not.
$response = Http::accept('text/html')->get('/en/contact');
if ($response->header('X-Evolve-Page-Cache') === 'cached') {
echo 'Page successfully cached to disk';
}Clearing after an upgrade
Use this procedure whenever cached entries may contain something they should not, for example after upgrading a site from before 2.12 without php artisan deploy:
php artisan evolve:ssh live
cd current
php84 artisan evolve:clear-page-cache # delete every entry
php84 artisan evolve:rebuild-page-cache # warm it again, anonymouslyThe rebuild renders every route over HTTP without a session, so it only produces anonymous pages. The same two actions are on the panel's cache screen.
To verify, check that no cached page contains the info bar:
grep -rl 'evolve__infoBar' public/page-cache/ | head # no outputAnd from outside, request a page twice: the second answer comes from the cache when it carries no set-cookie header for the session cookie (named after SESSION_COOKIE, by default the app name followed by _session).