Skip to content

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.

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.

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.

  1. Give the schema a .default(). Live rows do not have the new key. Without a default, safeParse fails for the whole entry and the section falls back to seed content with nothing logged.

  2. Add it to the form, with help for the editor and an ai.purpose for the assistant.

  3. Render it in every branch of your component, including the branch the default value takes.

  4. 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.

Do not hand roll these in a template. They run before your component is called.

  • Disabled list items are removed. stripDisabled walks the loaded view and drops any array item whose enabled is false, at any depth. A template that filters on enabled itself is writing dead code, and one that forgets to is still correct.
  • Richtext is sanitised on write, so set:html on 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.

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.

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.