Skip to main content

author-composition-sections

When to use​

Author a Section composition by API: the scoping rules (P22-P25), selectedField priority, the section-slot node shape, and the wrapper + section-slot pattern.

Part of the API-authoring set. Start at author-composition-via-api, which routes to this one.

Authoring a Section composition: scoping rules (P22-P25)​

So far the recipes have been about page-level compositions (Templates). Authoring Section compositions via API has its own contract: sections have no data of their own, so how they get the right scope at runtime is non-obvious.

STOP before writing the section: ask the Repeater question. If its selected_field is a multiple: true group, global field, file or link, ask the user "This field holds multiple items. Should the section show every item (adds a Repeater) or one item?" and wait. Ask even when the request seems to imply an answer: Studio asks every time. The answer sets cardinality and the root Repeater (understand-linked-schemas). Never ask for a reference, modular-block, single-value or whole-entry section.

How a Section gets data when placed on a Template: (P22)​

A Section composition is data-less when authored standalone (its own canvas has nothing to bind against). It only gets data when placed inside a Template. The Template hands it a scoped slice of the page entry.

The SDK's scoping pipeline:

Section's dataSources.template = <scoped slice of pageEntry at selectedField, with references resolved>
  • selectedField empty/absent makes the section's template the whole page entry (with any reference following). Use this when a section reads multiple fields off the page.
  • selectedField = "products" makes the section's template the products field's value (an array, with references resolved). Use this when a section iterates one field.
  • selectedField = "hero.cta" (dotted path) traverses. Each segment can drill into nested groups or follow references.
  • selectedField does NOT traverse array indices. The dotted path drills nested Groups and single references, but a numeric segment into a multi-value field (modular_blocks.1.…, products.0.…) resolves the section's template to null (verified: section_scoped_data[uid].template came back null, whole section blank). To iterate an array nested inside a Modular Block or at a fixed index, leave selectedField empty (whole-entry scope) and put the full indexed path on the Repeater's items binding instead: value.path = { "modular_blocks": { "1": { "image_grid": { "image": {} } } } }. The binding-path flattener keeps numeric keys even though the scoping walk does not. Reserve selectedField for top-level Group / reference fields.

This means the section's own canvas always shows one empty placeholder (no data exists standalone). That's not a bug. The section only fills out at template-rendering time. (See build-section for the UI-side framing.)

Where selectedField is read from: three sources, in priority order (P23)​

collect-data-needs.ts (runtime) and sectionInstanceBindings.ts (editor) BOTH read in this order:

  1. node.metadata.sectionBindingOverride.selectedField: set on the template's section-composition node when API-placing the section
  2. node.metadata.selectedField: legacy fallback on the same node
  3. The section's linked_schemas default: declared on the section composition itself: [{ content_type_uid: "<page-ct>", selected_field: "<field>" }]

The Studio web UI sets all three correctly when you drag a section onto a template. API authors set them explicitly.

The linked_schemas-as-reference trap (P23)​

The compositions CT's linked_schemas MUST be a group-multiple field, NOT a reference field. If it's a reference field, the CMA silently drops any {content_type_uid, selected_field} object you PUT to it, every read comes back linked_schemas: []. The editor reads linked_schemas to derive the UPDATE_SECTION_CONTEXTS push that scopes each placed section instance. An empty linked_schemas means an empty scope push, meaning the editor canvas shows blank repeaters even though fetchCompositionData (runtime) renders correctly via the override.

