Crop an image slot
Add an image slot the editor crops to a fixed ratio.
Add an image slot the editor crops to a fixed ratio.
Prerequisites
Section titled “Prerequisites”- A section to add the slot to — Install scaffolds one.
- Its
fixtures.tsbuilt from a scenario, so the photo id resolves — Pick a fixture scenario.
Starting from a section you already have, add
an image slot — a crop config, the decision that fills it, an <Image>
in the Renderer, and a fixture value.
Pick the builder for the kind you want and pass only that kind’s options: image()
takes a crop, and a text slot does not, so a misplaced option is a compile
error rather than something check finds later.
Add image and imageAssign to the section’s existing kit import — the scaffolded
file takes only defineSchema, text and textBlock, so both are
missing until you do.
The slot and the decision that fills it are one declaration, in one file (Sections and declarations):
// section.ts — an image slot, own aspect, in `defineSchema`'s `slots`interior_image: image({ label: "Interior image", crop: { mode: "locked", aspect: 4 / 3, aspectLabel: "4:3" }, fill: imageAssign({ pick: "Pick a bright, decluttered interior shot.", }),}),A slot is editable by default, so nothing is written out: locked: true is what
takes a slot away from the user.
The slot is the whole binding. Its key mints the decision’s id and its produces — so
neither is yours to write or keep in agreement. imageAssign is the builder for an
image slot; giving this slot a textBlock instead is a compile error, not a check
finding.
// Renderer.tsx — a Slot.Image usage, own aspect<Slot.Image slot={slot.interior_image} className="w-full" />Slot.Image reads url, alt, responsive and mobile off the binding’s value, takes
the slot’s own declared frame off its def, and owns the marker — so this call site
states neither the geometry nor the marker, and cannot drift from what section.ts says.
Last, the fixture value. Add media and scenarios to the fixture file’s existing
import from @homepages/template-kit/fixtures — the scaffolded file takes only
defineFixtures, so they are missing until you do.
// fixtures.ts — the photo's asset id, exactly as a saved page states itinterior_image: media.photo(scenarios.luxuryMultiUnit.photos[3]!.id),A fixture states the asset reference and nothing else: the served url, the alt
fallback and the responsive ladder are all attached when the fixture loads — see
Media is stated by id.
An image slot supports two independent geometry configs — use one, not both, per slot:
crop— steers the editor’s cropper only:mode: "locked"fixes anaspect(w/h) and shows theaspectLabelpill;mode: "free"lets the user resize any edge. This is what the steps above use.frame— a per-breakpoint aspect (desktop/mobile, switching at a declaredbreakpoint) that is the single source of truth for BOTH the cropper’s target ratio and the Renderer’s own layout box (imported as a shared constant on both sides — see the schema declaration). Reach forframeinstead ofcropwhen the section’s markup itself needs to know the ratio (e.g. to size a wrapper), not just the editor.
frame.breakpoint is one thing only: the viewport width at which the crop
switches from the mobile aspect to the desktop one. It is a <picture>
switch over which cropped rendition is served, and it means nothing else.
In particular it carries no obligation to match your layout’s own
breakpoints. A section whose columns reflow at lg (1024px) may perfectly
well switch its hero crop at 425px — the two answer different questions (“when
does this grid stack?” versus “when is the tall crop the better picture?”) and
picking one number for both is a coincidence, not a rule. Choose each on its own
merits and don’t chase agreement between them.
Either way, framing is baked into the served asset — the Renderer never
applies an aspect ratio to the pixels itself; it only reads url, alt,
responsive (and mobile) off the slot’s ImageValue.
Complete diff
Section titled “Complete diff”Three additions across the section folder’s three files — section.ts (the slot,
carrying the decision that produces it), Renderer.tsx, fixtures.ts — plus
image and imageAssign on section.ts’s existing kit import and media and
scenarios on the fixture file’s:
// section.ts — in `defineSchema`'s `slots`interior_image: image({ label: "Interior image", crop: { mode: "locked", aspect: 4 / 3, aspectLabel: "4:3" }, fill: imageAssign({ pick: "Pick a bright, decluttered interior shot.", }),}),// Renderer.tsx — wherever the image belongs in the markup<Slot.Image slot={slot.interior_image} className="w-full" />// fixtures.ts — in the authored-slots objectinterior_image: media.photo(scenarios.luxuryMultiUnit.photos[3]!.id),Verify
Section titled “Verify”Verify with the author loop.
Rules that can fire
Section titled “Rules that can fire”schema-invalid— thecropconfig saysmode: "locked"and states noaspect, which is the one shape the cropper cannot act on.missing-slot-marker— the new editable 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.render-invariant— the fixture value resolves to no url, so the section renders a src-less<img>.
See also
Section titled “See also”Renderer.tsx—ImageValue, the fields a Renderer reads vs. only carries, andframe.- Slot primitives — the
Imageprimitive’s full prop table.