================================================================================ SSR FULL PAGE CACHE (FPC) ================================================================================ OVERVIEW The SSR (Server-Side Rendered) Full Page Cache system pre-renders static pages via headless Chrome (Playwright) and caches the resulting HTML in Redis for optimal SEO and performance. Routes marked with #[Static_Page] attribute are automatically cached and served to unauthenticated users. This system is ideal for: - Public-facing pages (landing pages, about, blog posts, documentation) - Content that changes infrequently but is accessed frequently - Pages requiring optimal SEO (search engines get pre-rendered HTML) - Reducing server load for high-traffic public pages CORE CONCEPTS 1. STATIC PAGE MARKING Routes opt-in to FPC by adding the #[Static_Page] attribute to their controller method. Only routes with this attribute are eligible for caching. 2. AUTHENTICATION-AWARE CACHING FPC only serves cached content to unauthenticated users (no active session). Logged-in users always get dynamic content, ensuring personalized experiences. 3. BUILD-KEY AUTO-INVALIDATION Cache keys incorporate the application's build_key, ensuring all cached pages are automatically invalidated on deployment. No manual cache clearing needed during deployments. 4. ETAG VALIDATION Each cached page includes a 30-character ETag for HTTP cache validation. Browsers can send If-None-Match headers to receive 304 Not Modified responses, reducing bandwidth usage. 5. REDIRECT CACHING If a route redirects (300-399 status codes), the FPC system caches the first redirect response without following it, preserving redirect behavior. 6. QUERY PARAMETER STRIPPING GET parameters are stripped from URLs before cache key generation, ensuring /about and /about?utm_source=email serve the same cached content. ATTRIBUTE SYNTAX Add the #[Static_Page] attribute to any route method: #[Static_Page] #[Route('/about')] public static function about(Request $request, array $params = []) { return view('frontend.about'); } Requirements: - Route must be GET only (POST, PUT, DELETE not supported) - Route must not require authentication (served to unauthenticated users only) - Route should produce consistent output regardless of GET parameters CACHE LIFECYCLE 1. ROUTE DISCOVERY During request dispatch, the Dispatcher checks if the matched route has the #[Static_Page] attribute. 2. SESSION CHECK If the user has an active session, FPC is bypassed and the route executes normally to provide personalized content. 3. CACHE LOOKUP For unauthenticated users, the Dispatcher queries Redis for a cached version using the key format: ssr_fpc:{build_key}:{sha1(url)} 4. CACHE GENERATION (ON MISS) If no cache exists, the system automatically runs: php artisan rsx:ssr_fpc:create /route This command: - Launches headless Chrome via Playwright - Sets X-RSpade-FPC-Client header to prevent infinite loops - Navigates to the route and waits for _debug_ready event - Captures the fully rendered DOM or redirect response - Stores the result in Redis with metadata (ETag, build_key, etc.) 5. CACHE SERVING The cached HTML is served with appropriate headers: - ETag: {30-char hash} for cache validation - Cache-Control: public, max-age=300 (5 min in production) - Cache-Control: no-cache, must-revalidate (0s in development) 6. CACHE INVALIDATION Caches are automatically invalidated when: - Application is deployed (build_key changes) - Redis LRU eviction policy removes old entries - Manual reset via rsx:ssr_fpc:reset command COMMANDS php artisan rsx:ssr_fpc:create Generate static cache for a specific URL. Arguments: url The URL path to cache (e.g., /about) Behavior: - Only available in non-production environments - Requires rsx.ssr_fpc.enabled = true - Strips GET parameters from URL - Launches Playwright to render page - Stores result in Redis - Runs under GENERATE_STATIC_CACHE exclusive lock Examples: php artisan rsx:ssr_fpc:create / php artisan rsx:ssr_fpc:create /about php artisan rsx:ssr_fpc:create /blog/post-title php artisan rsx:ssr_fpc:reset Clear all SSR FPC cache entries from Redis. Behavior: - Available in all environments (local, staging, production) - Safe to run - only affects ssr_fpc:* keys - Does NOT affect application caches, sessions, or queue jobs - Reports count of cleared cache entries When to use: - After major content updates across the site - When troubleshooting caching issues - Before deployment to ensure fresh cache generation (optional) - When build_key changes (auto-invalidation should handle this) CONFIGURATION Configuration is stored in config/rsx.php under the 'ssr_fpc' key: 'ssr_fpc' => [ // Enable SSR Full Page Cache system 'enabled' => env('SSR_FPC_ENABLED', false), // Playwright generation timeout in milliseconds 'generation_timeout' => env('SSR_FPC_TIMEOUT', 30000), ], Environment Variables: SSR_FPC_ENABLED Enable/disable the FPC system (default: false) SSR_FPC_TIMEOUT Page generation timeout in ms (default: 30000) To enable FPC, add to .env: SSR_FPC_ENABLED=true REDIS CACHE STRUCTURE Cache Key Format: ssr_fpc:{build_key}:{sha1(url)} Example: ssr_fpc:abc123def456:5d41402abc4b2a76b9719d911017c592 Cache Value (JSON): { "url": "/about", "code": 200, "page_dom": "...", "redirect": null, "build_key": "abc123def456", "etag": "abc123def456789012345678901234", "generated_at": "2025-10-17 14:32:10" } For redirect responses: { "url": "/old-page", "code": 301, "page_dom": null, "redirect": "/new-page", "build_key": "abc123def456", "etag": "abc123def456789012345678901234", "generated_at": "2025-10-17 14:32:10" } Cache Eviction: Redis LRU (Least Recently Used) policy automatically removes old entries when memory limits are reached. No manual TTL management required. SECURITY CONSIDERATIONS 1. INFINITE LOOP PREVENTION The Playwright script sets X-RSpade-FPC-Client: 1 header to identify itself as a cache generation request. Session::__is_fpc_client() checks this header to prevent FPC from serving cached content to the generation process. 2. PRODUCTION COMMAND BLOCKING The rsx:ssr_fpc:create command throws a fatal error in production to prevent accidental cache generation in production environments. Cache generation should happen automatically on first request or via staging/local environments. 3. AUTHENTICATION BYPASS FPC only serves to unauthenticated users. Auth checks still run for logged-in users, ensuring secure routes remain protected even if accidentally marked with #[Static_Page]. 4. QUERY PARAMETER STRIPPING GET parameters are stripped before caching to prevent cache poisoning via malicious query strings. Routes that depend on GET parameters should NOT use #[Static_Page]. PERFORMANCE CHARACTERISTICS Cache Hit (Unauthenticated User): - Served directly from Redis before route execution - No PHP controller execution - No database queries - Minimal memory usage - Response time: ~1-5ms Cache Miss (First Request): - Playwright launches headless Chrome - Page renders fully (waits for _debug_ready + network idle) - DOM captured and stored in Redis - Response time: ~500-2000ms (one-time cost) Authenticated User: - FPC bypassed completely - Normal route execution - No performance impact DEBUGGING AND TROUBLESHOOTING Enable console_debug output for FPC: CONSOLE_DEBUG_FILTER=SSR_FPC php artisan rsx:debug /about Common issues: 1. Cache not being served - Check: rsx.ssr_fpc.enabled = true - Check: Route has #[Static_Page] attribute - Check: User is unauthenticated (no active session) - Check: Redis is running and accessible 2. Cache generation fails - Check: Playwright is installed (npm install) - Check: Timeout setting (increase SSR_FPC_TIMEOUT) - Check: Route returns valid HTML (not JSON/PDF/etc.) - Check: Route doesn't redirect to external domains 3. Stale cache after deployment - Cache should auto-invalidate (build_key changed) - Manual reset: php artisan rsx:ssr_fpc:reset - Check: build_key is being regenerated on deployment 4. 304 responses not working - Check: Browser sends If-None-Match header - Check: ETag matches exactly (case-sensitive) - Check: Response includes ETag header FUTURE ROADMAP Planned enhancements: 1. SITEMAP INTEGRATION Automatically generate static caches for all routes in sitemap.xml during deployment. Ensures all public pages are pre-cached before first user visit. 2. PARALLEL GENERATION Generate multiple static caches concurrently using a worker pool, reducing total cache generation time for large sites. 3. SHARED SECRET KEYS Support deployment across multiple servers with shared cache keys, enabling cache pre-generation on staging before production deployment. 4. EXTERNAL CACHE SERVICE Move cache generation to a separate service (e.g., AWS Lambda, dedicated worker) to reduce load on application servers during cache regeneration. 5. PROGRAMMATIC CACHE RESET Add programmatic methods to clear specific route caches after content updates: Rsx_Static_Cache::clear('/blog/post-slug') Rsx_Static_Cache::clear_pattern('/blog/*') RELATED DOCUMENTATION - php artisan rsx:man routing (Route system and attributes) - php artisan rsx:man caching (General caching concepts) - php artisan rsx:man console_debug (Debug output configuration) - php artisan rsx:man auth (Authentication system) EXAMPLES Basic static page: #[Static_Page] #[Route('/')] #[Auth('Permission::anybody()')] public static function index(Request $request, array $params = []) { return view('frontend.index', [ 'featured_products' => Product_Model::where('featured', true)->get(), ]); } Static page with redirect: #[Static_Page] #[Route('/old-page')] #[Auth('Permission::anybody()')] public static function old_page(Request $request, array $params = []) { return redirect('/new-page', 301); } NOT suitable for FPC (query-dependent): // ❌ DO NOT use #[Static_Page] - depends on GET parameters #[Route('/search')] #[Auth('Permission::anybody()')] public static function search(Request $request, array $params = []) { $query = $params['q'] ?? ''; return view('search.results', [ 'results' => Product_Model::search($query)->get(), ]); } Pre-generate cache in deployment script: #!/bin/bash # deployment.sh # Deploy application git pull composer install --no-dev --optimize-autoloader php artisan migrate # Pre-generate static caches for high-traffic pages php artisan rsx:ssr_fpc:create / php artisan rsx:ssr_fpc:create /about php artisan rsx:ssr_fpc:create /pricing php artisan rsx:ssr_fpc:create /contact # Note: This is optional - caches will auto-generate on first request ================================================================================