Wyspy serwerowe

Czasem jeden obszar skądinąd statycznej strony potrzebuje świeżych danych przy każdym żądaniu — komentarze pod zbuforowanym wpisem, informacja o dostępności na stronie produktu. Wyspy serwerowe utrzymują stronę statyczną i renderują na serwerze tylko ten obszar, w chwili gdy ktoś stronę ogląda.

1. Napisz wyspę

Wyspa to moduł-fragment pod src/islands/ — zwykły plik .js albo .ts (nie .ht.js, bo wyspy są fragmentami, a nie stronami). Ta sama myśl co wszędzie indziej w sitelo: funkcja zwracająca HTML.

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

Dostaje { name, props, request } i musi zwrócić ciąg HTML. Moduły wysp żyją wyłącznie na serwerze — kod bez odwołań pod src/ nigdy nie trafia do przeglądarki.

2. Umieść ją na stronie

Zaimportuj island() z sitelo/islands. Statyczny build publikuje HTML zastępczy; propsy są osadzone w miejscu na wyspę, więc trzymaj je małe i niepoufne.

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

export default ({ params }) =>
  html(
    body(
      article('…statyczna treść…'),
      island('comments', { postId: params.slug }, '<p>Wczytywanie komentarzy…</p>'),
      script({ type: 'module', src: '/islands.js' }),
    ),
  )

3. Dodaj loader kliencki

Maleńki skrypt pobiera każdy wyrenderowany fragment i podmienia go na miejscu. Przechodzi zwykłym potokiem zasobów, więc wystarczy prosty punkt wejścia src/islands.js:

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

mountIslands()

W sitelo (dev) i w sitelo preview to już działa — oba serwują wyspy pod /_sitelo/islands/<nazwa> z katalogu src/islands/. Podgląd ładuje natywne moduły .js / .mjs (tak jak host Node); wyspy w TypeScripcie działają w dev dzięki Vite.

Produkcja

Twój hosting statyczny dalej serwuje strony. Zamontuj mały handler tam, gdzie uruchamiasz kod serwerowy — Node, serverless albo funkcja edge — a wyrenderuje te same moduły wysp. Pełny przewodnik z uruchamialnym hostem Node oraz zaczątkami dla Netlify i Vercela znajdziesz w przykładzie wysp serwerowych.

// np. serwer Node albo funkcja serverless/edge
import { createIslandsHandler } from 'sitelo/islands/server'

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

// webowy Request → Response | null (null = to nie żądanie wyspy)
export default { fetch: (request) => handleIslands(request) }

Na zwykłym http Node’a albo na expressie użyj zamiast tego createIslandsNodeHandler(options) — te same opcje, sygnatura (req, res, next). Podłącz automatycznie każdy moduł .js / .mjs spod src/islands/ przez createIslandsFromDirectory. Jeśli loader pobiera z innego źródła albo z innej ścieżki, podaj mountIslands({ endpoint: 'https://api.example.com/islands' }) i dopasuj do tego opcję endpoint handlera.

Lista kontrolna stabilności

Strategie ładowania

Domyślnie każda wyspa pobiera się, gdy tylko strona się załaduje, więc strona z ośmioma wyspami robi osiem równoczesnych żądań podczas pierwszego malowania. Podaj when, żeby odłożyć te, których nie widać od razu.

// Ładuj razem ze stroną — domyślnie.
island('cart', { id }, '<p>…</p>')

// Poczekaj na wywołanie w bezczynności.
island('recommendations', { id }, '<p>…</p>', { when: 'idle' })

// Poczekaj, aż wejdzie w pole widzenia.
island('comments', { postId }, '<p>Wczytywanie komentarzy…</p>', {
  when: 'visible',
  rootMargin: '400px',   // zacznij ładować 400px wcześniej
})

rootMargin dotyczy tylko 'visible' i domyślnie wynosi '200px'. Wyspy mają też limit czasu, zamiast kręcić się w nieskończoność:

mountIslands({
  timeout: 5000,        // na wyspę; 0 wyłącza. Domyślnie 10000
  rootMargin: '300px',  // domyślne dla wysp z `when: 'visible'`
})

mountIslands() kończy się, gdy natychmiastowe wyspy się ustabilizują — odłożone ładują się później samodzielnie i celowo nie czeka się na nie. Wyspa, która zawiodła albo przekroczyła limit czasu, zachowuje swój HTML zastępczy.

Propsy to niezaufane wejście

Propsy wyspy pochodzą od klienta. Są osadzone w stronie, odsyłane z żądaniem i każdy może je wcześniej zmienić:

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

Traktuj propsy, które dostaje Twoja wyspa, dokładnie jak parametr zapytania — waliduj je i nigdy nie używaj ich do sięgania po dane, których oglądający i tak nie ma prawa zobaczyć.

Gdy propsy wybierają dane uprzywilejowane, podpisz je. Ustaw sekret, a sitelo podpisze każde miejsce na wyspę podczas buildu i odrzuci wszystko inne kodem 403:

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

Tę samą zmienną czytają sitelo, sitelo preview i createIslandsHandler, więc dev, podgląd i produkcja są zgodne. Daj ten sam sekret swojemu hostingowi produkcyjnemu. Wolisz ustawić go w kodzie?

import { configureIslands } from 'sitelo/islands'

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

Podpisy to HMAC-SHA256 z nazwy wyspy i jej propsów, więc podpis wystawiony dla jednej wyspy nie da się odtworzyć na innej. Podpisywanie dowodzi, że propsy pochodzą z Twojego buildu — nie ukrywa ich, więc nadal muszą być niepoufne. Bez sekretu propsy są przyjmowane takie, jakie są, a ich walidacja to w całości zadanie Twojego modułu wyspy.

Warto wiedzieć