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.nowreports 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.
Reason
Section titled “Reason”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).
Before
Section titled “Before”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.
See also
Section titled “See also”- Server vs client — how a file is scoped, and what an island may do.
- Schema system — declaring the slot the value arrives in.