# Rendu serveur et pages prérendues

> Utilisez SuperScheduler en SSR ou en prérendu : livrez une coquille utile, réservez l’espace, montez le moteur DOM côté client et gardez une CSP stricte.

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

Côté serveur, les composants Pro et Lite rendent tous deux un `<div>` vide ; le planificateur est construit dans le navigateur une fois le composant monté. Rendez côté serveur une coquille de même hauteur, avec du vrai contenu comme les prochaines réservations, puis montez le planificateur côté client, idéalement depuis un module importé à la demande. Les paquets peuvent être importés côté serveur, s’hydratent sans incohérence et n’exigent de votre Content Security Policy ni scripts inline ni styles inline.

SuperScheduler dessine sa grille avec un moteur DOM impératif, et ce moteur a besoin d’un navigateur : il mesure la zone visible, écoute les événements de défilement et de pointeur, et positionne les nœuds au fil du défilement. Le rendu serveur et le prérendu statique fonctionnent pourtant bien avec lui, à condition de décider ce que le serveur envoie pendant que le navigateur construit la vraie grille. Cela vaut pour les deux éditions.

## Ce que rend le serveur
`SuperSchedulerComponent`, issu de `super-scheduler`, `super-scheduler/react-render` ou `super-scheduler-lite`, rend un unique `<div>` vide côté serveur. Le contrôle est créé dans `componentDidMount`, qui ne s’exécute jamais côté serveur ; par conséquent :

- le HTML prérendu ne contient ni lignes, ni événements, ni en-têtes ;
- `ref.current.control` et `controlRef` restent vides jusqu’à ce que le navigateur monte le composant ;
- importer les paquets côté serveur ne pose aucun problème : aucun module ne touche `window` ou `document` au moment de l’import, modules Pro par sous-chemin compris ;
- l’hydratation concorde : le premier rendu du navigateur est le même `<div>` vide, et le contrôle le remplit après l’hydratation.

> **Behavior:**
> Le contenu React passé à `emptyState` ou `errorState` est rendu via des portails créés dans le navigateur : il n’apparaît donc pas non plus dans le HTML du serveur.

## Livrer une coquille utile
Une boîte vide jusqu’à l’exécution de JavaScript, c’est un premier affichage médiocre et une page vide pour les robots d’indexation et pour les lecteurs sans JavaScript. Rendez plutôt une page de substitution côté serveur :

- **Réservez l’espace.** Donnez au conteneur, en CSS, la hauteur qu’aura le planificateur, pour que rien ne bouge en dessous quand la grille apparaît (pas de décalage de mise en page).
- **Montrez du vrai contenu.** Un titre, les libellés de la barre d’outils et une courte liste des réservations du jour ou à venir indiquent aux visiteurs et aux moteurs de recherche à quoi sert la page. La coquille peut utiliser les mêmes données que le planificateur.
- **Signalez le chargement.** `aria-busy="true"` sur la coquille indique aux technologies d’assistance que la région est encore en construction.
- **Gardez les parties statiques à l’extérieur.** Titres, légendes et filtres qui ne dépendent pas du contrôle peuvent être rendus côté serveur une fois pour toutes et rester en place quand la grille se monte.

Les pages d’exemples de ce site fonctionnent ainsi : chacune est prérendue avec un aperçu statique de la première vue, et le planificateur interactif le remplace quand le visiteur lance la démo.

