Pick a fixture scenario
Source a fixture's slots from real property data, via defineFixtures or by the fill-by-source rule.
Fill a section’s fixtures.ts from the kit’s golden scenarios instead of inventing
values, so preview stays honest and every fixture is grounded in a real,
self-consistent property.
Prerequisites
Section titled “Prerequisites”- A section with a
fixtures.tsto fill — Install scaffolds one.
Pick a scenario
Section titled “Pick a scenario”scenario(key, authoredSlots) — the entry point for defineFixtures
— takes a ScenarioKey directly, so an unknown key is a compile error:
export const fixtures = defineFixtures( schema, (scenario) => ({ base: scenario("luxuryMultiUnit", { /* authored slots only */ }) }),);The export name is part of the contract: check and the preview server load
fixtures.ts’s export const fixtures binding by name. There is nothing else to pass:
defineFixtures takes the schema and the build callback, and reads the fill decisions off
the schema itself — every decision rides on the slot it fills, so there is no second
declaration for a fixture to be handed.
base almost always builds on luxuryMultiUnit — it’s the breadth ceiling, so a
layout that survives it survives everything smaller. Reach for sparseSingleFamily
or forRentCondo for a states entry designed to look
different against a thinner property — pass a different key to that state’s own
scenario() call, since a defineFixtures state resolves independently and never
inherits base’s scenario.
| Scenario | units | listing_intent | property_type | contacts | pois | poi photos | photos | contact logos | media | documents |
|---|---|---|---|---|---|---|---|---|---|---|
luxuryMultiUnit |
12 | for_sale | multi_family | 3 | 24 | 18 of 24 | 12 | both logos | 3 videos (1 upload, 2 links) + 3D tour | 3 |
sparseSingleFamily |
1 | for_sale | single_family | 1 | 4 | none | 3 (reused from forRentCondo) |
none (null) | empty video pool, no 3D tour | none |
forRentCondo |
1 | for_rent | condo_townhome | 2 | 8 | 5 of 8 | 8 | both logos | 1 video (upload), no 3D tour | none |
poi photos counts the rows whose photo is stated — a POI’s image rides on the
row rather than in a slot, and it is absent on many places for real
(how to state one). The
scenarios that carry them keep some rows bare on purpose, so a section is exercised
against both; sparseSingleFamily is the thin-property case and carries none.
documents counts s.documents, the listing’s attached files — a brochure, a
disclosure pack. They are optional collateral rather than something every listing
has, so only luxuryMultiUnit carries any and the other two are the empty branch a
document slot has to render.
Reading a scenario’s raw data directly (rather than through scenario()) is still
useful — for the helper cheat-sheet below, and for the values an .override(…)
states when a scenario cannot produce them.
Import scenarios from the same fixtures entry — never the root barrel; dev-only
sample data must never reach a published renderer bundle’s import graph:
import { scenarios } from "@homepages/template-kit/fixtures";
const luxury = scenarios.luxuryMultiUnit;const sparse = scenarios.sparseSingleFamily;const rent = scenarios.forRentCondo;Fill an authored slot by its fill-spec source
Section titled “Fill an authored slot by its fill-spec source”defineFixtures resolves a fact-bound or compute()-bound slot
on its own — you only author what’s left: a plain content slot, or one behind a
fill decision. scenario()’s authored-slots
argument subtracts every source-bound key from the section’s schema, so writing one
there is a compile error rather than a value that quietly loses. Don’t invent a value
for the rest either — ground them in the same scenario:
| Slot’s fill-spec source | How the fixture fills it |
|---|---|
a fact (fact.<container>.<field>) |
resolved automatically by defineFixtures; read straight off the scenario (s.facts.address.display, primaryContact(s).fields.phone) only to state an .override(…) |
compute() |
resolved automatically by defineFixtures; a helper (propertyMetrics(s), poisByCategory(s), contactCards(s)) if you need the value itself |
| a fill decision | a short sample fill grounded in the scenario’s facts — a headline about this property, never lorem ipsum or invented facts — authored either way |
A fixture that gets this wrong is worse than a missing one: a slot bound
fact.unit.beds given an authored value that doesn’t match s.facts.units[0].beds
teaches nothing about the layout it’s supposed to preview — one more reason
defineFixtures is the shape to reach for: a fact-bound slot cannot drift from the
scenario, because nothing you write can override it short of .override().
The helper cheat-sheet
Section titled “The helper cheat-sheet”Every helper is a named export of the fixtures subpath, so reaching for one means
adding it to the fixture file’s existing import — the scaffolded file takes only
defineFixtures and scenario. scenarios and propertyMetrics are the two the
worked example below uses:
import { contactCards, poisByCategory, primaryContact, propertyMetrics, scenarios,} from "@homepages/template-kit/fixtures";
const s = scenarios.luxuryMultiUnit;| Helper | Returns |
|---|---|
primaryContact(s) |
The scenario’s primary contact (ContactFixture, position 0 by construction) — throws if the scenario has none |
contactCards(s) |
Every attached contact, in order (ContactFixture[]) |
poisByCategory(s) |
The scenario’s POIs as PoiRows, bucketed by category (Record<PoiFixture["category"], PoiRow[]>) |
propertyMetrics(s) |
beds/baths/sqft/price off the lowest-position unit, and bedsMin/bedsMax/bathsMin/bathsMax/sqftMax/priceStart across all units |
propertyMetrics returns kit fact vocabulary — bedsMin, priceStart and siblings, not
a template’s own slot names, so the kit never learns what a section calls its slots. Map
it to your slots at the call site, off a const m = propertyMetrics(s); in the same
module:
// fixtures.ts — in the base scenario's authored slotsbeds_min: m.bedsMin,price_start: m.priceStart,Fields the helpers don’t cover are readable straight off the scenario: s.photos,
s.pois, s.amenities, s.contacts, s.media, s.documents, and s.facts (the raw
PropertyFacts). A contact’s org name and logos (logo_light_id/logo_dark_id) live
on ContactFixture["fields"], not on a sibling “brand” object — read them off
primaryContact(s).fields or s.contacts.
Media comes off the scenario as an id
Section titled “Media comes off the scenario as an id”Every media reference in a scenario is an asset id, the same thing a saved page
holds — s.photos[n].id, primaryContact(s).fields.headshot_id / .logo_light_id /
.logo_dark_id, an upload row’s id in s.media.videos, s.documents[n].id, and a
floor plan ref’s id.
Put the id in the fixture through the typed constructors — media.photo(id) for any
image slot, media.video(id) for a hosted video, media.document(id) for a document
slot (a document list maps the array: s.documents.map((d) => media.document(d.id)))
— and stop there: the url, the alt fallback, the responsive ladder, and a document’s
filename, content_type and size_bytes are attached for you when the fixture
loads. A url is never statable in a
fixture; media.video.link(url, provider?, { poster? }) is the one deliberate exception,
because a provider embed’s url IS the saved value (its poster, though, is still a pack
image id, never a URL). (s.media.tour_3d_url stays a plain URL
too — a 3D tour is a third-party embed with no asset behind it.)
s.media.videos is the whole candidate pool, not one selection, and it mixes both
kinds: an upload row names a pack asset, a link row states the url an agent
pasted. Branch on source before reading a row — only an upload row has an id
media.video() accepts, and only a link row has a url.
Add a scenario-driven state
Section titled “Add a scenario-driven state”A states entry designed to look different against a sparser property sources from a
different scenario, not from overridden literals — see
Preview edge cases
for the full pattern.
Complete diff
Section titled “Complete diff”The defineFixtures call under Steps is the whole change — nothing outside
fixtures.ts moves.
Verify
Section titled “Verify”Verify with the author loop.
Rules that can fire
Section titled “Rules that can fire”fixtures-invalid— the export binds a plain object rather than the moduledefineFixturesreturns, or that module is malformed.section-unrenderable— the scenario you picked cannot supply a fact one of the section’s slots needs, sofixtures.tsthrows while it loads.
See also
Section titled “See also”fixtures.ts— thefixtures.tscontract and the fill-by-source rule.- Golden scenarios — the scenario breadth table.
- Preview edge cases —
states, including a scenario-swapped state. - The fact vocabulary — the facts a scenario’s
factssupplies.