Ir al contenido

Astro y Next.js

Wu incluye componentes de primera parte para Astro y Next, para que no tengas que escribir a mano la plomería de init y mount.

Ventana de terminal
npm install @wu-framework/astro # peer: astro >= 4
npm install @wu-framework/next # peers: next >= 13, react >= 18

También son accesibles desde el paquete principal como wu-framework/integrations/astro y wu-framework/integrations/next.

WuApp WuAppSSR
Renderizado en el servidor <div> vacío ✅ marcado completo, en su shadow root
Contenido en el HTML inicial ninguno sí — visible con curl
Bueno para islas interactivas bajo el pliegue SEO, primer pintado, contenido sobre el pliegue
Necesita un módulo de servidor no
Desde 2.0 2.7

WuApp renderiza un marcador de posición y monta después de la hidratación. WuAppSSR ejecuta renderApp() durante el render de servidor, así que el HTML ya contiene el marcado de la micro-app —encapsulado en un Declarative Shadow DOM con sus estilos dentro— y luego lo hidrata en el sitio.

El framework del shell y el de la micro-app no tienen por qué coincidir. WuAppSSR es un componente React porque Next es React, pero la micro-app que renderiza puede ser React, Vue, Preact, Svelte, Solid o HTML plano: lo que sea con lo que se haya construido su módulo de servidor.


src/layouts/Base.astro
---
import WuShell from '@wu-framework/astro/WuShell.astro';
---
<html>
<body>
<WuShell apps={[
{ name: 'header', url: '/apps/header' },
{ name: 'cart', url: '/apps/cart' },
]}>
<slot />
</WuShell>
</body>
</html>
Prop Tipo Significado
apps Array<{ name, url, strategy? }> Se pasa a wu.init()
sandbox 'module' | 'strict' | 'eval' Modo de sandbox global
debug boolean Registro detallado

La configuración viaja al navegador dentro de una etiqueta <script> con JSON; el script de montaje lo empaqueta Vite. Esa indirección existe porque define:vars obligaría a usar is:inline, y un especificador wu-framework pelado no se resuelve en el navegador.

---
import WuApp from '@wu-framework/astro/WuApp.astro';
---
<WuApp name="header" />
<WuApp name="dashboard" lazy />
Prop Por defecto Significado
name Nombre de la micro-app registrada
lazy false Retrasa el montaje hasta que sea visible (IntersectionObserver, margen de 200 px)
class / style Se aplican al <div> contenedor
---
import WuAppSSR from '@wu-framework/astro/WuAppSSR.astro';
import cartServer from '../apps/cart/server.js';
---
<WuAppSSR name="cart" module={cartServer} props={{ userId: 42 }} />
Prop Tipo Por defecto Significado
name string Debe coincidir con lo que registra el cliente
module módulo o () => import(...) El módulo de servidor de la app
props Record<string, any> {} Props de render; se publican para la hidratación
styles string | string[] CSS extra encapsulado dentro del shadow root
hydrate boolean true Monta/hidrata en el cliente tras la carga
fallbackToClient boolean true Si falla el SSR, deja el hueco y monta en el cliente
class / style string Se aplican al contenedor

Funciona tanto en la salida SSR de Astro como en la estática. En un build estático el marcado se cocina en tiempo de build, que es exactamente lo que quieres para páginas de contenido.

El componente emite <script type="application/json" data-wu-ssr-state="..."> junto al contenedor, y su script de cliente lo publica en window.__WU_SSR_STATE__ antes de llamar a wu.mount(), que es donde mount() lee las props para entregárselas al slot hydrate de la app.

Pon hydrate={false} para contenido inerte: se envía el marcado del servidor y ningún JavaScript monta encima.

astro.config.mjs
import wu from '@wu-framework/astro';
export default defineConfig({
integrations: [
wu({
apps: [
{ name: 'header', url: '/apps/header', strategy: 'eager' },
{ name: 'dashboard', url: '/apps/dashboard' },
],
sandbox: 'module',
}),
],
});

Opciones: apps, sandbox, debug y cdn (inyectar el runtime desde una URL en vez de empaquetarlo). La integración también añade wu-framework a optimizeDeps.include de Vite.

Usa la integración o WuShell, no ambas: dos llamadas a init() son inofensivas (es idempotente) pero la duplicación confunde.


app/layout.jsx
import { WuShell } from '@wu-framework/next';
export default function RootLayout({ children }) {
return (
<html><body>
<WuShell apps={[{ name: 'header', url: '/apps/header' }]}>
{children}
</WuShell>
</body></html>
);
}

WuShell es un componente de cliente. Importa el runtime dinámicamente dentro de un efecto, así que nada corre durante el SSR ni en un render de RSC. wu.init() es idempotente, así que un doble montaje de StrictMode es inofensivo.