→ https://superscheduler.org/fr/examples/hotel-rooms/
## Monter côté client
Rendez la coquille côté serveur et pendant l’hydratation, puis passez au planificateur. `useSyncExternalStore` avec un instantané serveur à `false` donne un indicateur qui vaut `false` aux deux endroits et `true` juste après l’hydratation, sans incohérence. Charger le planificateur avec `React.lazy` garde son code hors du premier bundle ; la coquille sert aussi de fallback `Suspense` pendant le téléchargement du 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%"` remplit l’élément hôte du composant, qui est un `<div>` sans style : dimensionnez-le donc en 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;
}
```

Vous devriez voir la liste des séjours dans le code source de la page et au premier affichage, puis la grille dans la même boîte un instant après que la page devient interactive, sans décalage du contenu situé en dessous.

Importez `super-scheduler/styles.css` (ou `super-scheduler-lite/styles.css`) une seule fois depuis votre layout racine ou votre feuille de style globale, pour qu’il fasse partie du CSS que le serveur référence déjà. L’importer depuis le module chargé à la demande fonctionne aussi quand votre bundler découpe le CSS par chunk.

## Notes par framework
### Next.js
Aucun des deux paquets ne marque ses modules avec la directive `'use client'`. Dans l’App Router, rendez le planificateur depuis votre propre Client Component : un fichier qui commence par `'use client'` et importe `SuperSchedulerComponent`. Les Client Components sont tout de même rendus côté serveur : le planificateur arrive donc sous forme de `<div>` vide et le modèle de coquille ci-dessus s’applique tel quel. Pour ne pas rendre du tout ce composant côté serveur, chargez-le avec `next/dynamic` et `{ ssr: false, loading: () => <Shell /> }` depuis un Client Component ; l’App Router n’accepte pas `ssr: false` dans les Server Components. Dans le Pages Router, `next/dynamic` avec `ssr: false` fonctionne directement dans une page. Importez la feuille de style dans le layout racine (App Router) ou dans `pages/_app` (Pages Router).

### React Router et Remix
Les modules de route sont rendus côté serveur en mode SSR, et au moment du build quand vous prérendez : utilisez donc l’indicateur client et l’import à la demande présentés plus haut dans le composant de route. Les données de la coquille peuvent venir du loader de la route, ce qui garde les rendus serveur et client identiques. Importez la feuille de style depuis la route racine ou depuis votre CSS global.

### Autres frameworks
La règle est partout la même : rendez un espace réservé dimensionné partout où le framework fait un rendu côté serveur, et ne montez le composant que dans le navigateur, par exemple comme îlot exécuté uniquement côté client.

## Hydratation
La bibliothèque elle-même ne produit aucune incohérence d’hydratation. Les incohérences viennent en général de la coquille :

- Ne calculez pas de dates à partir de l’horloge pendant le rendu. Le serveur et le navigateur peuvent ne pas être d’accord sur « aujourd’hui », ni sur le fuseau horaire. Passez les dates depuis votre loader, ou calculez-les après le montage.
- Formatez les dates de la coquille avec une locale explicite, comme le fait `toString('d MMM', 'en-us')` ci-dessus, et jamais avec les réglages par défaut de l’appareil.
- Sous `StrictMode`, le développement monte les composants deux fois ; le composant crée un contrôle neuf à chaque montage et libère le précédent.

## Content Security Policy
SuperScheduler fonctionne sous une politique stricte :

- **Scripts.** Aucun script inline, ni `eval`, ni `new Function`. Le code se charge sous forme de modules depuis votre bundle, y compris les chunks que Pro charge à la demande avec un `import()` dynamique (prise en charge du clavier, menus, bulles, packs de langue). `script-src 'self'`, ou l’origine qui sert votre bundle, suffit.
- **Styles.** La feuille de style est un fichier CSS ordinaire : `style-src 'self'` la couvre. Le moteur positionne les nœuds en écrivant dans `element.style` via le CSSOM, ce que `style-src` ne restreint pas, et il n’injecte aucun élément `<style>`. Vous n’avez pas besoin de `'unsafe-inline'` pour la bibliothèque.
- **Images.** La feuille de style Pro dessine quelques petites icônes et formes de squelette sous forme d’images SVG `data:`, comme le bouton de suppression d’un événement. Autorisez-les avec `img-src 'self' data:`, sinon ces décorations n’apparaîtront pas.

```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:**
> Les chaînes HTML que vous donnez au planificateur (`html` d’un événement, `html` d’une cellule, `bubbleHtml`, `html` d’un élément de menu, `html` d’une zone) sont insérées avec `innerHTML`. Sous un `style-src` strict, les attributs `style="..."` inline qu’elles contiennent sont bloqués : utilisez des classes. Les attributs de gestionnaires d’événements inline sont bloqués par `script-src`, comme il se doit. La bibliothèque affecte les chaînes HTML avec `innerHTML`, les vôtres comme celles de ses propres menus, messages et retours de glissement, et ne crée aucune politique Trusted Types : les pages qui imposent `require-trusted-types-for 'script'` ont besoin d’une politique par défaut.

Votre propre balisage rendu côté serveur suit les mêmes règles : la prop `style` de React devient un attribut `style` dans le HTML du serveur, qu’un `style-src` strict bloque avant l’hydratation. C’est pourquoi l’exemple dimensionne le conteneur avec une classe.

## Liste de vérification
- Coquille rendue côté serveur, avec la hauteur finale du planificateur réservée en CSS.
- Planificateur monté uniquement dans le navigateur, depuis un module importé à la demande.
- Feuille de style importée une seule fois depuis le layout racine ou le CSS global.
- Aucune sortie dépendant de l’horloge ou de la locale de l’appareil dans les rendus serveur.
- CSP avec `script-src 'self'`, `style-src 'self'` et `img-src 'self' data:` ; des classes plutôt que des styles inline dans vos chaînes HTML.

Guides associés : [Intégration React](https://superscheduler.org/fr/docs/react-integration/), [Virtualisation et performances](https://superscheduler.org/fr/docs/performance-virtualization/), [Thèmes](https://superscheduler.org/fr/docs/theming/) et [Dépannage](https://superscheduler.org/fr/docs/troubleshooting/).
