Серверные острова

Иногда одной области в остальном статичной страницы нужны свежие данные на каждый запрос — комментарии под закэшированной записью блога, наличие товара на карточке. Серверные острова оставляют страницу статичной и рендерят только эту область на сервере, когда страницу открывают.

1. Напишите остров

Остров — это модуль-фрагмент внутри src/islands/: обычный файл .js или .ts (не .ht.js, потому что острова — фрагменты, а не страницы). Идея та же, что и везде в sitelo: функция, возвращающая 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>`
}

Он получает { name, props, request } и должен вернуть строку HTML. Модули островов работают только на сервере — код без ссылок внутри src/ никогда не попадает в браузер.

2. Разместите его на странице

Импортируйте island() из sitelo/islands. Статическая сборка публикует запасной HTML; пропсы встраиваются в плейсхолдер, поэтому держите их небольшими и без секретов.

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

export default ({ params }) =>
  html(
    body(
      article('…статическое содержимое…'),
      island('comments', { postId: params.slug }, '<p>Загрузка комментариев…</p>'),
      script({ type: 'module', src: '/islands.js' }),
    ),
  )

3. Добавьте клиентский загрузчик

Крошечный скрипт запрашивает каждый отрендеренный фрагмент и подставляет его. Он проходит через обычный конвейер ресурсов, так что достаточно простой точки входа src/islands.js:

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

mountIslands()

В sitelo (dev) и sitelo preview это уже работает — оба отдают острова по адресу /_sitelo/islands/<name> из src/islands/. Preview загружает нативные модули .js / .mjs (как и хост на Node); острова на TypeScript поддерживаются в разработке через Vite.

Продакшн

Ваш статический хостинг продолжает отдавать страницы. Подключите небольшой обработчик там, где у вас выполняется серверный код — Node, serverless или edge-функция — и он отрендерит те же модули островов. Полный разбор с готовым Node-хостом и заготовками для Netlify и Vercel — в примере серверных островов.

// например, сервер Node или serverless/edge-функция
import { createIslandsHandler } from 'sitelo/islands/server'

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

// Web Request → Response | null (null = это не запрос острова)
export default { fetch: (request) => handleIslands(request) }

С обычным http из Node или с express используйте createIslandsNodeHandler(options) — те же параметры, сигнатура (req, res, next). Все модули .js / .mjs внутри src/islands/ можно подключить автоматически через createIslandsFromDirectory. Если загрузчик обращается к другому источнику или пути, передайте mountIslands({ endpoint: 'https://api.example.com/islands' }) и согласуйте это с параметром endpoint обработчика.

Что считается стабильным

Стратегии загрузки

По умолчанию каждый остров загружается сразу вместе со страницей, так что страница с восемью островами делает восемь одновременных запросов во время первой отрисовки. Передайте when, чтобы отложить те, что не видны сразу.

// Загружается сразу вместе со страницей — по умолчанию.
island('cart', { id }, '<p>…</p>')

// Ждёт момента простоя.
island('recommendations', { id }, '<p>…</p>', { when: 'idle' })

// Ждёт появления в области просмотра.
island('comments', { postId }, '<p>Загрузка комментариев…</p>', {
  when: 'visible',
  rootMargin: '400px',   // начать загрузку на 400px раньше
})

rootMargin применяется только к 'visible' и по умолчанию равен '200px'. Кроме того, острова завершаются по таймауту, а не крутятся вечно:

mountIslands({
  timeout: 5000,        // на остров; 0 отключает. По умолчанию 10000
  rootMargin: '300px',  // по умолчанию для островов с `when: 'visible'`
})

mountIslands() завершается, как только «немедленные» острова улеглись — отложенные загружаются позже сами по себе и намеренно не ожидаются. Остров, который упал или не уложился в таймаут, сохраняет свой запасной HTML.

Пропсы — недоверенный ввод

Пропсы острова приходят от клиента. Они встроены в страницу, возвращаются с запросом, и кто угодно может изменить их по пути:

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

Относитесь к пропсам, которые получает ваш остров, точно как к параметру запроса: проверяйте их и никогда не используйте, чтобы достать данные, которые посетителю и так не положены.

Когда пропсы выбирают привилегированные данные, подписывайте их. Задайте секрет — и sitelo подпишет каждый плейсхолдер во время сборки, отклоняя всё остальное с кодом 403:

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

Ту же переменную читают sitelo, sitelo preview и createIslandsHandler, так что разработка, preview и продакшн согласованы. Передайте тот же секрет своему продакшн-хосту. Предпочитаете задать его в коде?

import { configureIslands } from 'sitelo/islands'

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

Подписи — это HMAC-SHA256 от имени острова и его пропсов, поэтому подпись, выданная одному острову, не сработает для другого. Подпись доказывает, что пропсы пришли из вашей сборки — но не скрывает их, так что секретов в них по-прежнему быть не должно. Без секрета пропсы принимаются как есть, и их проверка целиком на вашем модуле острова.

Полезно знать