# Server-Rendering und vorgerenderte Seiten

> SuperScheduler mit SSR oder statischem Vorrendern nutzen: sinnvolle Hülle ausliefern, Platz reservieren, die DOM-Engine im Client mounten, CSP streng halten.

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

Auf dem Server rendern sowohl die Pro- als auch die Lite-Komponente ein leeres `<div>`; der Planer wird im Browser aufgebaut, nachdem die Komponente gemountet ist. Rendern Sie auf dem Server eine Hülle mit derselben Höhe und echtem Inhalt, etwa den nächsten Buchungen, und mounten Sie den Planer dann im Client, idealerweise aus einem per Lazy Import geladenen Modul. Die Pakete lassen sich auf dem Server importieren, hydrieren ohne Abweichungen und verlangen von Ihrer Content Security Policy weder Inline-Skripte noch Inline-Styles.

SuperScheduler zeichnet sein Raster mit einer imperativen DOM-Engine, und diese Engine braucht einen Browser: Sie misst den sichtbaren Bereich, lauscht auf Scroll- und Zeigerereignisse und positioniert Knoten, während Sie scrollen. Server-Rendering und statisches Vorrendern funktionieren trotzdem gut damit, sofern Sie entscheiden, was der Server sendet, während der Browser das echte Raster aufbaut. Das gilt für beide Editionen.

## Was der Server rendert
`SuperSchedulerComponent`, aus `super-scheduler`, `super-scheduler/react-render` oder `super-scheduler-lite`, rendert auf dem Server ein einzelnes leeres `<div>`. Das Control wird in `componentDidMount` erzeugt, das auf dem Server nie läuft. Daher gilt:

- Das vorgerenderte HTML enthält keine Zeilen, Ereignisse oder Köpfe;
- `ref.current.control` und `controlRef` bleiben leer, bis der Browser die Komponente mountet;
- die Pakete auf dem Server zu importieren ist sicher: Kein Modul greift beim Import auf `window` oder `document` zu, auch nicht die Subpath-Module von Pro;
- die Hydration stimmt überein: Das erste Rendern im Browser ist dasselbe leere `<div>`, und das Control füllt es nach der Hydration.

> **Behavior:**
> React-Inhalte, die Sie an `emptyState` oder `errorState` übergeben, werden über Portale gerendert, die im Browser entstehen; auch sie erscheinen daher nicht im Server-HTML.

## Eine sinnvolle Hülle ausliefern
Ein leerer Kasten, bis JavaScript läuft, ist ein schwacher erster Paint und für Crawler sowie für Leser ohne JavaScript eine leere Seite. Rendern Sie stattdessen auf dem Server einen Platzhalter:

- **Platz reservieren.** Geben Sie dem Container per CSS die Höhe, die der Planer haben wird, damit sich nichts darunter verschiebt, wenn das Raster erscheint (kein Layout Shift).
- **Echten Inhalt zeigen.** Eine Überschrift, die Beschriftungen der Werkzeugleiste und eine kurze Liste der heutigen oder kommenden Buchungen sagen Besuchern und Suchmaschinen, wozu die Seite dient. Die Hülle kann dieselben Daten wie der Planer verwenden.
- **Als ladend kennzeichnen.** `aria-busy="true"` an der Hülle teilt assistiven Technologien mit, dass der Bereich noch aufgebaut wird.
- **Statische Teile draußen lassen.** Titel, Legenden und Filter, die nicht vom Control abhängen, können dauerhaft auf dem Server gerendert werden und bleiben stehen, wenn das Raster gemountet wird.

Die Beispielseiten dieser Website funktionieren so: Jede wird mit einer statischen Vorschau der ersten Ansicht vorgerendert, und der Live-Planer ersetzt sie, wenn der Besucher die Demo startet.

→ https://superscheduler.org/de/examples/hotel-rooms/
## Im Client mounten
Rendern Sie die Hülle auf dem Server und während der Hydration, und wechseln Sie dann zum Planer. `useSyncExternalStore` mit einem Server-Snapshot von `false` liefert ein Flag, das an beiden Stellen `false` ist und direkt nach der Hydration `true`, ohne Abweichung. Wenn Sie den Planer mit `React.lazy` laden, bleibt sein Code aus dem ersten Bundle heraus; die Hülle dient zugleich als `Suspense`-Fallback, während der Chunk heruntergeladen wird.

