Skip to content
HomePagesHomePages template kit

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.

Reading the directive prologue is the whole test, and it is deliberately the same test Next.js applies — so the instinct you already have transfers, and there is no second rule to learn for predicting which side a file lands on.

It is also what keeps the lint preset cheap. Deciding a file’s side means reading its leading statements — no build transform, no import graph, no type information and no project config. That is why setting up @homepages/eslint-plugin-template is two lines and never asks for your tsconfig.json.

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.

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 contract files (Renderer.tsx, section.ts and 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.

It is not an exemption switch. Marking a file "use client" does not opt it out of the authoring rules. Exactly three of them stop at this boundary; every other rule runs on an island exactly as it runs on a server-rendered file.

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.

It is not a promise of interactivity in the editor

Section titled “It is not a promise of interactivity in the editor”

The directive decides how a file is linted and how it behaves on the published page. It says nothing about the editor canvas, where an island stays static SSR HTML unless it declares live: true — a separate declaration, and a required one. See require-island-editor.

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