SSR(3) RSX Framework Manual SSR(3) NAME SSR - Server-side rendering of jqhtml components via Node.js SYNOPSIS use App\RSpade\Core\SSR\Rsx_SSR; $result = Rsx_SSR::render_component('Component_Name', $args, 'Bundle_Class'); // $result = ['html' => '...', 'timing' => [...], 'preload' => [...]] DESCRIPTION Rsx_SSR renders jqhtml components to HTML strings on the server using a long-running Node.js process. The rendered HTML is injected into a Blade template so the browser receives fully-formed markup without waiting for JavaScript execution. Primary use case: search engine crawlers and social media link previews. Crawlers that don't execute JavaScript see empty SPA shells. SSR provides real HTML content for indexing while the same components work as normal client-side jqhtml components for regular users. SSR pages are for unauthenticated users only. The SSR Node environment has no session, no cookies, and no user context. Authenticated users should be served the normal SPA/CSR version of the page. HOW IT WORKS 1. PHP controller calls Rsx_SSR::render_component() 2. PHP acquires a Redis advisory lock (LOCK_SSR_RENDER) to serialize SSR operations across concurrent requests 3. PHP ensures the SSR Node server is running (auto-starts if needed) 4. PHP reads the compiled bundle files and sends them + the component name to the Node server over a Unix socket 5. Node creates an isolated jsdom environment, executes the bundles, runs the full RSpade 10-phase boot lifecycle (with SSR guards on browser-only classes), then renders the component 6. The component's on_load() runs normally, making Ajax calls back to PHP to fetch data (e.g., Controller.get_page_data()) 7. Node returns the rendered HTML and any preload data 8. PHP injects the HTML into a Blade template and optionally seeds preload data via PageData for client-side hydration The Node server stays running across requests and is automatically restarted when the manifest build key changes (indicating code changes). CONTROLLER USAGE A typical SSR controller has three parts: the SSR route, a CSR fallback route, and a public Ajax endpoint for data. 1. SSR Route (returns pre-rendered HTML): use App\RSpade\Core\SSR\Rsx_SSR; use App\RSpade\Core\View\PageData; #[Route('/pricing', methods: ['GET'])] public static function index(Request $request, array $params = []) { $result = Rsx_SSR::render_component( 'Pricing_Page', // Component class name [], // Component args (becomes this.args) 'Marketing_Bundle' // Bundle containing the component ); // Seed preload data so client-side hydration skips on_load() if (!empty($result['preload'])) { PageData::add([ 'ssr' => true, 'ssr_preload' => $result['preload'], ]); } return rsx_view('Pricing_Index', [ 'html' => $result['html'], ]); } 2. Blade Template (injects SSR HTML into the page): The Blade template outputs {!! $html !!} inside the container where the component would normally render client-side. 3. Public Ajax Endpoint (serves data to both SSR and CSR): #[Ajax_Endpoint] public static function get_page_data(Request $request, array $params = []) { return [ 'plans' => Plan_Model::where('active', true)->get()->toArray(), 'features' => Feature_Model::all()->toArray(), ]; } The component's on_load() calls this endpoint identically whether running server-side in Node or client-side in the browser: async on_load() { const data = await Marketing_Controller.get_page_data(); this.data.plans = data.plans; this.data.features = data.features; } COMPONENT RESTRICTIONS Components rendered via SSR must follow these rules: - Data loading goes in on_load(), not on_create() on_create() should only set empty defaults (this.data.items = []). on_load() fetches data via Ajax endpoints. This is required because SSR captures the data returned by on_load() for the preload cache. - Ajax endpoints must be public (@auth-exempt on the controller) The SSR Node server has no session. Ajax calls from SSR go through PHP as unauthenticated requests. - No browser-only APIs in on_create() or on_load() window.innerWidth, navigator, localStorage (the real ones), etc. are not available. on_ready() is fine since it only runs client-side. - No user-specific content SSR pages are cached and served to all unauthenticated users. User-specific content belongs in authenticated SPA routes. SERVER LIFECYCLE The SSR Node server is managed automatically by Rsx_SSR: - Auto-start: First render_component() call starts the server - Persistence: Server stays running across PHP requests - Auto-restart: Server restarts when manifest build key changes - Concurrency: Redis advisory lock serializes SSR operations - Cleanup: Server is killed by PID when restarting Files: storage/rsx-tmp/ssr-server.sock Unix socket storage/rsx-tmp/ssr-server.pid Process ID storage/rsx-tmp/ssr-server.build_key Last known build key The server process is daemonized (detached from PHP). It survives PHP-FPM worker recycling and terminal disconnects. NGINX AND PHP-FPM DEADLOCK PREVENTION SSR components that call Ajax endpoints create a callback loop: Browser -> nginx -> PHP-FPM (page request) | v Node SSR server | v (Ajax callback) nginx -> PHP-FPM (ajax request) If both the page request and the Ajax callback compete for the same PHP-FPM worker pool, a deadlock occurs when all workers are busy handling SSR page requests, leaving no workers for Ajax callbacks. Solution: Route Ajax requests to a separate PHP-FPM pool. PHP-FPM Configuration: /etc/php/8.4/fpm/pool.d/www.conf (page requests): [www] listen = /run/php/php-fpm.sock pm = ondemand pm.max_children = 4 /etc/php/8.4/fpm/pool.d/ajax.conf (Ajax requests): [ajax] listen = /run/php/php-fpm-ajax.sock pm = ondemand pm.max_children = 4 Nginx Configuration (add to each server block): # Ajax requests use separate FPM pool (prevents SSR deadlock) location /_ajax/ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/index.php; fastcgi_param SCRIPT_NAME /index.php; fastcgi_pass unix:/run/php/php-fpm-ajax.sock; } For HTTPS server blocks, also add: fastcgi_param HTTPS 'on'; This ensures SSR Ajax callbacks always have available workers, regardless of how many page requests are in-flight. PERFORMANCE First request after server start: ~800-1000ms (server startup + render) Subsequent requests (server warm): ~150-300ms (render only) Bundle code is cached in-memory by the Node server across requests. For high-traffic SSR pages, combine with the Full Page Cache system (see rsx:man ssr_fpc) to serve pre-rendered HTML from Redis without invoking the SSR server at all. SEE ALSO rsx:man ssr_fpc Full Page Cache for static SSR pages rsx:man pagedata Server-to-client data passing rsx:man jqhtml Component system rsx:man ajax_error_handling Ajax endpoint patterns AUTHOR RSpade Framework RSpade March 2026 SSR(3)