# island

> Load part of a page after the rest, for live or slow data.

An island is a component that can load after the rest of the page. Use islands in a site you build to static files, for the few parts that need live data, such as stock levels, a basket or a visitor's name. On a server, use them to send the page before its slowest parts are ready.

```kiln
{% island "components/stock" with { productId: product.id } %}
	<p>Checking stock…</p>
{% endisland %}
```

`island` takes a template and props, like [`component`](https://kilnlang.com/docs/tags/component/). The content inside the tag is the fallback, which the page shows until the island loads.

## How islands render

The template doesn't decide when an island renders. The code that renders the page does, with the `islands` render option:

- By default, an island renders in place, exactly like a component, and its fallback isn't used. This suits development, tests, pages your server renders for each request, and static sites with no server.
- With `islands: deferIslands(…)`, each island becomes a `<kiln-island>` element holding its fallback. In the browser, it fetches the island's HTML from your server and swaps it in. See [Deferring islands](#deferring-islands).
- With `islands: streamIslands()` and `renderStream`, the page is sent with each island's fallback, and each island follows in the same response when it is ready. See [Streaming islands](#streaming-islands).

So the same templates work for a static build, a server and your tests.

## Props

- Props must be JSON values: null, booleans, numbers, strings, lists and hashes. Pass dates as strings or timestamps. The rule is the same when islands render in place, so a template that works in development also works deferred.
- The island only sees its props and globals, like a component.
- An island has no slots. The content inside the tag is only the fallback, and it renders in the caller's scope.
- A deferred or streamed island renders on its own, so it can't [add to the page's outlets](https://kilnlang.com/docs/tags/outlet/).

Make the fallback the same size as the island, so the page doesn't jump when the island loads.

## Deferring islands

Pass the same `deferIslands` object to every page you render in a build. It records each island it meets, and its `manifest` lists them all. Save the manifest with your server code.

```ts
import { writeFile } from "node:fs/promises";
import { deferIslands } from "@lucidkiln/core";

const islands = deferIslands({ url: "/_islands/" });

for (const page of pages) {
	await writeFile(page.file, await kiln.render(page.template, page.data, { islands }));
}

await writeFile("build/islands.json", JSON.stringify(islands.manifest));
```

The island from the top of this page becomes:

```html
<kiln-island src="/_islands/4f0b6c1d9e2a7385">
	<p>Checking stock…</p>
</kiln-island>
```

The manifest holds each island's template and props, by id:

```json
{
	"4f0b6c1d9e2a7385": {
		"template": "components/stock.kiln",
		"props": { "productId": 42 }
	}
}
```

- Only the id reaches the browser. The props stay in the manifest, on your server.
- An id comes from the template and props, so an island has the same id on every page and in every build. After you deploy, a page that's still cached somewhere asks for an id that either means the same island or doesn't exist.
- Kiln adds the id to the end of `url`, so `"/_islands?id="` works too.

| Option | Default | Description |
| --- | --- | --- |
| `url` | Required | The text before each island's id in its `src` |
| `script` | `true` | Whether Kiln adds the script that runs `<kiln-island>`. See [Content Security Policy](#content-security-policy) |

## Serving islands

Your server looks up the id in the manifest and renders the island with [`render`](https://kilnlang.com/docs/api/rendering/), as it would a page. Kiln doesn't add a route, so this works with any server. Here it is in a Cloudflare Worker:

```ts
import type { IslandManifest } from "@lucidkiln/core";
import islands from "./build/islands.json";

const manifest: IslandManifest = islands;

export default {
	async fetch(request: Request) {
		const { pathname } = new URL(request.url);
		if (pathname.startsWith("/_islands/")) {
			const id = pathname.slice("/_islands/".length);
			if (!Object.hasOwn(manifest, id)) {
				return new Response("Not found", { status: 404 });
			}
			const { template, props } = manifest[id];
			const html = await kiln.render(template, props, {
				state: { cookie: request.headers.get("cookie") },
			});
			return new Response(html, {
				headers: {
					"content-type": "text/html; charset=utf-8",
					"cache-control": "private, no-store",
					"x-robots-tag": "noindex",
				},
			});
		}
		return fetch(request);
	},
};
```

- Each island loads with its own request, which carries the visitor's cookies. Pass what the island needs from the request as [`state`](https://kilnlang.com/docs/api/extending/#request-data).
- Check who is signed in here, as you would for any page. An id says which island to render, not who may see it.
- Choose the caching for each island. Use `public` with a short `max-age` for data everyone sees, and `private` for one visitor's data.
- `x-robots-tag: noindex` stops search engines listing island responses as pages.
- Islands inside an island render in place, since the island is already live.

## Streaming islands

On a server, pass `streamIslands()` to [`renderStream`](https://kilnlang.com/docs/api/streaming/). The page doesn't wait for its islands: each one starts rendering when the page reaches it, and its fallback is sent in its place. When the page is done, each island's HTML follows as soon as it is ready, in the same response.

```ts
import { streamIslands } from "@lucidkiln/core";

const stream = await kiln.renderStream("pages/product", { product }, {
	islands: streamIslands(),
	signal: request.signal,
});
```

The island from the top of this page is sent first as:

```html
<kiln-island stream="1">
	<p>Checking stock…</p>
</kiln-island>
```

And once it is ready, after the rest of the page:

```html
<template><p>Stock for 42</p></template><kiln-island fill="1"></kiln-island>
```

- Islands arrive in the order they finish, not the order they appear in the page.
- Kiln adds them after everything the page writes, even `</html>`. Browsers put them in the body, where the script moves each into its place. Without JavaScript, the fallbacks stay.
- An island renders with the same `state`, locale and time zone as the page.
- If an island fails, the stream ends with its error, as for any error once the page has been sent. Islands that haven't arrived keep their fallbacks.
- The islands count towards the page's limits. `maxOutputLength` and `maxSteps` count them as part of the page, and each island's nesting starts at the depth of its tag.
- Islands inside `cache`, `set`, `apply` or a macro render in place, since their output may be reused or never sent.
- `render` and `renderBlock` can't stream, so with `streamIslands()` they render islands in place.

| Option | Default | Description |
| --- | --- | --- |
| `script` | `true` | Whether Kiln adds the script that runs `<kiln-island>`. See [Content Security Policy](#content-security-policy) |

## The kiln-island element

Kiln adds a small script before the first deferred or streamed island in each page. It defines `<kiln-island>`:

- As soon as a deferred island's element is on the page, it fetches its `src`. A streamed island waits for its HTML to arrive.
- If the response succeeds and is HTML, the element replaces itself with it. Scripts in the HTML run.
- If the request fails, or JavaScript is off, the fallback stays.
- The element has `display: contents`, so it doesn't change the page's layout.

### Content Security Policy

If your Content Security Policy blocks inline scripts, pass `script: false` and serve the script as a file, or add it with your nonce. `islandScript` holds its source.

```ts
import { deferIslands, islandScript } from "@lucidkiln/core";

const islands = deferIslands({ url: "/_islands/", script: false });
await writeFile("build/kiln-island.js", islandScript);
```

Then load it in your layout, with `<script src="/kiln-island.js" defer></script>`. Islands already on the page start loading when it runs.

## Islands in cached sections

A [`cache`](https://kilnlang.com/docs/tags/cache/) section can hold islands. A saved copy remembers its deferred islands, so reusing it in a later render still adds them to the manifest. Copies rendered in place and copies with deferred islands are saved separately. Streamed islands render in place inside a cached section.
