Island server

sitelo membangun HTML statis. Island server mengisi bagian yang harus segar — jam, komentar, stok, apa pun yang butuh permintaannya. Resep ini membangun halaman dengan island waktu, lalu menjalankan server Node kecil yang menyajikan dist/ dan /_sitelo/islands.

Salinan proyek ini ada di repositori sitelo, di bawah examples/islands/.

Yang Anda dapat

Tata letak proyek

my-site/
  sitelo.config.js
  server.js              # Host Node: dist statis + island
  netlify.toml           # rewrite Netlify → function
  vercel.json            # rewrite Vercel → rute api
  package.json
  netlify/functions/
    islands.mjs          # handler island Netlify
  api/islands/
    [...path].js         # handler island Vercel
  src/
    index.ht.js          # halaman dengan placeholder island
    js/
      islands.js         # loader klien (dibundel ke dist/)
    islands/
      time.js            # modul fragmen khusus server
    css/
      styles.css
export default {
  site: 'https://example.com',
}

1. Modul island

.js biasa (bukan .ht.js). Menerima { name, props, request } dan mengembalikan string HTML. Yang ini memakai waktu permintaan dan user-agent agar Anda bisa melihat ia dirender per permintaan.

export default function time({ props, request }) {
  const label = typeof props?.label === 'string' ? props.label : 'Waktu server'
  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">Dirender saat diminta untuk <code>${escapeHtml(ua.slice(0, 48))}</code></p>
  `
}

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

2. Tempatkan island di sebuah halaman

island() menyematkan props ke penampungnya. Build mengirim cadangannya; pemuat menggantinya ketika endpoint menjawab.

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

export default () =>
  html({ lang: 'id' },
    head(
      title('Demo island server'),
      link({ rel: 'stylesheet', href: '/styles.css' }),
    ),
    body(
      h1('Halaman statis, island hidup'),
      p('HTML ini dibangun sekali. Kotak di bawah diisi saat permintaan datang.'),
      island(
        'time',
        { label: 'Sekarang' },
        '<p>Memuat waktu server…</p>',
      ),
      script({ type: 'module', src: '/islands.js' }),
    ),
  )

3. Pemuat klien

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

Setelah sitelo build, proses ini menyajikan dist/ dan merender island dengan createIslandsNodeHandler dari sitelo/islands/server. Modul island tetap di luar dist/ — host mengimpornya dari 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('Tidak ditemukan')
    }
  })
})

server.listen(port, () => {
  console.log(`Mendengarkan di http://localhost:${port}`)
})

5. Build dan jalankan

npm install
sitelo build
node server.js

Buka http://localhost:3000. Anda mestinya melihat cadangannya sekejap, lalu waktu server. Tekan muat ulang — cap waktunya berubah. Di sitelo (pengembangan) dan sitelo preview Anda tidak butuh server.js: CLI-nya sudah menyajikan /_sitelo/islands.

Penerapan

Contoh ini menyertakan kerangka host di samping server Node:

Untuk tanpa server atau edge di tempat lain, gunakan createIslandsHandler (Request → Response web) — lihat dokumentasi Island server. Arahkan mountIslands({ endpoint }) ke URL fungsi itu bila tidak satu asal.

Catatan

Hosting statis saja

GitHub Pages, S3 biasa, dan hosting sejenis tidak punya proses server. Tanpa endpoint island, HTML cadangan cukup tetap di tempatnya — halamannya tetap jalan, hanya tanpa fragmen langsungnya.

Jaga props tetap kecil

Props berjalan di atribut HTML dan string kueri permintaan. Jangan menaruh rahasia atau muatan besar di sana — ambil semua itu di dalam modul island, di server.

Dokumentasi Island server · Situs dasar / penerapan · Semua contoh