Two outcomes from this:

  • Proper fix: model linked_schemas as a group-multiple on the compositions CT. See provision-studio-project for the CT field schema. This makes every UI-driven path work without manual overrides.
  • Runtime escape hatch (if you can't change the CT shape right now): set metadata.sectionBindingOverride on each template's section-composition node:
    {
    "type": "section-composition",
    "uid": "INST-1",
    "metadata": {
    "compositionUID": "<the-section's-entry-uid-blt…>",
    "sectionBindingOverride": { "selectedField": "products" }
    }
    }
    This makes the runtime/SSR path correct (collect-data-needs.ts honors it). The editor canvas will still show blank repeaters until linked_schemas is also populated. So this escape hatch covers production rendering but doesn't unblock editor authoring: both must agree.

The editor vs runtime asymmetry (P23, in detail)​

PathWhat it readsSource file
Runtime / SSR (sdk.fetchCompositionData)sectionBindingOverride.selectedField first, then metadata.selectedField, then the linked_schemas defaultcollect-data-needs.ts
Editor canvas (live editing in Studio web app)UPDATE_SECTION_CONTEXTS push from the Studio web app, derived from the section's linked_schemas field. sectionBindingOverride is also honored as an override on top.sectionInstanceBindings.ts + Studio web app messaging

So you can have a section that renders perfectly on the deployed site (runtime path uses the override) yet shows empty repeaters in the editor (editor path's linked_schemas is empty). Both metadata paths must point to the same selectedField for editor + runtime to agree.

Recipe: section iterates one field on the page (P24)​

When a section's selectedField is set to the iterated field (e.g. selectedField: "products" and the section's job is to render one card per product), the section's dataSources.template IS the products array. A top-level Repeater inside the section must bind to scope-root, NOT to { path: { products: {} } }:

{
"type": "repeater",
"uid": "R-section-products",
"metadata": { "mode": "preview" },
"props": {
"items": {
"binding": {
"type": "template",
"value": { "path": {} } // ← scope-root, NOT { products: {} }
}
}
},
"slots": {
"<R-slot>": [
// ConditionBlock + card here, per the multi-reference recipe;
// card bindings still use type:"repeater" with repeaterUID
]
}
}

getBindingStringForCS flattens {path: {}} to "", so the binding resolves to dataSources.template directly. With selectedField: "products", that IS the products array. A path of {products: {}} would resolve to dataSources.template.products, undefined when the section's template IS the array, not an object containing one.

Reference iterations still need data_sources.resolvedReferences on the composition: key "template", value is the array of field paths on the parent entry (e.g. "template": ["products"]). The runtime resolves these along the selectedField path. You set them on the parent (template) composition, not the section composition.

Decision rule: single-field repeater section vs whole-entry section (P25)​

Section's jobselectedFieldRepeater binding (if any)When to use
One repeater over one field of the page (a card grid, a feature list)Set to that field ("products")Scope-root { path: {} }Most reusable sections: Editions list, Features grid, Card grid
A custom component reading several fields off the page entry (header reading brand + nav_links + signin_label. Hero reading the hero group)Leave unset: section's template is the whole page entryOriginal template.<field> paths work verbatimSingle-purpose sections with no iteration: Header, Hero, static 3D block

The choice is data-shape-driven: one field iterated calls for selectedField plus a scope-root. Many fields read calls for whole-entry scope and the original paths.

Verifying a section's scope headlessly (P26 preview)​

The section's own canvas can't confirm binding correctness (no data exists standalone). Use the SSR cold-load instead:

const spec = await sdk.fetchCompositionData({ url: "/<some-page-using-the-section>" });
console.log(JSON.stringify(spec.data.section_scoped_data["<instance-uid>"], null, 2));
// → { selectedField: "products", parentRepeaterUID: null,
// template: [<resolved entry 1>, <resolved entry 2>, ...] }

spec.data.section_scoped_data[<instance-uid>] is the runtime resolution: selectedField + the actual scoped template array. If template shows the right array length and right entry sample, the section's scope is correct. If it's undefined or wrong-shaped, the bindings won't work.

section-slot node shape: the one legitimate empty-slots node​

The built-in section-slot type declares a placeholder inside a section's ui. The template that embeds this section then fills the slot via a section-composition node (see next recipe: Pattern B). Non-obvious shape:

{
"type": "section-slot",
"uid": "SS1",
"props": {},
"slots": { "SS1": [] } // ← key is the node's OWN uid; value is [] until filled
}

Two things that trip API authors:

  1. slots.<own-uid>: [] is required: the section-slot's own uid appears as the slot key, with an empty array. This is the ONE legitimate exception to the preflight rule "no empty slot arrays". Omitting the entry (no slots at all, or slots: {}) leaves the template unable to target the slot when composing.
  2. The slot is filled by the TEMPLATE, not the section. In the section's authored tree the slot stays empty. The template's section-composition node holds the fill in its own slots, keyed by the section-slot uid (Pattern B).

Section slots typically sit inside a Repeater + Condition Block on the wrapper section (Pattern B for reference / MB iteration) or directly under the section root (Pattern B for a simpler single-slot section).

Pattern B: wrapper + section-slot, filled by another Section​

When the leaf content belongs in its own Section (reused across wrappers, different per content type, or swapped per template), a wrapper List Section exposes a section-slot per item and the template fills it. Recipe and node trees: author-composition-section-slot-fill.

Report a problem with this page