# JQHTML Server-Side Rendering (SSR) > **See [SPECIFICATION.md](./SPECIFICATION.md) for the complete technical specification.** ## Overview The JQHTML SSR system renders components to HTML on the server for SEO purposes. Unlike React/Vue/Angular which require separate data-fetching abstractions, JQHTML SSR runs the exact same component code - including `on_load()` with real HTTP requests. **Primary use case:** SEO - rendering meaningful HTML for search engine crawlers. ## Installation ```bash npm install @jqhtml/ssr ``` Or install globally for CLI access: ```bash npm install -g @jqhtml/ssr ``` --- ## Quick Start ### Starting the Server ```bash cd /var/www/html/jqhtml/aux/ssr npm install # Start on TCP port node src/server.js --tcp 9876 # Or with Unix socket (better performance, local only) node src/server.js --socket /tmp/jqhtml-ssr.sock ``` ### Server Options ``` --tcp Listen on TCP port --socket Listen on Unix socket --max-bundles Max cached bundle sets (default: 10) --timeout Default render timeout (default: 30000) --help Show help ``` --- ## Reference CLI Example The package includes `jqhtml-ssr-example`, a complete reference implementation that demonstrates the full SSR workflow. **Use this as the canonical source of truth for building integrations in any language.** ### Basic Usage ```bash # Start the SSR server in one terminal jqhtml-ssr --tcp 9876 # In another terminal, run the example jqhtml-ssr-example \ --vendor ./bundles/vendor.js \ --app ./bundles/app.js \ --component Dashboard_Index_Action \ --base-url http://localhost:3000 ``` ### Example Options ``` REQUIRED: --vendor, -v Path to vendor bundle (contains @jqhtml/core) --app, -a Path to app bundle (contains components) --component, -c Component name to render OPTIONS: --args Component arguments as JSON (default: {}) --base-url, -b Base URL for fetch requests (default: http://localhost:3000) --timeout, -t Render timeout in milliseconds (default: 30000) --port, -p SSR server port (default: 9876) --socket, -s Use Unix socket instead of TCP --format, -f Output format: pretty, json, html-only (default: pretty) --help, -h Show help message ``` ### Output Formats **Pretty (default)** - Human-readable output showing: - Rendered HTML (formatted) - localStorage cache entries - sessionStorage cache entries - Timing information ```bash jqhtml-ssr-example -v vendor.js -a app.js -c My_Component ``` **JSON** - Raw server response for programmatic use: ```bash jqhtml-ssr-example -v vendor.js -a app.js -c My_Component --format json > result.json ``` **HTML-only** - Just the rendered HTML: ```bash jqhtml-ssr-example -v vendor.js -a app.js -c My_Component --format html-only > output.html ``` ### Example with Component Arguments ```bash jqhtml-ssr-example \ -v ./bundles/vendor.js \ -a ./bundles/app.js \ -c User_Profile \ --args '{"user_id": 123, "show_avatar": true}' ``` ### Integration Reference The `jqhtml-ssr-example` source code (`bin/jqhtml-ssr-example.js`) is the **canonical reference** for building integrations. Key implementation details: 1. **Connection** - TCP socket to `localhost:PORT` or Unix socket 2. **Request format** - Newline-delimited JSON 3. **Bundle loading** - Read JS files, send as `content` strings 4. **Request structure** - See the commented request object in the source 5. **Response parsing** - JSON with `status`, `payload`, and `error` fields When building integrations in PHP, Python, Go, etc., refer to this example for the exact protocol and data structures. --- ## Integration ### Protocol The server uses a simple newline-delimited JSON protocol over TCP or Unix socket. **Request format:** ```json { "id": "unique-request-id", "type": "render", "payload": { "bundles": [ { "id": "vendor", "content": "..." }, { "id": "app", "content": "..." } ], "component": "Dashboard_Index_Action", "args": { "user_id": 123 }, "options": { "baseUrl": "http://localhost:3000", "timeout": 30000 } } } ``` **Response format:** ```json { "id": "unique-request-id", "status": "success", "payload": { "html": "
...
", "cache": { "localStorage": {}, "sessionStorage": {} }, "timing": { "total_ms": 231, "bundle_load_ms": 49, "render_ms": 99 } } } ``` ### PHP Integration Example ```php socket = stream_socket_client("unix://$socketPath", $errno, $errstr, 5); if (!$this->socket) { throw new Exception("SSR connection failed: $errstr"); } } public function render(string $component, array $args = [], array $bundles = []): array { $request = json_encode([ 'id' => uniqid('ssr-'), 'type' => 'render', 'payload' => [ 'bundles' => $bundles, 'component' => $component, 'args' => $args, 'options' => [ 'baseUrl' => 'http://localhost', 'timeout' => 30000 ] ] ]) . "\n"; fwrite($this->socket, $request); $response = fgets($this->socket); return json_decode($response, true); } public function ping(): bool { $request = json_encode([ 'id' => 'ping', 'type' => 'ping', 'payload' => [] ]) . "\n"; fwrite($this->socket, $request); $response = json_decode(fgets($this->socket), true); return $response['status'] === 'success'; } } // Usage $ssr = new JqhtmlSSR(); $result = $ssr->render('Dashboard_Index_Action', ['user_id' => 123], $bundles); if ($result['status'] === 'success') { echo $result['payload']['html']; } ``` ### Node.js Client Example ```javascript const net = require('net'); function sendSSRRequest(port, request) { return new Promise((resolve, reject) => { const client = net.createConnection({ port }, () => { client.write(JSON.stringify(request) + '\n'); }); let data = ''; client.on('data', (chunk) => { data += chunk.toString(); if (data.includes('\n')) { client.end(); resolve(JSON.parse(data.trim())); } }); client.on('error', reject); setTimeout(() => { client.end(); reject(new Error('Request timeout')); }, 30000); }); } // Usage const response = await sendSSRRequest(9876, { id: 'render-1', type: 'render', payload: { bundles: [ { id: 'vendor', content: vendorBundleContent }, { id: 'app', content: appBundleContent } ], component: 'Dashboard_Index_Action', args: { user_id: 123 }, options: { baseUrl: 'http://localhost:3000' } } }); console.log(response.payload.html); ``` --- ## Request Types ### ping Health check. ```json { "id": "1", "type": "ping", "payload": {} } ``` Response includes `uptime_ms`. ### render Render a component to HTML. Required payload fields: - `bundles` - Array of `{ id, content }` objects - `component` - Component name to render - `options.baseUrl` - Base URL for relative fetch/XHR requests Optional: - `args` - Component arguments (default: `{}`) - `options.timeout` - Render timeout in ms (default: 30000) ### flush_cache Clear bundle cache. ```json { "id": "1", "type": "flush_cache", "payload": {} } ``` Or flush specific bundle: ```json { "id": "1", "type": "flush_cache", "payload": { "bundle_id": "app" } } ``` --- ## Error Codes | Code | Description | |------|-------------| | `PARSE_ERROR` | Invalid JSON or malformed request | | `BUNDLE_ERROR` | JavaScript syntax error in bundle | | `COMPONENT_NOT_FOUND` | Component not registered after loading bundles | | `RENDER_ERROR` | Error during component lifecycle | | `RENDER_TIMEOUT` | Component did not reach ready state in time | | `INTERNAL_ERROR` | Unexpected server error | --- ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ SSR Server (long-running Node.js process) │ │ │ │ 1. Receive request (component name, args, bundles) │ │ 2. Load bundles (cached) into isolated jsdom environment │ │ 3. Execute component lifecycle (including real fetch()) │ │ 4. Wait for on_ready │ │ 5. Return HTML + localStorage/sessionStorage cache dump │ └─────────────────────────────────────────────────────────────────┘ ``` **Key features:** - **Real data fetching** - Components make actual HTTP requests during SSR - **Cache export** - Server exports storage state for instant client hydration - **Bundle caching** - Prepared bundle code cached with LRU eviction - **Request isolation** - Each render gets fresh jsdom environment - **URL rewriting** - Relative URLs resolved against baseUrl --- ## File Structure ``` aux/ssr/ ├── SPECIFICATION.md # Complete technical specification ├── README.md # This file ├── package.json ├── bin/ │ └── jqhtml-ssr-example.js # Reference CLI example (canonical integration source) ├── src/ │ ├── index.js # Package exports │ ├── server.js # TCP/Unix socket server (jqhtml-ssr CLI) │ ├── environment.js # jsdom + jQuery environment setup │ ├── bundle-cache.js # Bundle caching with LRU eviction │ ├── http-intercept.js # fetch/XHR URL rewriting │ ├── storage.js # Fake localStorage/sessionStorage │ └── protocol.js # Message parsing/formatting ├── test/ │ ├── test-protocol.js # Protocol unit tests (16 tests) │ ├── test-storage.js # Storage unit tests (13 tests) │ └── test-server.js # Server integration tests (6 tests) ├── test-manual.js # Manual test with real bundles └── test-debug.js # Debug script for troubleshooting ``` --- ## Running Tests ```bash # Unit tests node test/test-protocol.js node test/test-storage.js node test/test-server.js # Manual test with real bundles (requires bundles_for_ssr_test/) node test-manual.js ``` --- ## Critical Technical Discoveries ### 1. jQuery Module Load Order (CRITICAL) **jQuery MUST be `require()`'d BEFORE `global.window` is set.** ```javascript // CORRECT - jQuery returns a factory function const jqueryFactory = require('jquery'); // First! global.window = jsdomWindow; // Second const $ = jqueryFactory(jsdomWindow); // Returns working jQuery // WRONG - jQuery auto-initializes and returns an object global.window = jsdomWindow; // First (BAD) const jqueryFactory = require('jquery'); // jQuery sees window, auto-binds const $ = jqueryFactory(jsdomWindow); // Returns object, not function! ``` **Why:** jQuery checks for `global.window` at require-time. If it exists, jQuery auto-initializes against that window instead of returning a factory. ### 2. Use `vm.runInThisContext` Not `vm.createContext` Using `vm.createContext` creates VM context boundary issues where function references don't work across contexts. jQuery wrapper functions fail because closures can't access variables across the boundary. **Solution:** Use `vm.runInThisContext` which executes in Node's global context. ### 3. Bundle Loading Order Load bundles in dependency order: 1. **Vendor bundle** - Contains `@jqhtml/core`, initializes `window.jqhtml` 2. **App bundle** - Contains templates and component classes ### 4. Global Cleanup When destroying environments, set globals to `undefined` instead of using `delete`: ```javascript global.window = undefined; // Safe // delete global.window; // Can cause issues ``` --- ## Dependencies ```json { "jsdom": "^24.0.0", "jquery": "^3.7.1" } ``` --- ## References - [SPECIFICATION.md](./SPECIFICATION.md) - Complete technical specification - [/packages/core/src/component.ts](/packages/core/src/component.ts) - Component lifecycle - [/docs/official/18_boot.md](/docs/official/18_boot.md) - Client-side boot/hydration - [jsdom documentation](https://github.com/jsdom/jsdom) - [jQuery in Node.js](https://www.npmjs.com/package/jquery)