Skip to content
HomePagesHomePages template kit

Compute a slot's value

Derive a slot's value from the property's facts with a function you write, instead of copying a fact or having AI write it.

Derive a slot’s value from the property’s facts with a function you write, rather than copying a single fact or handing the slot to AI.

  • A section to add the slot to — Install scaffolds one.
  • Its fixtures.ts built from a scenario, so the facts the function reads resolve in preview — Pick a fixture scenario.

A slot’s value arrives one of three ways, and it takes exactly one of them. source: fact.<container>.<field> copies a known fact verbatim (bind a slot to a property fact). A fill: on the slot hands it to the model (have AI write a slot’s content). compute() is the third: a function of yours over the same facts, run at derive time, deterministic and never AI.

Add number and compute to the section’s existing kit import — the scaffolded file takes only defineSchema, text and textBlock, so both are missing until you do.

It goes where a fact binding would — under the same source:

// section.ts — a value derived from the facts, in `defineSchema`'s `slots`
photo_count: number({
label: "Photos",
source: compute((f) => f.photos.length),
}),

f is the derive-time facts view. It is the same vocabulary a binding names: f.property.* carries every fact.property.* fact, f.unit and f.contact are the primary unit and contact card themselves, and beside them sit the things no slot can bind — the property’s photo, video, tour, floor-plan and document pools, the notes distilled from those documents, its always-on narrative, and the ambient year. Every fact the platform holds is reachable by construction, and every leaf is typed, so a missing unit is a null you have to handle rather than a crash in production. The fact vocabulary lists every entry and the shape it reads as.

The return type is checked against the slot it binds — a number() admits a number or null, since a number slot’s blank is null. An async callback fails to typecheck rather than silently running asynchronously at derive time, and the function must stay pure for the same reason a renderer must: the publisher and the editor have to produce the same output from the same facts. Reach for the ambient year rather than the clock.

There is no second name to keep in sync and no fill decision to write. defineSchema extracts the function keyed by the slot’s own name, so the slot key is the handle, and a computed slot is source-bound exactly as a fact-bound one is — it takes a source:, so giving it a fill: as well does not compile.

A computed slot is still an ordinary slot on the page, and still owes the editor a marker:

// Renderer.tsx — a computed slot renders like any other
<Slot.Number slot={slot.photo_count} as="span" format={(n) => `${n} photos`} />

format is a callback this markup owns — it shapes how the number reads at this one call site, and it is the only formatting seam there is: nothing about presentation is declared on the slot, and the kit ships no catalog of format names to reach for. It runs only on a filled value, and it withholds the text leaf, so an edit to a formatted slot round-trips through a re-render rather than typing live — see values and emptiness.

Fixtures come free with it. A computed slot resolves from the scenario the same way a fact-bound slot does, tagged computed where the provenance report prints it, so there is no fixture value to author — and authoring one is a compile error, not a value that quietly loses.

Two additions — one in section.ts, one in Renderer.tsx — plus number and compute on the section’s existing kit import. Nothing else in the section folder moves: a computed slot needs no fill entry and no fixture value.

// section.ts — in `defineSchema`'s `slots`
photo_count: number({
label: "Photos",
source: compute((f) => f.photos.length),
}),
// Renderer.tsx — a computed slot renders like any other
<Slot.Number slot={slot.photo_count} as="span" format={(n) => `${n} photos`} />

Verify with the author loop.

  • no-nondeterminism — the function reads the clock or a random source.
  • no-node-builtins-in-contract — the file imports a Node built-in to do the work; the derive-time bundle resolves no external specifier, so nothing there is reachable.
  • missing-slot-marker — the new slot never reaches the rendered DOM with its marker.
  • sidebar-order — the slot is declared in one position in the schema and rendered in another.