Skip to content
HomePagesHomePages template kit

island-server-frame

"An island's `serverFrame` declaration must match what it actually server-renders."

Venue: check — An island’s serverFrame declaration must match what it actually server-renders.

An island may declare that it has no server-rendered first frame:

export const editor = { live: true };
export const serverFrame = false; // nothing to render until a browser draws it

Declaring it removes the island’s implementation — and everything it imports — from the section’s renderer bundle. The server emits the island’s marker and nothing inside it; the browser mounts the island fresh instead of hydrating.

check renders every section’s fixtures with its islands marked, so it can see what each island actually produced, and holds the declaration to it:

What the island declared What it rendered What check does
serverFrame = false real markup fails — that markup is being deleted from published pages
nothing an empty element warns, naming the bytes the declaration would drop
anything else anything nothing to say

“Real markup” means content a visitor or a crawler gets with no JavaScript: text, or an element that is content standing empty — an <img>, <svg>, <video>, <iframe>, a form control. An empty <div> is not; neither is a <canvas>. That is the whole distinction the declaration turns on.

The two directions are asymmetric on purpose, because the evidence behind them is.

The failure is content deletion. A renderer build acts on the declaration: it never loads the module, so whatever the component would have returned is simply absent from the HTML every published page serves. Nothing else catches this. The section still renders, the island still hydrates in a browser, and the page looks right to the author who wrote it — while a crawler, a visitor with JavaScript off, and the page’s own layout before hydration all see a hole. Since the declaration is the thing that caused the deletion, grading it against a real render is the only place the mistake is visible.

The warning is advice, and cannot be more than that. Fixtures are one set of props among all the props a section can be filled with. An island that renders nothing for the fixtures you wrote may render plenty for a prop shape none of them reaches, and that island is correct as written. Refusing it would make the fixture corpus, rather than the contract, the thing you author against. So the empty direction reports what it saw, names what the declaration would be worth, and leaves the decision with you.

If it failed: delete the declaration. This island renders content the server can produce, and something downstream depends on it.

If it warned: decide which of two things is true.

  • The island’s content genuinely does not exist until a browser draws it — a map engine painting into a canvas, a chart measuring its container. Declare it, and the renderer stops carrying the library.
  • The island does render content, just not for the props these fixtures supply. Leave it undeclared. Nothing is refused, and you can silence the report by adding a fixture that exercises the case if it is worth having one.

An island declaring serverFrame = false must also be live: true in the editor (see require-island-editor): an island with no server frame that also does not hydrate in the canvas is a permanent blank hole.

sections/amenities/components/AmenityList.tsx
"use client";
import { useState } from "react";
export const editor = { live: true };
export const serverFrame = false; // ← but this component's rows ARE its content
export default function AmenityList({ amenities }: { amenities: string[] }) {
const [open, setOpen] = useState(false);
const shown = open ? amenities : amenities.slice(0, 3);
return (
<ul>
{shown.map((amenity) => (
<li key={amenity}>{amenity}</li>
))}
<li>
<button type="button" onClick={() => { setOpen(!open); }}>
{open ? "Show fewer" : "Show all"}
</button>
</li>
</ul>
);
}
sections/amenities/components/AmenityList.tsx
"use client";
import { useState } from "react";
export const editor = { live: true };
// No serverFrame declaration: the rows below are this island's first frame, and the
// server renders them.
export default function AmenityList({ amenities }: { amenities: string[] }) {
const [open, setOpen] = useState(false);
const shown = open ? amenities : amenities.slice(0, 3);
return (
<ul>
{shown.map((amenity) => (
<li key={amenity}>{amenity}</li>
))}
<li>
<button type="button" onClick={() => { setOpen(!open); }}>
{open ? "Show fewer" : "Show all"}
</button>
</li>
</ul>
);
}