Islas de servidor

A veces una región de una página por lo demás estática necesita datos frescos para cada petición: los comentarios bajo una entrada de blog cacheada, una etiqueta de stock en una página de producto. Las islas de servidor mantienen la página estática y renderizan solo esa región en un servidor cuando alguien la visita.

1. Escribe la isla

Una isla es un módulo de fragmento dentro de src/islands/ — un archivo .js o .ts normal (no .ht.js, porque las islas son fragmentos, no páginas). La misma idea que en todo sitelo: una función que devuelve 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>`
}

Recibe { name, props, request } y debe devolver una cadena HTML. Los módulos de isla son exclusivos del servidor: el código sin referenciar dentro de src/ nunca llega al navegador.

2. Colócala en una página

Importa island() desde sitelo/islands. La compilación estática publica el HTML de reserva; las props se incrustan en el marcador de posición, así que mantenlas pequeñas y sin secretos.

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

export default ({ params }) =>
  html(
    body(
      article('…contenido estático…'),
      island('comments', { postId: params.slug }, '<p>Cargando comentarios…</p>'),
      script({ type: 'module', src: '/islands.js' }),
    ),
  )

3. Añade el cargador de cliente

Un script mínimo descarga cada fragmento renderizado y lo inserta. Pasa por el pipeline de recursos habitual, así que basta con una entrada src/islands.js normal:

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

mountIslands()

En sitelo (dev) y en sitelo preview esto ya funciona: ambos sirven las islas en /_sitelo/islands/<name> desde src/islands/. Preview carga módulos .js / .mjs nativos (igual que un host de Node); las islas en TypeScript funcionan en desarrollo gracias a Vite.

Producción

Tu hosting estático sigue sirviendo las páginas. Monta un pequeño manejador allí donde ejecutes código de servidor —Node, serverless o una función edge— y renderizará los mismos módulos de isla. Para un recorrido completo con un host de Node ejecutable y plantillas para Netlify y Vercel, consulta el ejemplo de islas de servidor.

// p. ej. un servidor Node, o una función serverless/edge
import { createIslandsHandler } from 'sitelo/islands/server'

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

// Web Request → Response | null (null = no es una petición de isla)
export default { fetch: (request) => handleIslands(request) }

Con http de Node a secas o con express, usa createIslandsNodeHandler(options) en su lugar — mismas opciones, firma (req, res, next). Puedes conectar automáticamente todos los módulos .js / .mjs de src/islands/ con createIslandsFromDirectory. Si el cargador descarga desde otro origen o ruta, pasa mountIslands({ endpoint: 'https://api.example.com/islands' }) y haz que coincida con la opción endpoint del manejador.

Lista de estabilidad

Estrategias de carga

Por defecto, cada isla se descarga en cuanto carga la página, así que una página con ocho islas lanza ocho peticiones simultáneas durante el primer pintado. Pasa when para aplazar las que no se ven de inmediato.

// Carga en cuanto lo hace la página — el valor por defecto.
island('cart', { id }, '<p>…</p>')

// Espera a un callback de inactividad.
island('recommendations', { id }, '<p>…</p>', { when: 'idle' })

// Espera a que entre en pantalla.
island('comments', { postId }, '<p>Cargando comentarios…</p>', {
  when: 'visible',
  rootMargin: '400px',   // empieza a cargar 400px antes
})

rootMargin solo se aplica a 'visible' y su valor por defecto es '200px'. Además, las islas expiran en lugar de quedarse girando para siempre:

mountIslands({
  timeout: 5000,        // por isla; 0 lo desactiva. Por defecto 10000
  rootMargin: '300px',  // valor por defecto para islas con `when: 'visible'`
})

mountIslands() se resuelve en cuanto las islas inmediatas se han asentado; las aplazadas cargan después por su cuenta y deliberadamente no se esperan. Una isla que falla o expira conserva su HTML de reserva.

Las props son entrada no confiable

Las props de una isla las suministra el cliente. Se incrustan en la página, se devuelven en la petición, y cualquiera puede editarlas antes:

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

Trata las props que recibe tu isla exactamente como un parámetro de consulta: valídalas, y nunca las uses para consultar datos que quien mira no tenga ya derecho a ver.

Cuando las props seleccionan datos privilegiados, fírmalas. Define un secreto y sitelo firmará cada marcador de posición en tiempo de compilación, rechazando cualquier otro con un 403:

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

La misma variable la leen sitelo, sitelo preview y createIslandsHandler, así que desarrollo, preview y producción coinciden. Dale a tu host de producción el mismo secreto. ¿Prefieres definirlo en código?

import { configureIslands } from 'sitelo/islands'

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

Las firmas son HMAC-SHA256 sobre el nombre de la isla y sus props, así que una firma emitida para una isla no puede reutilizarse en otra. Firmar demuestra que las props vienen de tu compilación — no las oculta, así que siguen sin poder ser secretas. Sin un secreto, las props se aceptan tal cual y validarlas es responsabilidad exclusiva de tu módulo de isla.

Cosas que conviene saber