Skip to content
HomePagesHomePages template kit

determinism-drift

"Rendering the same fixture twice must produce byte-identical HTML."

Venue: check — Rendering the same fixture twice must produce byte-identical HTML.

Every fixture in the section’s invariant corpus is server-rendered twice, and the two outputs are compared byte for byte. Any difference is a violation.

The corpus is broader than the fixtures you wrote:

  • your base fixture;
  • each of your states fixtures;
  • one synthesized fixture per declared slot your base populates, with that slot set to its empty value (null for a scalar or an image, [] for a list).

You do not declare that last set. Nullability coverage is derived from the schema — from the slot’s type alone, with no exemption for what else it declares — so an empty branch cannot be forgotten by forgetting to write a fixture for it. It only runs one way, though: a slot your base leaves empty is skipped, so base should fill every slot it can.

no-nondeterminism bans the clock and randomness in your code. A linter reads one file at a time and can only report on what it can see, so it stops at your source. This check runs the render.

That gap is where the real bugs live, and they arrive through a package. A helper that mints a fresh id per call. A library that iterates a hash and hands back a different order each time. Nothing in your Renderer looks nondeterministic — you never wrote Math.random() — but the output moves.

It matters because the publisher and the editor both render the section and must agree. Byte-identical output is what makes a section’s HTML content-addressable: the same content must hash to the same page. A section whose HTML changes on every render re-publishes forever and never converges.

If the nondeterminism is your own: compute the value at fill time and pass it in as a slot value, or move the markup into a "use client" island — an island renders in the browser after hydration, and its output is nobody’s content hash.

If the nondeterminism came from a package: replace it, or give it a deterministic input. An id generator gets a stable id derived from props. A collection gets sorted before you map it.

sections/facts/Renderer.tsx
import { bindSlots, Section, Slot } from "@homepages/template-kit";
import { nanoid } from "nanoid";
import { schema } from "./schema.js";
import type { Props } from "./schema.js";
export function Renderer({ slots, nav }: Props) {
const slot = bindSlots(schema, slots);
const descriptionId = nanoid(); // ← a new id on every render; the HTML differs each time
return (
<Section id={nav.selfAnchor}>
<Slot.Text slot={slot.headline} as="h1" aria-describedby={descriptionId} />
<Slot.Text slot={slot.summary} as="p" id={descriptionId} />
</Section>
);
}

Nothing here reads the clock or a random source, so no lint rule fires. The section still renders differently every single time.

The id is derived from something the section was given, so it is the same on every render:

sections/facts/Renderer.tsx
import { bindSlots, defineSchema, Section, Slot, text } from "@homepages/template-kit";
import type { SectionProps } from "@homepages/template-kit";
const schema = defineSchema({
label: "Facts",
anchor: "facts",
slots: {
headline: text.medium({ label: "Headline" }),
summary: text.long({ label: "Summary" }),
},
});
type Props = SectionProps<typeof schema>;
export function Renderer({ slots, nav }: Props) {
const slot = bindSlots(schema, slots);
const descriptionId = `${nav.selfAnchor}-summary`;
return (
<Section id={nav.selfAnchor}>
<Slot.Text slot={slot.headline} as="h1" aria-describedby={descriptionId} />
<Slot.Text slot={slot.summary} as="p" id={descriptionId} />
</Section>
);
}
  • no-nondeterminism — the same ban, enforced in your editor, on the code a linter can see.
  • Server vs client — why a server-rendered file must be a pure function of its props, and why an island is not.
  • render-invariant — the other check that runs over this same fixture corpus.