Server-Islands

Manchmal braucht ein Bereich einer ansonsten statischen Seite frische Daten pro Anfrage — Kommentare unter einem gecachten Blogbeitrag, ein Lagerbestand auf einer Produktseite. Server-Islands halten die Seite statisch und rendern genau diesen Bereich auf einem Server, wenn die Seite aufgerufen wird.

1. Die Island schreiben

Eine Island ist ein Fragment-Modul unter src/islands/ — eine schlichte .js- oder .ts-Datei (nicht .ht.js, denn Islands sind Fragmente, keine Seiten). Dieselbe Idee wie überall in sitelo: eine Funktion, die HTML zurückgibt.

export default async function comments({ props, request }) {
  const comments = await fetchComments(props.postId)
  return `<ul>${comments.map((c) => `<li>${c.text}</li>`).join('')}</ul>`
}

Sie erhält { name, props, request } und muss einen HTML-String zurückgeben. Island-Module laufen ausschließlich auf dem Server — nicht referenzierter Code unter src/ gelangt nie in den Browser.

2. In einer Seite platzieren

Importiere island() aus sitelo/islands. Der statische Build liefert das Fallback-HTML aus; die Props stecken im Platzhalter, halte sie also klein und frei von Geheimnissen.

import { html, body, article, script } from 'javascript-to-html'
import { island } from 'sitelo/islands'

export default ({ params }) =>
  html(
    body(
      article('…statischer Inhalt…'),
      island('comments', { postId: params.slug }, '<p>Kommentare werden geladen…</p>'),
      script({ type: 'module', src: '/islands.js' }),
    ),
  )

3. Den Client-Loader ergänzen

Ein winziges Skript holt jedes gerenderte Fragment und tauscht es ein. Es läuft durch die normale Asset-Pipeline, ein schlichter src/islands.js-Einstiegspunkt genügt also:

import { mountIslands } from 'sitelo/islands/client'

mountIslands()

In sitelo (dev) und sitelo preview funktioniert das bereits — beide liefern Islands unter /_sitelo/islands/<name> aus src/islands/ aus. Preview lädt native .js- / .mjs-Module (wie ein Node-Host); TypeScript-Islands werden in der Entwicklung über Vite unterstützt.

Produktion

Dein statischer Hoster liefert die Seiten weiterhin aus. Hänge einen kleinen Handler dort ein, wo du Server-Code ausführst — Node, Serverless oder eine Edge-Funktion — und er rendert dieselben Island-Module. Für einen vollständigen Durchgang mit lauffähigem Node-Host sowie Netlify- und Vercel-Vorlagen siehe das Server-Islands-Beispiel.

// z. B. ein Node-Server oder eine Serverless-/Edge-Funktion
import { createIslandsHandler } from 'sitelo/islands/server'

const handleIslands = createIslandsHandler({
  islands: {
    comments: () => import('./src/islands/comments.js'),
  },
})

// Web Request → Response | null (null = keine Island-Anfrage)
export default { fetch: (request) => handleIslands(request) }

Mit schlichtem Node-http oder express nimmst du stattdessen createIslandsNodeHandler(options) — dieselben Optionen, Signatur (req, res, next). Jedes .js- / .mjs-Modul unter src/islands/ lässt sich mit createIslandsFromDirectory automatisch verdrahten. Holt der Loader von einem anderen Ursprung oder Pfad, übergib mountIslands({ endpoint: 'https://api.example.com/islands' }) und stimme es mit der endpoint-Option des Handlers ab.

Stabilitäts-Checkliste

Ladestrategien

Standardmäßig lädt jede Island, sobald die Seite lädt; eine Seite mit acht Islands stellt also acht gleichzeitige Anfragen während des ersten Aufbaus. Übergib when, um die nicht sofort sichtbaren zu verzögern.

// Lädt, sobald die Seite lädt — der Standard.
island('cart', { id }, '<p>…</p>')

// Wartet auf einen Idle-Callback.
island('recommendations', { id }, '<p>…</p>', { when: 'idle' })

// Wartet, bis das Element sichtbar wird.
island('comments', { postId }, '<p>Kommentare werden geladen…</p>', {
  when: 'visible',
  rootMargin: '400px',   // beginnt 400px früher zu laden
})

rootMargin gilt nur für 'visible' und steht standardmäßig auf '200px'. Außerdem laufen Islands in einen Timeout, statt ewig zu drehen:

mountIslands({
  timeout: 5000,        // pro Island; 0 deaktiviert. Standard 10000
  rootMargin: '300px',  // Standard für Islands mit `when: 'visible'`
})

mountIslands() löst auf, sobald die sofortigen Islands fertig sind — verzögerte laden später von selbst und werden bewusst nicht abgewartet. Eine fehlgeschlagene oder abgelaufene Island behält ihr Fallback-HTML.

Props sind nicht vertrauenswürdig

Island-Props kommen vom Client. Sie stecken in der Seite, werden mit der Anfrage zurückgeschickt, und jede Person kann sie vorher bearbeiten:

GET /_sitelo/islands/profile?props={"userId":"someone-else"}

Behandle die Props, die deine Island empfängt, genau wie einen Query-Parameter — validiere sie, und nutze sie nie, um Daten nachzuschlagen, die die betrachtende Person nicht ohnehin sehen darf.

Wählen Props privilegierte Daten aus, dann signiere sie. Setze ein Secret, und sitelo signiert jeden Platzhalter zur Build-Zeit und weist alles andere mit einem 403 ab:

SITELO_ISLANDS_SECRET=$(openssl rand -hex 32) sitelo build

Dieselbe Variable lesen sitelo, sitelo preview und createIslandsHandler, sodass Entwicklung, Preview und Produktion übereinstimmen. Gib deinem Produktions-Host dasselbe Secret. Lieber im Code setzen?

import { configureIslands } from 'sitelo/islands'

configureIslands({ secret: process.env.MY_SECRET })

Signaturen sind HMAC-SHA256 über den Island-Namen und ihre Props, eine für eine Island ausgestellte Signatur lässt sich also nicht gegen eine andere wiederverwenden. Das Signieren belegt, dass die Props aus deinem Build stammen — es verbirgt sie nicht, sie müssen also weiterhin frei von Geheimnissen sein. Ohne Secret werden Props unverändert akzeptiert, und ihre Validierung liegt ganz bei deinem Island-Modul.

Gut zu wissen