Ilhas de servidor

Às vezes uma região de uma página, de resto estática, precisa de dados frescos a cada pedido — os comentários por baixo de um artigo em cache, uma etiqueta de stock numa página de produto. As ilhas de servidor mantêm a página estática e renderizam só essa região num servidor, quando a página é vista.

1. Escreve a ilha

Uma ilha é um módulo de fragmento dentro de src/islands/ — um ficheiro .js ou .ts normal (não .ht.js, porque as ilhas são fragmentos, não páginas). A mesma ideia de todo o sitelo: uma função que devolve 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>`
}

Recebe { name, props, request } e tem de devolver uma string HTML. Os módulos de ilha são exclusivos do servidor — o código sem referências dentro de src/ nunca chega ao navegador.

2. Coloca-a numa página

Importa island() de sitelo/islands. A compilação estática publica o HTML de reserva; as props são embutidas no marcador, por isso mantém-nas pequenas e sem segredos.

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

export default ({ params }) =>
  html(
    body(
      article('…conteúdo estático…'),
      island('comments', { postId: params.slug }, '<p>A carregar comentários…</p>'),
      script({ type: 'module', src: '/islands.js' }),
    ),
  )

3. Acrescenta o carregador de cliente

Um script minúsculo vai buscar cada fragmento renderizado e troca-o. Passa pelo pipeline de recursos habitual, por isso basta um ponto de entrada src/islands.js normal:

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

mountIslands()

Em sitelo (dev) e em sitelo preview isto já funciona — ambos servem as ilhas em /_sitelo/islands/<name> a partir de src/islands/. O preview carrega módulos .js / .mjs nativos (tal como um host Node); as ilhas em TypeScript funcionam em desenvolvimento graças ao Vite.

Produção

O teu alojamento estático continua a servir as páginas. Monta um pequeno handler onde correres código de servidor — Node, serverless ou uma função edge — e ele renderiza os mesmos módulos de ilha. Para um percurso completo com um host Node executável e esboços para Netlify e Vercel, vê o exemplo de ilhas de servidor.

// por exemplo um servidor Node, ou uma função serverless/edge
import { createIslandsHandler } from 'sitelo/islands/server'

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

// Web Request → Response | null (null = não é um pedido de ilha)
export default { fetch: (request) => handleIslands(request) }

Com http do Node simples ou com express, usa antes createIslandsNodeHandler(options) — as mesmas opções, assinatura (req, res, next). Podes ligar automaticamente todos os módulos .js / .mjs de src/islands/ com createIslandsFromDirectory. Se o carregador for buscar a outra origem ou caminho, passa mountIslands({ endpoint: 'https://api.example.com/islands' }) e faz corresponder à opção endpoint do handler.

Lista de estabilidade

Estratégias de carregamento

Por omissão cada ilha vai buscar os dados assim que a página carrega, por isso uma página com oito ilhas faz oito pedidos em simultâneo durante a primeira pintura. Passa when para adiar as que não estão logo à vista.

// Carrega assim que a página carrega — o valor por omissão.
island('cart', { id }, '<p>…</p>')

// Espera por um callback de inatividade.
island('recommendations', { id }, '<p>…</p>', { when: 'idle' })

// Espera até entrar no ecrã.
island('comments', { postId }, '<p>A carregar comentários…</p>', {
  when: 'visible',
  rootMargin: '400px',   // começa a carregar 400px antes
})

rootMargin só se aplica a 'visible' e vale '200px' por omissão. As ilhas também expiram em vez de ficarem a rodar para sempre:

mountIslands({
  timeout: 5000,        // por ilha; 0 desativa. Por omissão 10000
  rootMargin: '300px',  // valor por omissão para ilhas com `when: 'visible'`
})

mountIslands() resolve assim que as ilhas imediatas assentam — as adiadas carregam mais tarde por si e, deliberadamente, não são esperadas. Uma ilha que falhe ou expire mantém o seu HTML de reserva.

As props são entrada não fiável

As props de uma ilha vêm do cliente. São embutidas na página, devolvidas no pedido, e qualquer pessoa as pode editar pelo caminho:

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

Trata as props que a tua ilha recebe exatamente como um parâmetro de query — valida-as, e nunca as uses para ir buscar dados a que quem visita não tenha já direito.

Quando as props selecionam dados privilegiados, assina-as. Define um segredo e o sitelo assina cada marcador na compilação, rejeitando tudo o resto com um 403:

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

A mesma variável é lida por sitelo, sitelo preview e createIslandsHandler, por isso desenvolvimento, preview e produção concordam. Dá o mesmo segredo ao teu host de produção. Preferes defini-lo em código?

import { configureIslands } from 'sitelo/islands'

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

As assinaturas são HMAC-SHA256 sobre o nome da ilha e as suas props, por isso uma assinatura emitida para uma ilha não pode ser reutilizada noutra. Assinar prova que as props vieram da tua compilação — não as esconde, por isso continuam a não poder ser segredos. Sem segredo, as props são aceites tal como estão e validá-las cabe inteiramente ao teu módulo de ilha.

Coisas a saber