Recetas de servidor y SSR
Renderiza HTML, React y Vue con WU en Node; comprueba escape, estado, CSP, errores y respuestas HTTP reales.
De un renderer a una respuesta HTTP
El servidor de WU recibe un módulo con render(props) y devuelve el marcado de una micro-app. El host HTTP conserva la responsabilidad de cargar datos, elegir módulos confiables y construir la respuesta. No necesitas inicializar el runtime del navegador para usar wu-framework/server.
El proyecto incluye una receta completa en examples/ssr/render-page.mjs. Compone tres renderers reales —HTML, React y Vue— y genera un documento con Declarative Shadow DOM, estado de hidratación y cabeceras CSP.
Puedes descargar las recetas ejecutables, extraer el ZIP y ejecutar npm install. Incluye el código, las pruebas y un manifest independiente para reproducirlas fuera del portal.
node examples/ssr/render-page.mjs Ada
node examples/ssr/server.mjs
La primera orden escribe HTML en stdout. La segunda abre http://127.0.0.1:4310/?name=Ada. Los comandos se ejecutan desde la raíz del proyecto con sus dependencias instaladas. El servidor acepta la variable PORT y se cierra con Ctrl+C.
Un renderer HTML pequeño
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' },
});
El contenedor completo está en result.html. Su forma es:
<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 contiene el interior del contenedor; result.containerAttrs permite que Astro u otro motor construyan el elemento exterior. state sólo contiene lo devuelto explícitamente por el renderer. Los renderers HTML personalizados deciden qué datos publicar; los helpers React y Vue devuelven las props como estado.
React y Vue sin compilar JSX ni SFC
Esta receta usa React.createElement y h para que Node pueda ejecutarla directamente. En un proyecto de componentes puedes importar el resultado del build de servidor equivalente.
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' } },
]);
La inyección del renderer mantiene la misma instancia de React o Vue que utiliza el host. Los imports públicos de los helpers son wu-framework/server y wu-framework/server/adapters; no existen subpaths wu-framework/server/react o wu-framework/server/vue en el mapa de exports del paquete.
Estado y CSP por respuesta
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 });
Usa el mismo nonce en estilos, script de estado y cabecera CSP de esa respuesta; genera uno nuevo para la siguiente. renderStateScript publica por defecto window.__WU_SSR_STATE__. Su opción merge: true conserva entradas de otras apps cuando cada integración emite su propio script.
La receta HTTP local usa securityHeaders({ nonce, https: false, hsts: false }) porque escucha por HTTP en loopback. Para un servidor HTTPS configura la política de transporte correspondiente. El helper no inicia TLS ni administra certificados.
No publiques secretos en el estado: el HTML de la respuesta y las props de hidratación son visibles al cliente.
Escapar no es sanitizar un documento
escapeHtml convierte los caracteres especiales de un valor de texto. serializeState conserva JSON válido y neutraliza <, >, U+2028 y U+2029 para impedir que un dato cierre el script de estado. La prueba introduce </script><img ...> como nombre y comprueba que no aparece una etiqueta ejecutable.
El HTML devuelto por render() se inserta tal cual. Si recibes HTML externo, sanitízalo antes de entregarlo al renderer con una política adecuada al producto; renderApp no elimina etiquetas ni atributos peligrosos de ese HTML. El CSS del renderer también pertenece a código confiable.
El estado debe ser serializable como JSON. Referencias circulares y BigInt fallan en serializeState; funciones y propiedades undefined no constituyen estado de hidratación portable.
Fallos parciales y 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 conserva el orden de las entradas aunque renderice en paralelo. Sin failFast, devuelve HTML de las apps sanas, contenedores vacíos para los fallos y una lista errors. El host decide si responde 200 con una degradación visible o cambia el estado HTTP.
Con failFast: true, la promesa rechaza. Eso no cancela automáticamente los otros renderers que ya empezaron; evita trabajo con efectos externos dentro de render.
| Fallo | Resultado |
|---|---|
| Nombre inválido | Rechazo antes de renderizar; máximo 50 caracteres, inicio alfanumérico y resto alfanumérico, _ o - |
Falta module |
Error que solicita el renderer |
Módulo sin render |
Error de resolución |
render lanza |
Error con code: WU_SSR_RENDER_FAILED, appName y cause |
shadowMode distinto de open/closed |
Error de opciones |
Nonce o varName inválidos |
Error antes de emitir el atributo o script correspondiente |
Resultado no admitido de render |
Error de normalización; no tiene necesariamente el código de fallo del renderer |
Qué falta para hidratar
La receta produce HTML real sin descargar JavaScript de las apps. Para hacerlo interactivo, registra las mismas apps en el cliente y móntalas en sus contenedores SSR. Un lifecycle con hydrate adopta el DOM existente; sin él, WU usa mount. Los adapters de React y Vue deben recibir el mismo árbol y las mismas props del servidor.
shadowMode: 'closed' sirve para marcado inerte: la hidratación normal no puede acceder a esa raíz. declarative: false produce marcado plano con data-wu-ssr="flat", útil cuando no necesitas encapsulación declarativa.
Consulta SSR e hidratación y el lifecycle para conectar ambos lados. Un módulo WASM/WASI puede aportar lógica portable al renderer a través de un runner del host; por sí solo no sustituye el servidor HTTP, la resolución de datos ni la política de respuesta. La guía Un servicio conectado con WU recorre solicitudes, contratos y cancelación entre aplicaciones desde el navegador.
Ejecutar las comprobaciones
node scripts/verify-server-examples.mjs
La suite comprueba los tres renderers, correspondencia entre nonce y CSP, estado, escape, opciones inválidas, código/contexto de fallos, degradación parcial y salida plana. Las pruebas usan los exports del paquete instalado, no copias de sus funciones.