Wyspy serwerowe

sitelo buduje statyczny HTML. Wyspy serwerowe uzupełniają te kawałki, które muszą być świeże — zegar, komentarze, dostępność, cokolwiek potrzebuje żądania. Ten przepis buduje stronę z wyspą czasu, a potem uruchamia mały serwer Node, który serwuje dist/ i /_sitelo/islands.

Kopia tego projektu leży w repozytorium sitelo, pod examples/islands/.

Co dostajesz

Układ projektu

my-site/
  sitelo.config.js
  server.js              # Host Node: statyczny dist + wyspy
  netlify.toml           # przepisanie Netlify → funkcja
  vercel.json            # przepisanie Vercel → trasa api
  package.json
  netlify/functions/
    islands.mjs          # obsługa wysp dla Netlify
  api/islands/
    [...path].js         # obsługa wysp dla Vercela
  src/
    index.ht.js          # strona z miejscem na wyspę
    js/
      islands.js         # loader kliencki (wchodzi do paczki w dist/)
    islands/
      time.js            # moduł fragmentu tylko po stronie serwera
    css/
      styles.css
export default {
  site: 'https://example.com',
}

1. Moduł wyspy

Zwykły .js (nie .ht.js). Dostaje { name, props, request } i zwraca ciąg HTML. Ten używa czasu żądania i user-agenta, żebyś widział, że renderuje się przy każdym żądaniu.

export default function time({ props, request }) {
  const label = typeof props?.label === 'string' ? props.label : 'Czas serwera'
  const now = new Date().toISOString()
  const ua = request?.headers?.get?.('user-agent') ?? 'unknown'

  return `
    <p><strong>${label}:</strong> <time datetime="${now}">${now}</time></p>
    <p class="muted">Wyrenderowane na żądanie o <code>${escapeHtml(ua.slice(0, 48))}</code></p>
  `
}

function escapeHtml(value) {
  return value
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
}

2. Umieść wyspę na stronie

island() osadza propsy w miejscu na wyspę. Build publikuje treść zastępczą; loader podmienia ją, gdy punkt końcowy odpowie.

import { html, head, title, link, body, h1, p, script } from 'javascript-to-html'
import { island } from 'sitelo/islands'

export default () =>
  html({ lang: 'pl' },
    head(
      title('Demo wysp serwerowych'),
      link({ rel: 'stylesheet', href: '/styles.css' }),
    ),
    body(
      h1('Statyczna strona, żywa wyspa'),
      p('Ten HTML zbudowano raz. Ramka poniżej wypełnia się w chwili żądania.'),
      island(
        'time',
        { label: 'Właśnie teraz' },
        '<p>Wczytywanie czasu serwera…</p>',
      ),
      script({ type: 'module', src: '/islands.js' }),
    ),
  )

3. Loader kliencki

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

mountIslands()
body {
  font-family: system-ui, sans-serif;
  max-width: 36rem;
  margin: 2rem auto;
  padding: 0 1rem;
  line-height: 1.5;
}

[data-sitelo-island] {
  margin: 1.5rem 0;
  padding: 1rem 1.25rem;
  border: 1px solid #ccc;
}

[data-sitelo-island-state='loading'] {
  opacity: 0.7;
}

.muted {
  color: #666;
  font-size: 0.9rem;
}

4. Host Node

Po sitelo build ten proces serwuje dist/ i renderuje wyspy przez createIslandsNodeHandler z sitelo/islands/server. Moduły wysp zostają poza dist/ — host importuje je z src/.

import fs from 'node:fs'
import http from 'node:http'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { createIslandsFromDirectory, createIslandsNodeHandler } from 'sitelo/islands/server'

const root = path.dirname(fileURLToPath(import.meta.url))
const dist = path.join(root, 'dist')
const port = Number(process.env.PORT) || 3000

const handleIslands = createIslandsNodeHandler({
  islands: createIslandsFromDirectory(path.join(root, 'src/islands')),
})

const MIME = {
  '.html': 'text/html; charset=utf-8',
  '.js': 'text/javascript; charset=utf-8',
  '.css': 'text/css; charset=utf-8',
  '.svg': 'image/svg+xml',
  '.png': 'image/png',
  '.ico': 'image/x-icon',
  '.xml': 'application/xml',
  '.json': 'application/json',
}

function sendFile(res, filePath) {
  const ext = path.extname(filePath)
  res.statusCode = 200
  res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream')
  fs.createReadStream(filePath).pipe(res)
}

function resolveStatic(urlPath) {
  const clean = decodeURIComponent(urlPath.split('?')[0])
  const relative = clean === '/' ? 'index.html' : clean.replace(/^\/+/, '')
  const candidate = path.normalize(path.join(dist, relative))

  if (!candidate.startsWith(dist + path.sep) && candidate !== dist) {
    return null
  }
  if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
    return candidate
  }

  const asIndex = path.join(candidate, 'index.html')
  if (fs.existsSync(asIndex) && fs.statSync(asIndex).isFile()) {
    return asIndex
  }

  return null
}

const server = http.createServer(async (req, res) => {
  await handleIslands(req, res, () => {
    const file = resolveStatic(req.url ?? '/')
    if (file) {
      sendFile(res, file)
      return
    }

    const notFound = path.join(dist, '404.html')
    res.statusCode = 404
    if (fs.existsSync(notFound)) {
      sendFile(res, notFound)
    } else {
      res.setHeader('Content-Type', 'text/plain; charset=utf-8')
      res.end('Nie znaleziono')
    }
  })
})

server.listen(port, () => {
  console.log(`Nasłuchuje na http://localhost:${port}`)
})

5. Build i uruchomienie

npm install
sitelo build
node server.js

Otwórz http://localhost:3000. Powinieneś przez chwilę zobaczyć treść zastępczą, a potem czas serwera. Odśwież — znacznik czasu się zmieni. W sitelo (dev) i w sitelo preview nie potrzebujesz server.js: CLI już serwuje /_sitelo/islands.

Wdrożenie

Ten przykład przynosi zaczątki hostów obok serwera Node:

Dla serverless albo edge gdzie indziej użyj createIslandsHandler (webowy Request → Response) — zobacz dokumentację wysp serwerowych. Jeśli funkcja nie jest z tego samego źródła, wskaż jej adres przez mountIslands({ endpoint }).

Uwagi

Same hostingi statyczne

GitHub Pages, czyste S3 i podobne hostingi nie mają procesu serwerowego. Bez punktu końcowego wysp HTML zastępczy po prostu zostaje — strony nadal działają, tylko bez żywego fragmentu.

Trzymaj propsy małe

Propsy podróżują w atrybucie HTML i w query stringu żądania. Nie wkładaj tam sekretów ani dużych ładunków — te pobieraj wewnątrz modułu wyspy, na serwerze.

Dokumentacja wysp serwerowych · Podstawowa strona / wdrożenie · Wszystkie przykłady