# Renderizado en servidor y páginas prerenderizadas

> Usa SuperScheduler en apps con SSR o prerenderizado estático: entrega un shell útil, reserva el espacio, monta el motor DOM en el cliente y mantén una CSP estricta.

Source: https://superscheduler.org/es/docs/ssr-prerender/
Reviewed: 2026-10-07

En el servidor, los componentes de Pro y de Lite renderizan un `<div>` vacío; el scheduler se construye en el navegador después de que el componente se monte. Renderiza en el servidor un shell con la misma altura y contenido real, como las próximas reservas, y monta después el scheduler en el cliente, idealmente desde un módulo importado de forma diferida. Los paquetes se pueden importar en el servidor, se hidratan sin discrepancias y no necesitan que tu Content Security Policy permita scripts ni estilos en línea.

SuperScheduler dibuja su cuadrícula con un motor DOM imperativo, y ese motor necesita un navegador: mide el área visible, escucha los eventos de scroll y de puntero, y coloca los nodos a medida que te desplazas. El renderizado en servidor y el prerenderizado estático funcionan bien con él, siempre que decidas qué envía el servidor mientras el navegador construye la cuadrícula real. Esto se aplica a las dos ediciones.

## Qué renderiza el servidor
`SuperSchedulerComponent`, ya venga de `super-scheduler`, de `super-scheduler/react-render` o de `super-scheduler-lite`, renderiza en el servidor un único `<div>` vacío. El control se crea en `componentDidMount`, que nunca se ejecuta en el servidor, así que:

- el HTML prerenderizado no contiene filas, eventos ni cabeceras;
- `ref.current.control` y `controlRef` siguen vacíos hasta que el navegador monta el componente;
- importar los paquetes en el servidor es seguro: ningún módulo toca `window` ni `document` al importarse, tampoco los módulos por subpath de Pro;
- la hidratación coincide: el primer render del navegador es el mismo `<div>` vacío, y el control lo rellena después de la hidratación.

> **Behavior:**
> El contenido React que se pasa a `emptyState` o a `errorState` se renderiza mediante portales que se crean en el navegador, así que tampoco aparece en el HTML del servidor.

## Entregar un shell útil
Una caja vacía hasta que se ejecuta JavaScript es un primer pintado pobre, y una página vacía para los rastreadores y para quienes leen sin JavaScript. En su lugar, renderiza en el servidor un sustituto:

- **Reserva el espacio.** Da al contenedor, en CSS, la altura que tendrá el scheduler, para que nada de lo que hay debajo se mueva cuando aparezca la cuadrícula (sin desplazamiento del layout).
- **Muestra contenido real.** Un título, las etiquetas de la barra de herramientas y una lista breve de las reservas de hoy o de las próximas les dicen a los visitantes y a los buscadores para qué sirve la página. El shell puede usar los mismos datos que el scheduler.
- **Márcalo como en carga.** `aria-busy="true"` en el shell indica a las tecnologías de apoyo que la región todavía se está construyendo.
- **Deja fuera las partes estáticas.** Los títulos, las leyendas y los filtros que no dependen del control se pueden renderizar en el servidor de forma definitiva y se quedan cuando se monta la cuadrícula.

Las páginas de ejemplo de este sitio funcionan así: cada una se prerenderiza con una vista previa estática de la primera vista, y el scheduler interactivo la sustituye cuando el visitante inicia la demo.

→ https://superscheduler.org/es/examples/hotel-rooms/
## Montar en el cliente
Renderiza el shell en el servidor y durante la hidratación, y después cambia al scheduler. `useSyncExternalStore` con una instantánea de servidor `false` te da un indicador que vale `false` en los dos sitios y `true` justo después de la hidratación, sin discrepancias. Cargar el scheduler con `React.lazy` mantiene su código fuera del primer bundle; el shell sirve también como fallback de `Suspense` mientras se descarga el chunk.

