Files
rspade_system/app/RSpade/man/ssr.txt
root daa9bb2fb1 Framework updates
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-12 19:09:07 +00:00

205 lines
8.0 KiB
Plaintext
Executable File

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)