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.
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:
- node.metadata.sectionBindingOverride.selectedField: set on the template’s section-composition node when API-placing the section
- node.metadata.selectedField: legacy fallback on the same node
- 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-composition's-composable_uid>", "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)
| Path | What it reads | Source file |
|---|---|---|
| Runtime / SSR (sdk.fetchCompositionData) | sectionBindingOverride.selectedField first, then metadata.selectedField, then the linked_schemas default | collect-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 job | selectedField | Repeater 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 entry | Original template.<field> paths work verbatim | Single-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:
- 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.
- 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 carries a sectionSlotCompositions map (Pattern B recipe below) that pairs each section-slot uid with the composable_uid of the fill section.
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).
Recipe: Pattern B (wrapper + section-slot, filled by a Simple Section)
Sibling to the multi-reference recipe. Same iteration outcome (one card per reference / MB item), but the leaf content lives in a separate section composition rather than in the wrapper’s own ui. Verified end-to-end for references (Case 8) and modular blocks (Case 9) in docs/api/sections.md.
When to prefer Pattern B over Pattern A (self-contained):
- Leaf content is reused across multiple wrappers (e.g. an author card shown in blog lists AND author-page hero).
- Leaf content varies per CT and you want each CT’s binding tree in its own composition entry.
- You want to swap the fill section per template instance without editing the wrapper.
Shape overview:
| Composition | Kind | Role |
|---|---|---|
| Wrapper section | List Section: Repeater at root + CB per CT/block, CB slot holds a section-slot node | Iterates the reference/MB, emits one slot per item |
| Fill section (one per branch) | Simple Section: no root Repeater, linked_schemas.selected_field scoped to the reference target CT (or block) | Binds leaf fields |
| Template | Has a section-composition node embedding the wrapper. Its sectionSlotCompositions pairs the wrapper’s section-slot uid with each fill section’s composable_uid | Wires the slot to the fill |
Wrapper section: Repeater + CB + section-slot
// ui tree of the wrapper section
{
"type": "repeater", "uid": "R1",
"metadata": { "mode": "preview" },
"props": { "items": { "binding": { "type": "template", "value": { "path": {} } } } },
"slots": { "<R1-slot>": [
{ "type": "condition-block", "uid": "CB1",
"metadata": { "condition": { "type": "reference", "value": "author" } },
"slots": { "<CB1-slot>": [
{ "type": "section-slot", "uid": "SS1",
"slots": { "SS1": [] } }
] } }
] } }
Wrapper section entry also declares linked_schemas: [{ content_type_uid: "<parent-CT>", selected_field: "<ref-or-mb-field>" }] (group-multiple field, see P23).
Fill section: Simple Section per branch
One fill section per allowed CT (references) or block (MB). No root Repeater. Its top-level node is the leaf component. linked_schemas.selected_field names the reference target CT (or block schema path) so the SDK scopes each iteration’s item into the section’s dataSources.template.
// ui tree of the fill section (e.g. author-card)
{
"type": "author-card", "uid": "AC1",
"props": {
"name": { "binding": { "type": "template", "value": { "path": { "name": {} } } } },
"photo": { "binding": { "type": "template", "value": { "path": { "photo": { "url": {} } } } } }
}
}
Note the fill section’s bindings use type: "template" (NOT type: "repeater"). The wrapper’s SDK scoping already unwraps the iteration item into the fill section’s template scope.
Template: node + sectionSlotCompositions
The template’s section-composition node embedding the wrapper carries a sectionSlotCompositions map keyed by section-slot uid, whose value is the fill section’s composable_uid:
{
"type": "section-composition",
"uid": "INST-1",
"metadata": {
"compositionUID": "<wrapper-section's composable_uid>",
"sectionSlotCompositions": {
"SS1": { "compositionUID": "<author-fill-section's composable_uid>" }
// add one entry per CB branch when references allow multiple CTs; each
// branch's section-slot gets its own uid, each paired with its own fill
}
}
}
Also populate the template’s linked_sections (P27) with references to BOTH the wrapper section entry AND every distinct fill section entry.
Assembled: reference-iterating wrapper filled by an author fill
Wrapper section linked_schemas: [{ ct: "blog_post", selected_field: "primary_author" }]
ui: Repeater(items:{path:{}}, mode:"preview")
└── ConditionBlock(condition:{type:"reference", value:"author"})
└── section-slot uid:"SS1" slots:{"SS1":[]}
Author fill section linked_schemas: [{ ct: "blog_post", selected_field: "primary_author" }]
ui: author-card
├── name: binding template path:{name:{}}
└── photo: binding template path:{photo:{url:{}}}
Template (blog_post)
data_sources: [{ uid:"template", resolvedReferences:{ "template":["primary_author"] } }]
linked_sections: [wrapper-entry-ref, author-fill-entry-ref]
ui: page → … → section-composition uid:"INST-1"
metadata.compositionUID: "<wrapper composable_uid>"
metadata.sectionSlotCompositions.SS1.compositionUID: "<author-fill composable_uid>"
MB variant (Case 9): swap the CB discriminator to { type: "modular_block", value: "<block-uid>" }. Wrapper’s selected_field names the MB field. Fill section’s selected_field names the same MB path plus the block key. Leaf bindings inside the fill section still use type: "template".