```tsx
// src/PlanningPage.tsx
import { Suspense, lazy, useSyncExternalStore } from 'react'
import { SuperScheduler } from 'super-scheduler'

// Fetched in the browser only, after hydration: the scheduler stays out of the page's first bundle.
const Planning = lazy(() => import('./Planning'))

const subscribe = () => () => {}

/** False on the server and during hydration, true afterwards: no hydration mismatch. */
function useIsClient(): boolean {
  return useSyncExternalStore(
    subscribe,
    () => true,
    () => false,
  )
}

interface Props {
  readonly rooms: SuperScheduler.ResourceData[]
  readonly events: SuperScheduler.EventData[]
}

export function PlanningPage({ rooms, events }: Props) {
  const isClient = useIsClient()
  const shell = <PlanningShell rooms={rooms} events={events} />
  return (
    // .planning reserves the scheduler's height in CSS, so the page does not move when it mounts.
    <section className="planning" aria-label="Room planning">
      {isClient ? (
        <Suspense fallback={shell}>
          <Planning rooms={rooms} events={events} />
        </Suspense>
      ) : (
        shell
      )}
    </section>
  )
}

/** Server-rendered stand-in with real content, at the same size as the grid. */
function PlanningShell({ rooms, events }: Props) {
  const roomName = new Map(rooms.map((room) => [room.id, room.name]))
  return (
    <div className="planning__shell" aria-busy="true">
      <h2>Upcoming stays</h2>
      <ul>
        {events.slice(0, 12).map((event) => (
          <li key={String(event.id)}>
            {new SuperScheduler.Date(event.start).toString('d MMM', 'en-us')}
            {' · '}
            {event.resource === undefined ? '' : roomName.get(event.resource)}
            {' · '}
            {event.text}
          </li>
        ))}
      </ul>
    </div>
  )
}
```
```tsx
// src/Planning.tsx
import { useMemo } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'

const TIME_HEADERS: SuperScheduler.TimeHeaderData[] = [
  { groupBy: 'Month' },
  { groupBy: 'Day', format: 'd' },
]

interface Props {
  readonly rooms: SuperScheduler.ResourceData[]
  readonly events: SuperScheduler.EventData[]
}

/** Loaded with React.lazy, so it needs a default export. */
export default function Planning({ rooms, events }: Props) {
  // The control splices the array it receives: give it its own copy.
  const owned = useMemo(() => events.slice(), [events])
  return (
    <SuperSchedulerComponent
      startDate="2026-10-01"
      days={31}
      scale="Day"
      cellWidth={44}
      // Fills .planning, whose height is fixed in CSS.
      height="100%"
      timeHeaders={TIME_HEADERS}
      resources={rooms}
      events={owned}
    />
  )
}
```
`height="100%"` llena el elemento anfitrión del componente, que es un `<div>` sin estilos, así que dale tamaño desde CSS:

```css
.planning {
  height: 560px;
}
/* The scheduler's host element and the shell fill the reserved box. */
.planning > div {
  height: 100%;
}
.planning__shell {
  overflow: auto;
}
```

Deberías ver la lista de estancias en el código fuente de la página y en el primer pintado, y después la cuadrícula en la misma caja un momento después de que la página sea interactiva, sin que se mueva el contenido de debajo.

Importa `super-scheduler/styles.css` (o `super-scheduler-lite/styles.css`) una sola vez desde tu layout raíz o tu hoja de estilos global, para que forme parte del CSS que el servidor ya enlaza. Importarlo desde el módulo cargado de forma diferida también funciona cuando tu bundler divide el CSS por chunk.

## Notas por framework
### Next.js
Ninguno de los dos paquetes marca sus módulos con la directiva `'use client'`. En el App Router, renderiza el scheduler desde tu propio Client Component: un archivo que empieza por `'use client'` e importa `SuperSchedulerComponent`. Los Client Components también se renderizan en el servidor, así que el scheduler llega como un `<div>` vacío y el patrón de shell anterior se aplica sin cambios. Para no renderizar ese componente en el servidor en absoluto, cárgalo con `next/dynamic` y `{ ssr: false, loading: () => <Shell /> }` desde dentro de un Client Component; el App Router no acepta `ssr: false` en los Server Components. En el Pages Router, `next/dynamic` con `ssr: false` funciona directamente en una página. Importa la hoja de estilos en el layout raíz (App Router) o en `pages/_app` (Pages Router).

