WU / DOCSUNIVERSAL MICROFRONTENDS
EN

Server and SSR recipes

Render HTML, React, and Vue with WU in Node; verify escaping, state, CSP, errors, and real HTTP responses.

From renderer to HTTP response

WU’s server API receives a module with render(props) and returns a micro-app’s markup. The HTTP host remains responsible for loading data, choosing trusted modules, and constructing the response. You do not need to initialize the browser runtime to use wu-framework/server.

The project includes a complete recipe in examples/ssr/render-page.mjs. It composes three real renderers—HTML, React, and Vue—and generates a document with Declarative Shadow DOM, hydration state, and CSP headers.

You can download the runnable recipes, extract the ZIP, and run npm install. It includes code, tests, and an independent manifest to reproduce them outside the portal.

node examples/ssr/render-page.mjs Ada
node examples/ssr/server.mjs

The first command writes HTML to stdout. The second opens http://127.0.0.1:4310/?name=Ada. Run the commands from the project root with dependencies installed. The server accepts the PORT variable and shuts down with Ctrl+C.

A small HTML renderer

import { escapeHtml, renderApp } from 'wu-framework/server';

const greeting = {
  render({ name }) {
    return {
      html: `<p>Hola, ${escapeHtml(name)}.</p>`,
      styles: 'p { color: #b7d900; }',
      state: { name },
    };
  },
};

const result = await renderApp('greeting', {
  module: greeting,
  props: { name: 'Ada' },
});

The complete container is in result.html. Its shape is:

<div id="wu-app-greeting" data-wu-app="greeting" data-wu-ssr="open">
  <template shadowrootmode="open">
    <style>p { color: #b7d900; }</style>
    <div data-wu-root><p>Hola, Ada.</p></div>
  </template>
</div>

result.inner contains the container’s interior; result.containerAttrs lets Astro or another engine construct the outer element. state contains only what the renderer explicitly returns. Custom HTML renderers decide which data to publish; React and Vue helpers return props as state.

React and Vue without compiling JSX or SFCs

This recipe uses React.createElement and h so Node can run it directly. In a component project, you can import the equivalent server build output.

import * as React from 'react';
import { renderToString as reactToString } from 'react-dom/server';
import { createSSRApp, h } from 'vue';
import { renderToString as vueToString } from '@vue/server-renderer';
import { renderReact, renderVue, renderApps } from 'wu-framework/server';

const reactApp = renderReact(
  ({ name }) => React.createElement('p', null, `Hola, ${name}`),
  { react: React, renderToString: reactToString },
);
const vueApp = renderVue(
  { props: ['name'], setup: props => () => h('p', `Hola, ${props.name}`) },
  { createSSRApp, renderToString: vueToString },
);

const result = await renderApps([
  { name: 'react-greeting', module: reactApp, props: { name: 'Ada' } },
  { name: 'vue-greeting', module: vueApp, props: { name: 'Ada' } },
]);

Renderer injection retains the same React or Vue instance used by the host. Public helper imports are wu-framework/server and wu-framework/server/adapters; wu-framework/server/react and wu-framework/server/vue subpaths do not exist in the package exports map.

State and CSP per response

import { createNonce, renderApps, renderStateScript, securityHeaders } from 'wu-framework/server';

const nonce = createNonce();
const rendered = await renderApps(apps, { nonce, failFast: true });
const headers = securityHeaders({ nonce });
const body = rendered.html + renderStateScript(rendered.state, { nonce });

Use the same nonce in styles, the state script, and the CSP header for that response; generate a new one for the next response. renderStateScript publishes window.__WU_SSR_STATE__ by default. Its merge: true option preserves entries from other apps when each integration emits its own script.

The local HTTP recipe uses securityHeaders({ nonce, https: false, hsts: false }) because it listens over HTTP on loopback. For an HTTPS server, configure the corresponding transport policy. The helper does not start TLS or manage certificates.

Do not publish secrets in state: response HTML and hydration props are visible to the client.

Escaping is not document sanitization

escapeHtml converts special characters in a text value. serializeState preserves valid JSON and neutralizes <, >, U+2028, and U+2029 to prevent data from closing the state script. The test supplies </script><img ...> as a name and verifies that no executable tag appears.

HTML returned by render() is inserted as-is. If you receive external HTML, sanitize it before passing it to the renderer with a policy appropriate to the product; renderApp does not remove dangerous tags or attributes from that HTML. Renderer CSS also belongs to trusted code.

State must be JSON-serializable. Circular references and BigInt fail in serializeState; functions and undefined properties are not portable hydration state.

Partial failures and failFast

import { renderApps } from 'wu-framework/server';

const result = await renderApps([
  { name: 'healthy', module: { render: () => '<p>Disponible</p>' } },
  { name: 'optional', module: { render() { throw new Error('Sin datos'); } } },
]);

console.log(result.errors[0].app); // optional
console.log(result.byApp.optional);
// <div id="wu-app-optional" data-wu-app="optional"></div>

renderApps preserves input order despite parallel rendering. Without failFast, it returns HTML from healthy apps, empty containers for failures, and an errors list. The host decides whether to return 200 with visible degradation or change the HTTP status.

With failFast: true, the promise rejects. This does not automatically cancel other renderers that have already started; avoid work with external effects inside render.

Failure Result
Invalid name Rejection before rendering; maximum 50 characters, alphanumeric start followed by alphanumeric characters, _, or -
Missing module Error requesting the renderer
Module without render Resolution error
render throws Error with code: WU_SSR_RENDER_FAILED, appName, and cause
shadowMode other than open/closed Options error
Invalid nonce or varName Error before emitting the corresponding attribute or script
Unsupported render result Normalization error; does not necessarily carry the renderer failure code

What remains for hydration

The recipe produces real HTML without downloading app JavaScript. To make it interactive, register the same apps on the client and mount them in their SSR containers. A lifecycle with hydrate adopts the existing DOM; without it, WU uses mount. React and Vue adapters must receive the same tree and props as the server.

shadowMode: 'closed' is suitable for inert markup: normal hydration cannot access that root. declarative: false produces flat markup with data-wu-ssr="flat", useful when you do not need declarative encapsulation.

See SSR and hydration and the lifecycle to connect both sides. A WASM/WASI module can supply portable renderer logic through a host runner; alone, it does not replace the HTTP server, data resolution, or response policy. The A service connected through WU guide covers requests, contracts, and cancellation between applications in the browser.

Run the checks

node scripts/verify-server-examples.mjs

The suite checks all three renderers, nonce/CSP consistency, state, escaping, invalid options, failure codes/context, partial degradation, and flat output. Tests use exports from the installed package, not copies of its functions.

BUILD WU WITH US

Found a bug? Have an idea?

Your experience improves WU. Tell us what went wrong or help support its development.