Skip to content
HomePagesHomePages template kit
Menu

template-kit/no-nondeterminism

The template-kit/no-nondeterminism authoring rule — what it enforces, why, and how to fix it.

No clock reads and no randomness in server-rendered code. Islands are exempt.

Whether a file is server-rendered or an island is decided per file, by the "use client" directive — see server vs client. On an island this rule installs no visitors at all.

In a server-rendered file, two shapes are banned:

  • A read of a nondeterministic source: Date.now, Math.random, crypto.randomUUID, crypto.getRandomValues, performance.now. The member expression alone is enough — Date.now reports even without a call, because handing the function itself somewhere is the same read one hop later.
  • new Date() with no arguments. That reads the clock.

Two carve-outs:

  • new Date(props.listed_at) is fine. It parses a value the section was given, which is perfectly deterministic. Only the zero-argument form reads the clock.
  • Your own definitions are yours. If the object name resolves to a real local declaration — const performance = { now: () => 1 }; — it is not the global, and the rule leaves it alone.

The publisher and the editor both run a section’s Renderer, and they must produce byte-identical HTML from the same props. A clock read or a random value makes the two renders disagree: the section’s output is hashed, so a Date.now() in the markup means the section looks changed on every single render — a fresh version each publish, and an editor canvas that never settles.

An island does not have this problem. It runs in the browser, after hydration, and its output is nobody’s content hash. That is why it is exempt.

Compute the value at fill time and pass it in as a slot value — that is the right route for anything that is content. Reach for an island only when the value is genuinely live (a countdown, a “time since listed” that must tick).

import { Section, Slot } from "@homepages/template-kit";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) {
return (
<Section>
<Slot id="headline" as="h1" textLeaf>
<span>{slots.headline}</span>
</Slot>
<p>© {new Date().getFullYear()} — all rights reserved</p>
</Section>
);
}

The year is content, so it becomes a slot the fill pipeline supplies. Declare it in schema.ts, and the Renderer just renders what it was given:

import { Section, Slot } from "@homepages/template-kit";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) {
return (
<Section>
<Slot id="headline" as="h1" textLeaf>
<span>{slots.headline}</span>
</Slot>
<Slot id="copyright_year" textLeaf>
<p>© {slots.copyright_year} — all rights reserved</p>
</Slot>
</Section>
);
}

If the value must be live in the browser rather than baked at fill time, move that markup into a "use client" component in the section folder instead, and render it from the Renderer — the rule does not run there.