'use client';
import { WuApp } from '@wu-framework/next';
<WuApp name="header" />
<WuApp name="dashboard" lazy />
Prop Por defecto Significado
name / appName Nombre de la micro-app (appName gana si se pasan ambos)
lazy false Retrasa el montaje hasta que sea visible
className / style Se aplican al contenedor
onMount / onUnmount null Callbacks ({ name }) => void

Solo de cliente por construcción: el runtime se importa dentro de useEffect, así que el servidor renderiza un <div id> vacío y el cliente lo rellena. Sin desajustes de hidratación, y sin necesidad de dynamic(..., { ssr: false }).

Renderizado en el servidor — un Server Component

Sección titulada «Renderizado en el servidor — un Server Component»
// app/page.jsx — sin 'use client'
import { WuAppSSR } from '@wu-framework/next';
export default function Page() {
return (
<main>
<WuAppSSR
name="cart"
module={() => import('../apps/cart/server.js')}
props={{ userId: 42 }}
/>
</main>
);
}
Prop Tipo Por defecto Significado
name / appName string Nombre de la micro-app (appName gana)
module () => import(...) o módulo El módulo de servidor de la app
props object {} Props de render; se publican para la hidratación
styles string | string[] CSS extra dentro del shadow root
shadowMode 'open' | 'closed' 'open' Modo del shadow root declarativo
hydrate boolean true Monta/hidrata en el cliente; false = marcado inerte
fallbackToClient boolean true Si falla el SSR, degrada a un montaje de cliente en vez de tirar la página
className / style Se aplican al contenedor

Es un Server Component async: espera a renderApp(), coloca el HTML renderizado con dangerouslySetInnerHTML (React no debe entrar en un subárbol del que wu es dueño), y renderiza un <WuMount> hermano que hace la mitad del cliente.

WuMount se exporta por separado para configuraciones a medida:

'use client';
import { WuMount } from '@wu-framework/next';
<WuMount name="cart" containerId="wu-app-cart" props={serverProps} ssr />

No renderiza nada. Publica props en window.__WU_SSR_STATE__ y luego llama a wu.mount(), que detecta el shadow root del servidor y llama a hydrate en vez de a mount.

Este es el punto que más fácil se pasa por alto. Un shell Next puede alojar una micro-app Vue:

// apps/catalog/server.js — la mitad del servidor
import { renderVue } from 'wu-framework/server';
import App from './App.vue';
export default renderVue(App, { styles: css });
// apps/catalog/main.js — la mitad del navegador
import { wuVue } from 'wu-framework/adapters/vue';
import App from './App.vue';
wuVue.register('catalog', App);
// app/page.jsx — en el shell Next
<WuAppSSR name="catalog" module={() => import('../apps/catalog/server.js')} />

Next renderiza el HTML de la app Vue en el servidor, lo envía dentro de un shadow root, y el createSSRApp de Vue lo hidrata en el navegador. React nunca toca ese subárbol.


¿Este contenido importa para el SEO o el primer pintado?
├─ Sí → WuAppSSR
│ necesita un módulo de servidor (renderReact/renderVue/… o renderHtml)
│ envía HTML real, hidrata en el sitio, sin parpadeo de caja vacía
└─ No → WuApp
no necesita módulo de servidor
añade `lazy` para todo lo que esté bajo el pliegue

Puedes mezclar ambos en una misma página. Un hero renderizado en el servidor y tres islas perezosas es una composición perfectamente normal.

El script de WuApp en Astro consulta todos los contenedores. Su script de cliente selecciona cada div[data-wu-app] de la página, así que monta todas las islas de una pasada. Está bien, pero significa que el script corre una vez por página, no una vez por instancia de componente.

El WuAppSSR de Astro solo hidrata contenedores con data-wu-hydrate="true". Poner hydrate={false} deja el marcado inerte a propósito.

El WuApp de Next desmonta al limpiar el efecto. En una transición de ruta la app se desmonta y se vuelve a montar. Si eso es caro, marca la app con keepAlive: true en wu.init() para que el teardown se convierta en un ocultar: mira Pestañas con keep-alive.

Los fallos de SSR no son fatales por defecto. Ambos componentes WuAppSSR capturan un error de render, lo registran y dejan el contenedor vacío para que lo rellene el cliente. Pasa fallbackToClient={false} cuando un render de servidor ausente deba tirar la página de forma ruidosa.

El Declarative Shadow DOM necesita un navegador moderno: Chrome 111+, Safari 16.4+, Firefox 123+. Wu incluye declarativeShadowDomPolyfill(), y mount() también materializa por su cuenta cualquier <template shadowrootmode> que quede. Mira SSR.