Rendering fields
Ounce stores, validates and describes your content. It does not render it. The component in your template is the only thing that decides what actually reaches a visitor, and it is the only part of the chain that can fail without saying so.
Four places
Section titled “Four places”Adding one field to a section form makes the same promise in four places:
| To | Through | Fails how |
|---|---|---|
| The editor | A control in the admin form | Loudly. A missing control is visible |
| The AI | A writable path in describe() |
Loudly. A rejected write is reported back |
| History | A row in the field level diff | Loudly. A missing row is visible |
| The visitor | Your template component | Silently |
The first three are generated from the registry, so they cannot drift. The fourth is hand written, so it can. When a template does not render a field, the admin still shows the control, the editor still fills it in, the value still validates, the revision still records it, and the page still comes out without it. Nothing errors.
Total conditions
Section titled “Total conditions”Every writable path needs a rendering path. When a field’s rendering is conditional on another field, the condition has to be total: cover every value the other field can hold, starting with its default.
The fix in that case was a second branch, not a narrower one:
---const hasImage = Boolean(block.image?.link);const aside = hasImage && block.layout !== 'center';const banner = hasImage && block.layout === 'center';---aside || banner === hasImage for every value of layout. If that identity
does not hold for your branches, some value of the enum drops the field.
Adding a field
Section titled “Adding a field”-
Give the schema a
.default(). Live rows do not have the new key. Without a default,safeParsefails for the whole entry and the section falls back to seed content with nothing logged. -
Add it to the form, with
helpfor the editor and anai.purposefor the assistant. -
Render it in every branch of your component, including the branch the default value takes.
-
Render the page and look for the value. Not the admin, not the preview: the page. Fetch it and search the HTML for the string you typed in.
Handled for you
Section titled “Handled for you”Do not hand roll these in a template. They run before your component is called.
- Disabled list items are removed.
stripDisabledwalks the loaded view and drops any array item whoseenabledisfalse, at any depth. A template that filters onenableditself is writing dead code, and one that forgets to is still correct. - Richtext is sanitised on write, so
set:htmlon a stored richtext value is safe. A value that reaches your component has already been through the sanitiser. - Missing keys are filled by Zod defaults, so optional fields arrive as their
default rather than
undefined.
What is left for the template is layout and nothing else.
Repeaters and sections
Section titled “Repeaters and sections”A list field repeats data. It says nothing about whether the result renders as
one band per item or as a single compact block. That is the component’s choice,
and it is invisible in the schema, the admin and the diff.
If a section holding a list renders one full width section per item, then asking for “a repeater rather than five sections” cannot be satisfied by that section, however well the data fits. The fix is a section type whose component renders the list compactly. See Section types.
Unimplemented types
Section titled “Unimplemented types”A template may legitimately not implement a section type: sectionTypes is a
Partial<Record<SectionKey, ...>> and an unmapped key renders nothing. Field
types have no such escape. A field type is registered engine-wide, so every form
that uses it gets an admin control, and every template that receives its value
is expected to render it. If you register a field type, implement it everywhere
it can appear.