Files
2025-12-23 09:18:10 +00:00

13 KiB

JQHTML Server-Side Rendering (SSR)

See 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

npm install @jqhtml/ssr

Or install globally for CLI access:

npm install -g @jqhtml/ssr

Quick Start

Starting the Server

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 <port>        Listen on TCP port
--socket <path>     Listen on Unix socket
--max-bundles <n>   Max cached bundle sets (default: 10)
--timeout <ms>      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

# 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>      Path to vendor bundle (contains @jqhtml/core)
  --app, -a <path>         Path to app bundle (contains components)
  --component, -c <name>   Component name to render

OPTIONS:
  --args <json>            Component arguments as JSON (default: {})
  --base-url, -b <url>     Base URL for fetch requests (default: http://localhost:3000)
  --timeout, -t <ms>       Render timeout in milliseconds (default: 30000)
  --port, -p <port>        SSR server port (default: 9876)
  --socket, -s <path>      Use Unix socket instead of TCP
  --format, -f <format>    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
jqhtml-ssr-example -v vendor.js -a app.js -c My_Component

JSON - Raw server response for programmatic use:

jqhtml-ssr-example -v vendor.js -a app.js -c My_Component --format json > result.json

HTML-only - Just the rendered HTML:

jqhtml-ssr-example -v vendor.js -a app.js -c My_Component --format html-only > output.html

Example with Component Arguments

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:

{
  "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:

{
  "id": "unique-request-id",
  "status": "success",
  "payload": {
    "html": "<div class=\"Dashboard_Index_Action Component\">...</div>",
    "cache": {
      "localStorage": {},
      "sessionStorage": {}
    },
    "timing": {
      "total_ms": 231,
      "bundle_load_ms": 49,
      "render_ms": 99
    }
  }
}

PHP Integration Example

<?php
class JqhtmlSSR {
    private $socket;

    public function __construct(string $socketPath = '/tmp/jqhtml-ssr.sock') {
        $this->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

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.

{ "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.

{ "id": "1", "type": "flush_cache", "payload": {} }

Or flush specific bundle:

{ "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

# 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.

// 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:

global.window = undefined;  // Safe
// delete global.window;    // Can cause issues

Dependencies

{
  "jsdom": "^24.0.0",
  "jquery": "^3.7.1"
}

References