Skip to content
HomePagesHomePages template kit
Menu

Server vs client

The one concept the server-rendering rules assume — how a file is scoped server or client, and which rules stop at that boundary.

Several rules ban something “in server-rendered code” and say an island is exempt. This page is what “server-rendered” and “island” mean, once, so the rule pages do not each re-derive it.

A file is a client module if and only if its own directive prologue says "use client". Nothing else decides it — not what it imports, not what imports it, not the directory it sits in, not a build transform.

The consequence surprises people: the same code fails in one file and passes in the file next to it.

// sections/hero/Renderer.tsx — no directive → server-rendered
export function Renderer() {
return <p>{new Date().getFullYear()}</p>; // template-kit/no-nondeterminism
}
// sections/hero/Clock.tsx — directive → island
"use client";
export default function Clock() {
return <p>{new Date().getFullYear()}</p>; // fine
}

Importing an island from a server file does not make the server file an island. It also does not make the island a server file. Each is scoped on its own.

The directive prologue is the run of leading string-literal statements. Comments and blank lines do not end it; any other statement does.

"use strict";
"use client"; // still in the prologue → this file IS an island
const x = 1;
"use client"; // real code ended the prologue → this is NOT a directive, and NOT an island

The second file is the trap. It looks like an island, it is not one, and every server-rendering rule still applies to it. If you meant it to be an island, the directive must be the first statement in the file.

A server-rendered file (no directive) runs in the publisher and in the editor, and must be a pure function of its props: same props in, byte-identical HTML out. No clock, no randomness, no effects, no state, no event handlers, no network, no browser globals. It has none of those things available, and the two renders must agree.

An island (with the directive) is server-rendered inline with the rest of the page and then hydrated in the browser as its own React root. Effects, state, event handlers, and inline style are all legal there — that is what an island is for. Its props cross the server→browser boundary as JSON, which is the one new constraint it takes on (see serializable-island-props).

Interactivity is never banned. It is relocated: out of the server-rendered file, into a "use client" component in the same section folder, rendered from the Renderer like any other component.

Rule Exempt on a "use client" file?
no-nondeterminism yes — installs no visitors on an island
no-client-runtime-in-server yes
no-inline-style yes
every other rule no — it runs on island files too

no-hex deliberately does not exempt islands. The three exemptions above are all server-render concerns, and an island runs in the browser. A palette is not a render-path concern: the hydration boundary is not a palette boundary. A section carries no palette of its own in either render path, so "use client" plus style={{ color: "#ff0000" }} passes no-inline-style and still fails no-hex.

no-client-directive-in-contract is governed by this boundary but is not scoped by the directive — it is scoped by file glob. It runs on a section’s four contract files (Renderer.tsx, schema.ts, fill-spec.ts, fixtures.ts) and says those files may never carry the directive at all. They are the platform’s interface to the section and are read outside a browser; making one an island would take the section’s server-rendered HTML with it.

  • Islands — writing one, its props contract, and its editor options.
  • Rule index — every rule id, in both venues.