Îlots serveur

sitelo construit du HTML statique. Les îlots serveur remplissent les parties qui doivent être fraîches — une horloge, des commentaires, un stock, tout ce qui dépend de la requête. Cette recette construit une page avec un îlot d’heure, puis lance un petit serveur Node qui sert dist/ et /_sitelo/islands.

Une copie de ce projet se trouve dans le dépôt sitelo, sous examples/islands/.

Ce que vous obtenez

Structure du projet

my-site/
  sitelo.config.js
  server.js              # hôte Node : dist statique + îlots
  netlify.toml           # rewrite Netlify → fonction
  vercel.json            # rewrite Vercel → route api
  package.json
  netlify/functions/
    islands.mjs          # gestionnaire d’îlots Netlify
  api/islands/
    [...path].js         # gestionnaire d’îlots Vercel
  src/
    index.ht.js          # page avec un emplacement d’îlot
    js/
      islands.js         # chargeur client (inclus dans dist/)
    islands/
      time.js            # module de fragment côté serveur uniquement
    css/
      styles.css
export default {
  site: 'https://example.com',
}

1. Module d’îlot

Un simple fichier .js (pas .ht.js). Il reçoit { name, props, request } et renvoie une chaîne HTML. Celui-ci utilise l’heure de la requête et le user-agent pour que vous voyiez qu’il est rendu à chaque requête.

export default function time({ props, request }) {
  const label = typeof props?.label === 'string' ? props.label : 'Heure du serveur'
  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">Rendu à la requête pour <code>${escapeHtml(ua.slice(0, 48))}</code></p>
  `
}

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

2. Placer l’îlot dans une page

island() intègre les props dans l’emplacement réservé. Le build livre le repli ; le chargeur le remplace quand le point de terminaison répond.

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

export default () =>
  html({ lang: 'fr' },
    head(
      title('Démo des îlots serveur'),
      link({ rel: 'stylesheet', href: '/styles.css' }),
    ),
    body(
      h1('Page statique, îlot en direct'),
      p('Ce HTML a été construit une seule fois. L’encadré ci-dessous est rempli au moment de la requête.'),
      island(
        'time',
        { label: 'À l’instant' },
        '<p>Chargement de l’heure du serveur…</p>',
      ),
      script({ type: 'module', src: '/islands.js' }),
    ),
  )

3. Chargeur client

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. Hôte Node

Après sitelo build, ce processus sert dist/ et rend les îlots avec createIslandsNodeHandler depuis sitelo/islands/server. Les modules d’îlot restent hors de dist/ — l’hôte les importe depuis 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('Introuvable')
    }
  })
})

server.listen(port, () => {
  console.log(`À l’écoute sur http://localhost:${port}`)
})

5. Construire et lancer

npm install
sitelo build
node server.js

Ouvrez http://localhost:3000. Vous devriez voir brièvement le repli, puis l’heure du serveur. Rafraîchissez — l’horodatage change. Dans sitelo (dev) et sitelo preview, vous n’avez pas besoin de server.js : la CLI sert déjà /_sitelo/islands.

Déploiement

Cet exemple fournit des ébauches d’hôtes à côté du serveur Node :

Pour du serverless ou de l’edge ailleurs, utilisez createIslandsHandler (Request web → Response) — voir la documentation des îlots serveur. Pointez mountIslands({ endpoint }) vers l’URL de cette fonction si elle n’est pas de même origine.

Remarques

Hôtes purement statiques

GitHub Pages, S3 simple et hôtes similaires n’ont aucun processus serveur. Sans point de terminaison d’îlots, le HTML de repli reste simplement en place — les pages fonctionnent toujours, juste sans le fragment en direct.

Gardez les props petites

Les props voyagent dans l’attribut HTML et dans la chaîne de requête. N’y mettez ni secrets ni gros contenus — récupérez-les dans le module d’îlot, sur le serveur.

Documentation des îlots serveur · Site de base / déploiement · Tous les exemples