-
Notifications
You must be signed in to change notification settings - Fork 118
Description
Working with Laravel and Caddy.
Here is a Caddy file that works with page-cache, with the cache directory /public/page-cache/:
{
http_port 80
https_port 443
# --- Logs ---
log {
output file /project_path/storage/logs/caddy.log
format json
level ERROR
}
}
localvb2.test:443 {
tls internal
# --- Logs ---
log {
output file /project_path/storage/logs/caddy.log
format json
level INFO
}
# --- Default root: Laravel project root ---
root * /project_path
# --- Security headers ---
header {
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
Referrer-Policy "strict-origin-when-cross-origin"
}
# --- Vite build assets ---
@vite_build {
path /build/*
}
handle @vite_build {
root * /project_path/public
file_server
}
# --- Laravel static assets ---
# Exclude page-cache directory - it's handled by the page cache handler below
@laravel_static {
path_regexp \.(html|ico|css|js|gif|jpe?g|png|svg|woff2?|ttf|pdf|txt|json)$
not path_regexp ^/page-cache/
}
# --- Page Cache: Check for cached files before Laravel ---
# When request comes in for /path/url, try to serve /public/page-cache/path/url.html
# If cache file doesn't exist, request continues to Laravel
@page_cache {
path_regexp ^/(fr|en)/(boutique|shop)/.*
not path_regexp \.(html|php)$
file {
try_files /public/page-cache{path}.html
}
}
handle @page_cache {
header X-Cache-Status "from-cache"
# Switch root ONLY for cached pages
root * /project_path/public/page-cache
rewrite * {path}.html
file_server
}
# --- Laravel (default fallback) ---
# Everything else that hasn't been handled above goes to Laravel
handle {
root * //project_path/public
php_fastcgi 127.0.0.1:9000
file_server
}
# --- Compression ---
encode gzip zstd
}The other thing to note is that the cache pages should be generated only when Vite is in Build mode.
The links to the static assets will be wrong if the page is cached in dev mode (when they are served by the Vite server).
Solution is to extend Silber's middleware BaseCacheResponse (see in the documentation), and add:
use Illuminate\Support\Facades\Vite;
if (Vite::isRunningHot()) {
// Do NOT cache this page
Log::debug('Page is not cached: Vite is running hot'.$request->getUri());
return false;
}A third question that needs to be handled (in Dev like in Prod) is the question of the CSRF tokens.
When the user loads a cached page, the CSRF that comes with it belongs to an old, stale session. So when the backend receives a POST request, it should refuse it.
The solution is to add a js script to the pages, that refreshes the CSRF token when it is refused (note: I am sure it can be optimised). The advantage is that it also avoids the cases when a user is filling the form, and all of a sudden his token expires.
resources/js/utils/csrf.js
/**
* Global CSRF Token Management
* Provides utilities for handling CSRF tokens across all pages
*/
// Get current locale from URL or default to 'en'
const getCurrentLocale = () => {
const path = window.location.pathname;
const match = path.match(/^\/(en|fr)\//);
return match ? match[1] : 'en';
};
// Get CSRF token from meta tag
export const getCsrfToken = () => {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
};
// Update CSRF token in meta tag
export const updateCsrfToken = (newToken) => {
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
metaTag.setAttribute('content', newToken);
}
};
// Refresh CSRF token from server
export const refreshCsrfToken = async () => {
try {
const locale = getCurrentLocale();
const url = `/${locale}/csrf-token`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
credentials: 'same-origin',
});
if (response.ok) {
const data = await response.json();
if (data.token) {
updateCsrfToken(data.token);
return data.token;
}
}
} catch (error) {
console.error('Error refreshing CSRF token:', error);
}
return null;
};
/**
* Global authenticated fetch wrapper
* Automatically handles CSRF token refresh on 419 errors
*
* @param {string} url - The URL to fetch
* @param {object} options - Fetch options
* @returns {Promise<Response>}
*/
export const authenticatedFetch = async (url, options = {}) => {
const makeRequest = async (token, retryCount = 0) => {
const headers = {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': token,
'Accept': 'application/json',
...options.headers,
};
const response = await fetch(url, {
...options,
headers,
credentials: 'same-origin',
});
// Check if response is HTML (CSRF token expired or page expired)
const contentType = response.headers.get('content-type');
const isHtml = contentType && contentType.includes('text/html');
// Handle 419 (CSRF token mismatch) or HTML response (Page Expired)
if (response.status === 419 || (isHtml && response.status === 200)) {
// Only retry once to avoid infinite loops
if (retryCount < 1) {
const newToken = await refreshCsrfToken();
if (newToken) {
return makeRequest(newToken, retryCount + 1);
}
}
// If refresh failed or max retries reached, reload page
window.location.reload();
return null;
}
// Check if response is OK
if (!response.ok) {
// If 419 error, try to refresh token
if (response.status === 419 && retryCount < 1) {
const newToken = await refreshCsrfToken();
if (newToken) {
return makeRequest(newToken, retryCount + 1);
}
window.location.reload();
return null;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
// Check if response is JSON (if we expected JSON)
if (!contentType || !contentType.includes('application/json')) {
// If we got HTML but expected JSON, likely CSRF issue
if (isHtml && retryCount < 1) {
const newToken = await refreshCsrfToken();
if (newToken) {
return makeRequest(newToken, retryCount + 1);
}
window.location.reload();
return null;
}
}
return response;
};
try {
return await makeRequest(getCsrfToken());
} catch (error) {
// If it's a JSON parse error, likely got HTML response - try refresh
if (error instanceof SyntaxError) {
const newToken = await refreshCsrfToken();
if (newToken) {
try {
return await makeRequest(newToken, 1);
} catch (retryError) {
console.error('Error after token refresh:', retryError);
window.location.reload();
return null;
}
}
window.location.reload();
return null;
}
throw error;
}
};
// Make authenticatedFetch available globally
window.authenticatedFetch = authenticatedFetch;Apologies if all this looks obvious to many of the readers. I am still learning and was not suspecting all these details.
Maybe it could be added to the readme ?