### React Router y Remix
Los módulos de ruta se renderizan en el servidor en modo SSR y durante el build cuando prerenderizas, así que usa dentro del componente de ruta el indicador de cliente y la importación diferida de arriba. Los datos del shell pueden venir del loader de la ruta, lo que mantiene idénticos los renders de servidor y de cliente. Importa la hoja de estilos desde la ruta raíz o desde tu CSS global.

### Otros frameworks
La regla es la misma en todas partes: renderiza un marcador de posición con tamaño allí donde el framework renderiza en el servidor, y monta el componente solo en el navegador, por ejemplo como una isla exclusiva del cliente.

## Hidratación
La librería en sí no produce discrepancias de hidratación. Las discrepancias suelen venir del shell:

- No calcules fechas a partir del reloj durante el render. El servidor y el navegador pueden no estar de acuerdo sobre qué día es «hoy» ni sobre la zona horaria. Pasa las fechas desde tu loader o calcúlalas después del montaje.
- Formatea las fechas del shell con un locale explícito, como hace arriba `toString('d MMM', 'en-us')`, y nunca con los valores por defecto del dispositivo.
- Con `StrictMode`, en desarrollo los componentes se montan dos veces; el componente crea un control nuevo en cada montaje y libera el anterior.

## Content Security Policy
SuperScheduler funciona con una política estricta:

- **Scripts.** Sin scripts en línea, `eval` ni `new Function`. El código se carga como módulos desde tu bundle, incluidos los chunks que Pro carga bajo demanda con `import()` dinámico (soporte de teclado, menús, burbujas, paquetes de idioma). Basta con `script-src 'self'`, o con el origen que sirve tu bundle.
- **Estilos.** La hoja de estilos es un archivo CSS normal, así que `style-src 'self'` la cubre. El motor coloca los nodos escribiendo en `element.style` a través del CSSOM, algo que `style-src` no restringe, y no inyecta elementos `<style>`. No necesitas `'unsafe-inline'` para la librería.
- **Imágenes.** La hoja de estilos de Pro dibuja algunos iconos pequeños y formas de esqueleto de carga como imágenes SVG `data:`, por ejemplo el botón para eliminar un evento. Permítelas con `img-src 'self' data:`, o esas decoraciones no aparecerán.

```txt
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://api.example.com
```

> **Limitation:**
> Las cadenas HTML que le das al scheduler (`html` de evento, `html` de celda, `bubbleHtml`, `html` de elemento de menú, `html` de área) se insertan con `innerHTML`. Con un `style-src` estricto, los atributos `style="..."` en línea que contengan se bloquean, así que usa clases. Los atributos de handlers de eventos en línea los bloquea `script-src`, como debe ser. La librería asigna cadenas HTML con `innerHTML`, tanto las tuyas como las de sus propios menús, mensajes e indicaciones de arrastre, y no crea ninguna política de Trusted Types: las páginas que imponen `require-trusted-types-for 'script'` necesitan una política por defecto.

Tu propio marcado renderizado en servidor sigue las mismas reglas: la prop `style` de React se convierte en un atributo `style` en el HTML del servidor, que un `style-src` estricto bloquea antes de la hidratación. Por eso el ejemplo da tamaño al contenedor con una clase.

## Lista de comprobación
- Shell renderizado en el servidor, con la altura final del scheduler reservada en CSS.
- Scheduler montado solo en el navegador, desde un módulo importado de forma diferida.
- Hoja de estilos importada una sola vez desde el layout raíz o el CSS global.
- Nada que dependa del reloj ni del locale del dispositivo en los renders de servidor.
- CSP con `script-src 'self'`, `style-src 'self'` e `img-src 'self' data:`; clases en lugar de estilos en línea en tus cadenas HTML.

Guías relacionadas: [Integración con React](https://superscheduler.org/es/docs/react-integration/), [Virtualización y rendimiento](https://superscheduler.org/es/docs/performance-virtualization/), [Temas](https://superscheduler.org/es/docs/theming/) y [Solución de problemas](https://superscheduler.org/es/docs/troubleshooting/).