```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%"` füllt das Host-Element der Komponente, ein ungestyltes `<div>`; geben Sie ihm die Größe daher per 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;
}
```

Sie sollten die Liste der Aufenthalte im Seitenquelltext und beim ersten Paint sehen und dann, einen Moment nachdem die Seite interaktiv geworden ist, das Raster im selben Kasten, ohne dass sich der Inhalt darunter verschiebt.

Importieren Sie `super-scheduler/styles.css` (oder `super-scheduler-lite/styles.css`) einmal aus Ihrem Root-Layout oder Ihrem globalen Stylesheet, damit es Teil des CSS ist, das der Server bereits verlinkt. Der Import aus dem per Lazy Import geladenen Modul funktioniert ebenfalls, wenn Ihr Bundler CSS pro Chunk aufteilt.

## Hinweise zu Frameworks
### Next.js
Keines der beiden Pakete kennzeichnet seine Module mit der Direktive `'use client'`. Rendern Sie den Planer im App Router aus Ihrer eigenen Client Component: einer Datei, die mit `'use client'` beginnt und `SuperSchedulerComponent` importiert. Client Components werden trotzdem auf dem Server gerendert; der Planer kommt also als leeres `<div>` an, und das Hüllen-Muster oben gilt unverändert. Um das Server-Rendering dieser Komponente ganz zu überspringen, laden Sie sie mit `next/dynamic` und `{ ssr: false, loading: () => <Shell /> }` innerhalb einer Client Component; der App Router akzeptiert `ssr: false` nicht in Server Components. Im Pages Router funktioniert `next/dynamic` mit `ssr: false` direkt in einer Seite. Importieren Sie das Stylesheet im Root-Layout (App Router) oder in `pages/_app` (Pages Router).

### React Router und Remix
Routenmodule rendern im SSR-Modus auf dem Server und beim Vorrendern zur Build-Zeit; verwenden Sie daher das oben gezeigte Client-Flag und den Lazy Import innerhalb der Routenkomponente. Die Daten für die Hülle können aus dem Loader der Route kommen, wodurch Server- und Client-Rendering identisch bleiben. Importieren Sie das Stylesheet aus der Root-Route oder Ihrem globalen CSS.

### Andere Frameworks
Die Regel ist überall dieselbe: Rendern Sie einen Platzhalter mit fester Größe überall dort, wo das Framework auf dem Server rendert, und mounten Sie die Komponente nur im Browser, zum Beispiel als reine Client-Insel.

## Hydration
Die Bibliothek selbst erzeugt keine Hydration-Abweichungen. Abweichungen kommen meist aus der Hülle:

- Berechnen Sie während des Renderns keine Datumswerte aus der Uhr. Server und Browser können sich über „heute“ uneinig sein, und auch über die Zeitzone. Übergeben Sie die Datumswerte aus Ihrem Loader oder berechnen Sie sie nach dem Mount.
- Formatieren Sie Datumsangaben in der Hülle mit einer expliziten Locale, wie es `toString('d MMM', 'en-us')` oben tut, und nie mit den Voreinstellungen des Geräts.
- Unter `StrictMode` mountet die Entwicklungsumgebung Komponenten zweimal; die Komponente erzeugt bei jedem Mount ein frisches Control und gibt das vorherige frei.

## Content Security Policy
SuperScheduler funktioniert unter einer strengen Policy:

- **Skripte.** Keine Inline-Skripte, kein `eval` und kein `new Function`. Der Code wird als Module aus Ihrem Bundle geladen, einschließlich der Chunks, die Pro bei Bedarf mit dynamischem `import()` nachlädt (Tastaturunterstützung, Menüs, Bubbles, Sprachpakete). `script-src 'self'` oder der Origin, der Ihr Bundle ausliefert, genügt.
- **Styles.** Das Stylesheet ist eine normale CSS-Datei, also deckt `style-src 'self'` es ab. Die Engine positioniert Knoten, indem sie über das CSSOM in `element.style` schreibt, was `style-src` nicht einschränkt, und sie fügt keine `<style>`-Elemente ein. Sie brauchen `'unsafe-inline'` für die Bibliothek nicht.
- **Bilder.** Das Pro-Stylesheet zeichnet einige kleine Icons und Skeleton-Formen als `data:`-SVG-Bilder, etwa die Löschschaltfläche von Ereignissen. Erlauben Sie sie mit `img-src 'self' data:`, sonst erscheinen diese Verzierungen nicht.

```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:**
> HTML-Strings, die Sie dem Planer übergeben (`html` von Ereignissen, `html` von Zellen, `bubbleHtml`, `html` von Menüeinträgen, `html` von Bereichen), werden mit `innerHTML` eingefügt. Unter einem strengen `style-src` werden darin enthaltene Inline-Attribute `style="..."` blockiert; verwenden Sie daher Klassen. Inline-Attribute für Event-Handler werden von `script-src` blockiert, wie es sein soll. Die Bibliothek weist HTML-Strings mit `innerHTML` zu, Ihre ebenso wie die ihrer eigenen Menüs, Meldungen und Zieh-Rückmeldungen, und erstellt keine Trusted-Types-Policy: Seiten, die `require-trusted-types-for 'script'` erzwingen, brauchen eine Default-Policy.

Ihr eigenes, auf dem Server gerendertes Markup folgt denselben Regeln: Die `style`-Prop von React wird im Server-HTML zu einem `style`-Attribut, das ein strenges `style-src` vor der Hydration blockiert. Deshalb gibt das Beispiel dem Container seine Größe über eine Klasse.

## Checkliste
- Hülle auf dem Server gerendert, mit der endgültigen Höhe des Planers per CSS reserviert.
- Planer nur im Browser gemountet, aus einem per Lazy Import geladenen Modul.
- Stylesheet einmal aus dem Root-Layout oder dem globalen CSS importiert.
- Keine Ausgaben in Server-Renderings, die von der Uhr oder der Locale des Geräts abhängen.
- CSP mit `script-src 'self'`, `style-src 'self'` und `img-src 'self' data:`; Klassen statt Inline-Styles in Ihren HTML-Strings.

Verwandte Anleitungen: [React-Integration](https://superscheduler.org/de/docs/react-integration/), [Virtualisierung und Performance](https://superscheduler.org/de/docs/performance-virtualization/), [Theming](https://superscheduler.org/de/docs/theming/) und [Fehlerbehebung](https://superscheduler.org/de/docs/troubleshooting/).
