🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
205 lines
8.0 KiB
Plaintext
Executable File
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)
|